From d5f3d157264fceb173befcd3ea985e9a05569f9c Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 11 Jul 2025 10:54:19 -0700 Subject: [PATCH 01/57] feat: Add support for keyboard navigation to/from block comments. (#9227) * refactor: Update `TextInputBubble` to use `CommentEditor` for text editing. * feat: Designate `Bubble` as implementing `IFocusableNode`. * feat: Dismiss focused bubbles on Escape. * feat: Add support for keyboard navigation to block comments. * fix: Scroll comment editors rather than zooming the workspace. * chore: Add param to docstring. --- core/bubbles/bubble.ts | 33 ++++- core/bubbles/textinput_bubble.ts | 120 +++++------------- core/comments/comment_editor.ts | 6 + core/comments/rendered_workspace_comment.ts | 9 -- core/icons/comment_icon.ts | 6 +- .../block_comment_navigation_policy.ts | 76 +++++++++++ .../comment_editor_navigation_policy.ts | 54 ++++++++ core/keyboard_nav/icon_navigation_policy.ts | 7 + core/navigator.ts | 4 + core/workspace_svg.ts | 6 + 10 files changed, 221 insertions(+), 100 deletions(-) create mode 100644 core/keyboard_nav/block_comment_navigation_policy.ts create mode 100644 core/keyboard_nav/comment_editor_navigation_policy.ts diff --git a/core/bubbles/bubble.ts b/core/bubbles/bubble.ts index 20e730abb..c42e60254 100644 --- a/core/bubbles/bubble.ts +++ b/core/bubbles/bubble.ts @@ -9,7 +9,9 @@ import * as common from '../common.js'; import {BubbleDragStrategy} from '../dragging/bubble_drag_strategy.js'; import {getFocusManager} from '../focus_manager.js'; import {IBubble} from '../interfaces/i_bubble.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; +import type {IHasBubble} from '../interfaces/i_has_bubble.js'; import {ISelectable} from '../interfaces/i_selectable.js'; import {ContainerRegion} from '../metrics_manager.js'; import {Scrollbar} from '../scrollbar.js'; @@ -27,7 +29,7 @@ import {WorkspaceSvg} from '../workspace_svg.js'; * bubble, where it has a "tail" that points to the block, and a "head" that * displays arbitrary svg elements. */ -export abstract class Bubble implements IBubble, ISelectable { +export abstract class Bubble implements IBubble, ISelectable, IFocusableNode { /** The width of the border around the bubble. */ static readonly BORDER_WIDTH = 6; @@ -100,12 +102,14 @@ export abstract class Bubble implements IBubble, ISelectable { * element that's represented by this bubble (as a focusable node). This * element will have its ID overwritten. If not provided, the focusable * element of this node will default to the bubble's SVG root. + * @param owner The object responsible for hosting/spawning this bubble. */ constructor( public readonly workspace: WorkspaceSvg, protected anchor: Coordinate, protected ownerRect?: Rect, overriddenFocusableElement?: SVGElement | HTMLElement, + protected owner?: IHasBubble & IFocusableNode, ) { this.id = idGenerator.getNextUniqueId(); this.svgRoot = dom.createSvgElement( @@ -145,6 +149,13 @@ export abstract class Bubble implements IBubble, ISelectable { this, this.onMouseDown, ); + + browserEvents.conditionalBind( + this.focusableElement, + 'keydown', + this, + this.onKeyDown, + ); } /** Dispose of this bubble. */ @@ -229,6 +240,19 @@ export abstract class Bubble implements IBubble, ISelectable { getFocusManager().focusNode(this); } + /** + * Handles key events when this bubble is focused. By default, closes the + * bubble on Escape. + * + * @param e The keyboard event to handle. + */ + protected onKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape' && this.owner) { + this.owner.setBubbleVisible(false); + getFocusManager().focusNode(this.owner); + } + } + /** Positions the bubble relative to its anchor. Does not render its tail. */ protected positionRelativeToAnchor() { let left = this.anchor.x; @@ -694,4 +718,11 @@ export abstract class Bubble implements IBubble, ISelectable { canBeFocused(): boolean { return true; } + + /** + * Returns the object that owns/hosts this bubble, if any. + */ + getOwner(): (IHasBubble & IFocusableNode) | undefined { + return this.owner; + } } diff --git a/core/bubbles/textinput_bubble.ts b/core/bubbles/textinput_bubble.ts index 7479c06cf..0bad5fabc 100644 --- a/core/bubbles/textinput_bubble.ts +++ b/core/bubbles/textinput_bubble.ts @@ -4,7 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ +import {CommentEditor} from '../comments/comment_editor.js'; import * as Css from '../css.js'; +import {getFocusManager} from '../focus_manager.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {IHasBubble} from '../interfaces/i_has_bubble.js'; import * as touch from '../touch.js'; import {browserEvents} from '../utils.js'; import {Coordinate} from '../utils/coordinate.js'; @@ -21,12 +25,6 @@ import {Bubble} from './bubble.js'; * Used by the comment icon. */ export class TextInputBubble extends Bubble { - /** The root of the elements specific to the text element. */ - private inputRoot: SVGForeignObjectElement; - - /** The text input area element. */ - private textArea: HTMLTextAreaElement; - /** The group containing the lines indicating the bubble is resizable. */ private resizeGroup: SVGGElement; @@ -42,18 +40,12 @@ export class TextInputBubble extends Bubble { */ private resizePointerMoveListener: browserEvents.Data | null = null; - /** Functions listening for changes to the text of this bubble. */ - private textChangeListeners: (() => void)[] = []; - /** Functions listening for changes to the size of this bubble. */ private sizeChangeListeners: (() => void)[] = []; /** Functions listening for changes to the location of this bubble. */ private locationChangeListeners: (() => void)[] = []; - /** The text of this bubble. */ - private text = ''; - /** The default size of this bubble, including borders. */ private readonly DEFAULT_SIZE = new Size( 160 + Bubble.DOUBLE_BORDER, @@ -68,46 +60,47 @@ export class TextInputBubble extends Bubble { private editable = true; + /** View responsible for supporting text editing. */ + private editor: CommentEditor; + /** * @param workspace The workspace this bubble belongs to. * @param anchor The anchor location of the thing this bubble is attached to. * The tail of the bubble will point to this location. * @param ownerRect An optional rect we don't want the bubble to overlap with * when automatically positioning. + * @param owner The object that owns/hosts this bubble. */ constructor( public readonly workspace: WorkspaceSvg, protected anchor: Coordinate, protected ownerRect?: Rect, + protected owner?: IHasBubble & IFocusableNode, ) { - super(workspace, anchor, ownerRect, TextInputBubble.createTextArea()); + super(workspace, anchor, ownerRect, undefined, owner); dom.addClass(this.svgRoot, 'blocklyTextInputBubble'); - this.textArea = this.getFocusableElement() as HTMLTextAreaElement; - this.inputRoot = this.createEditor(this.contentContainer, this.textArea); + this.editor = new CommentEditor(workspace, this.id, () => { + getFocusManager().focusNode(this); + }); + this.contentContainer.appendChild(this.editor.getDom()); this.resizeGroup = this.createResizeHandle(this.svgRoot, workspace); this.setSize(this.DEFAULT_SIZE, true); } /** @returns the text of this bubble. */ getText(): string { - return this.text; + return this.editor.getText(); } /** Sets the text of this bubble. Calls change listeners. */ setText(text: string) { - this.text = text; - this.textArea.value = text; - this.onTextChange(); + this.editor.setText(text); } /** Sets whether or not the text in the bubble is editable. */ setEditable(editable: boolean) { this.editable = editable; - if (this.editable) { - this.textArea.removeAttribute('readonly'); - } else { - this.textArea.setAttribute('readonly', ''); - } + this.editor.setEditable(editable); } /** Returns whether or not the text in the bubble is editable. */ @@ -117,7 +110,7 @@ export class TextInputBubble extends Bubble { /** Adds a change listener to be notified when this bubble's text changes. */ addTextChangeListener(listener: () => void) { - this.textChangeListeners.push(listener); + this.editor.addTextChangeListener(listener); } /** Adds a change listener to be notified when this bubble's size changes. */ @@ -130,58 +123,6 @@ export class TextInputBubble extends Bubble { this.locationChangeListeners.push(listener); } - /** Creates and returns the editable text area for this bubble's editor. */ - private static createTextArea(): HTMLTextAreaElement { - const textArea = document.createElementNS( - dom.HTML_NS, - 'textarea', - ) as HTMLTextAreaElement; - textArea.className = 'blocklyTextarea blocklyText'; - return textArea; - } - - /** Creates and returns the UI container element for this bubble's editor. */ - private createEditor( - container: SVGGElement, - textArea: HTMLTextAreaElement, - ): SVGForeignObjectElement { - const inputRoot = dom.createSvgElement( - Svg.FOREIGNOBJECT, - { - 'x': Bubble.BORDER_WIDTH, - 'y': Bubble.BORDER_WIDTH, - }, - container, - ); - - const body = document.createElementNS(dom.HTML_NS, 'body'); - body.setAttribute('xmlns', dom.HTML_NS); - body.className = 'blocklyMinimalBody'; - - textArea.setAttribute('dir', this.workspace.RTL ? 'RTL' : 'LTR'); - body.appendChild(textArea); - inputRoot.appendChild(body); - - this.bindTextAreaEvents(textArea); - - return inputRoot; - } - - /** Binds events to the text area element. */ - private bindTextAreaEvents(textArea: HTMLTextAreaElement) { - // Don't zoom with mousewheel; let it scroll instead. - browserEvents.conditionalBind(textArea, 'wheel', this, (e: Event) => { - e.stopPropagation(); - }); - // Don't let the pointerdown event get to the workspace. - browserEvents.conditionalBind(textArea, 'pointerdown', this, (e: Event) => { - e.stopPropagation(); - touch.clearTouchIdentifier(); - }); - - browserEvents.conditionalBind(textArea, 'change', this, this.onTextChange); - } - /** Creates the resize handler elements and binds events to them. */ private createResizeHandle( container: SVGGElement, @@ -220,8 +161,12 @@ export class TextInputBubble extends Bubble { const widthMinusBorder = size.width - Bubble.DOUBLE_BORDER; const heightMinusBorder = size.height - Bubble.DOUBLE_BORDER; - this.inputRoot.setAttribute('width', `${widthMinusBorder}`); - this.inputRoot.setAttribute('height', `${heightMinusBorder}`); + this.editor.updateSize( + new Size(widthMinusBorder, heightMinusBorder), + new Size(0, 0), + ); + this.editor.getDom().setAttribute('x', `${Bubble.DOUBLE_BORDER / 2}`); + this.editor.getDom().setAttribute('y', `${Bubble.DOUBLE_BORDER / 2}`); this.resizeGroup.setAttribute('y', `${heightMinusBorder}`); if (this.workspace.RTL) { @@ -312,14 +257,6 @@ export class TextInputBubble extends Bubble { this.onSizeChange(); } - /** Handles a text change event for the text area. Calls event listeners. */ - private onTextChange() { - this.text = this.textArea.value; - for (const listener of this.textChangeListeners) { - listener(); - } - } - /** Handles a size change event for the text area. Calls event listeners. */ private onSizeChange() { for (const listener of this.sizeChangeListeners) { @@ -333,6 +270,15 @@ export class TextInputBubble extends Bubble { listener(); } } + + /** + * Returns the text editor component of this bubble. + * + * @internal + */ + getEditor() { + return this.editor; + } } Css.register(` diff --git a/core/comments/comment_editor.ts b/core/comments/comment_editor.ts index 69dadd884..ac1559c4b 100644 --- a/core/comments/comment_editor.ts +++ b/core/comments/comment_editor.ts @@ -53,6 +53,7 @@ export class CommentEditor implements IFocusableNode { 'textarea', ) as HTMLTextAreaElement; this.textArea.setAttribute('tabindex', '-1'); + this.textArea.setAttribute('dir', this.workspace.RTL ? 'RTL' : 'LTR'); dom.addClass(this.textArea, 'blocklyCommentText'); dom.addClass(this.textArea, 'blocklyTextarea'); dom.addClass(this.textArea, 'blocklyText'); @@ -86,6 +87,11 @@ export class CommentEditor implements IFocusableNode { }, ); + // Don't zoom with mousewheel; let it scroll instead. + browserEvents.conditionalBind(this.textArea, 'wheel', this, (e: Event) => { + e.stopPropagation(); + }); + // Register listener for keydown events that would finish editing. browserEvents.conditionalBind( this.textArea, diff --git a/core/comments/rendered_workspace_comment.ts b/core/comments/rendered_workspace_comment.ts index 3457e611a..c4c1f3d4e 100644 --- a/core/comments/rendered_workspace_comment.ts +++ b/core/comments/rendered_workspace_comment.ts @@ -74,15 +74,6 @@ export class RenderedWorkspaceComment this, this.startGesture, ); - // Don't zoom with mousewheel; let it scroll instead. - browserEvents.conditionalBind( - this.view.getSvgRoot(), - 'wheel', - this, - (e: Event) => { - e.stopPropagation(); - }, - ); } /** diff --git a/core/icons/comment_icon.ts b/core/icons/comment_icon.ts index 959eb2500..8f5a82c0d 100644 --- a/core/icons/comment_icon.ts +++ b/core/icons/comment_icon.ts @@ -11,7 +11,6 @@ import type {BlockSvg} from '../block_svg.js'; import {TextInputBubble} from '../bubbles/textinput_bubble.js'; import {EventType} from '../events/type.js'; import * as eventUtils from '../events/utils.js'; -import type {IBubble} from '../interfaces/i_bubble.js'; import type {IHasBubble} from '../interfaces/i_has_bubble.js'; import type {ISerializable} from '../interfaces/i_serializable.js'; import * as renderManagement from '../render_management.js'; @@ -62,7 +61,7 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { /** * The visibility of the bubble for this comment. * - * This is used to track what the visibile state /should/ be, not necessarily + * This is used to track what the visible state /should/ be, not necessarily * what it currently /is/. E.g. sometimes this will be true, but the block * hasn't been rendered yet, so the bubble will not currently be visible. */ @@ -340,7 +339,7 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { } /** See IHasBubble.getBubble. */ - getBubble(): IBubble | null { + getBubble(): TextInputBubble | null { return this.textInputBubble; } @@ -365,6 +364,7 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { this.sourceBlock.workspace as WorkspaceSvg, this.getAnchorLocation(), this.getBubbleOwnerRect(), + this, ); this.textInputBubble.setText(this.getText()); this.textInputBubble.setSize(this.bubbleSize, true); diff --git a/core/keyboard_nav/block_comment_navigation_policy.ts b/core/keyboard_nav/block_comment_navigation_policy.ts new file mode 100644 index 000000000..f2f1ab7e1 --- /dev/null +++ b/core/keyboard_nav/block_comment_navigation_policy.ts @@ -0,0 +1,76 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {TextInputBubble} from '../bubbles/textinput_bubble.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; + +/** + * Set of rules controlling keyboard navigation from an TextInputBubble. + */ +export class BlockCommentNavigationPolicy + implements INavigationPolicy +{ + /** + * Returns the first child of the given block comment. + * + * @param current The block comment to return the first child of. + * @returns The text editor of the given block comment bubble. + */ + getFirstChild(current: TextInputBubble): IFocusableNode | null { + return current.getEditor(); + } + + /** + * Returns the parent of the given block comment. + * + * @param current The block comment to return the parent of. + * @returns The parent block of the given block comment. + */ + getParent(current: TextInputBubble): IFocusableNode | null { + return current.getOwner() ?? null; + } + + /** + * Returns the next peer node of the given block comment. + * + * @param _current The block comment to find the following element of. + * @returns Null. + */ + getNextSibling(_current: TextInputBubble): IFocusableNode | null { + return null; + } + + /** + * Returns the previous peer node of the given block comment. + * + * @param _current The block comment to find the preceding element of. + * @returns Null. + */ + getPreviousSibling(_current: TextInputBubble): IFocusableNode | null { + return null; + } + + /** + * Returns whether or not the given block comment can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given block comment can be focused. + */ + isNavigable(current: TextInputBubble): boolean { + return current.canBeFocused(); + } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is an TextInputBubble. + */ + isApplicable(current: any): current is TextInputBubble { + return current instanceof TextInputBubble; + } +} diff --git a/core/keyboard_nav/comment_editor_navigation_policy.ts b/core/keyboard_nav/comment_editor_navigation_policy.ts new file mode 100644 index 000000000..456df8e97 --- /dev/null +++ b/core/keyboard_nav/comment_editor_navigation_policy.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {CommentEditor} from '../comments/comment_editor.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; + +/** + * Set of rules controlling keyboard navigation from a comment editor. + * This is a no-op placeholder (other than isNavigable/isApplicable) since + * comment editors handle their own navigation when editing ends. + */ +export class CommentEditorNavigationPolicy + implements INavigationPolicy +{ + getFirstChild(_current: CommentEditor): IFocusableNode | null { + return null; + } + + getParent(_current: CommentEditor): IFocusableNode | null { + return null; + } + + getNextSibling(_current: CommentEditor): IFocusableNode | null { + return null; + } + + getPreviousSibling(_current: CommentEditor): IFocusableNode | null { + return null; + } + + /** + * Returns whether or not the given comment editor can be navigated to. + * + * @param current The instance to check for navigability. + * @returns False. + */ + isNavigable(current: CommentEditor): boolean { + return current.canBeFocused(); + } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is a CommentEditor. + */ + isApplicable(current: any): current is CommentEditor { + return current instanceof CommentEditor; + } +} diff --git a/core/keyboard_nav/icon_navigation_policy.ts b/core/keyboard_nav/icon_navigation_policy.ts index 70631ce81..112239d06 100644 --- a/core/keyboard_nav/icon_navigation_policy.ts +++ b/core/keyboard_nav/icon_navigation_policy.ts @@ -6,6 +6,7 @@ import {BlockSvg} from '../block_svg.js'; import {getFocusManager} from '../focus_manager.js'; +import {CommentIcon} from '../icons/comment_icon.js'; import {Icon} from '../icons/icon.js'; import {MutatorIcon} from '../icons/mutator_icon.js'; import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; @@ -29,6 +30,12 @@ export class IconNavigationPolicy implements INavigationPolicy { getFocusManager().getFocusedNode() === current ) { return current.getBubble()?.getWorkspace() ?? null; + } else if ( + current instanceof CommentIcon && + current.bubbleIsVisible() && + getFocusManager().getFocusedNode() === current + ) { + return current.getBubble()?.getEditor() ?? null; } return null; diff --git a/core/navigator.ts b/core/navigator.ts index 2f095f6f9..9c7c22f59 100644 --- a/core/navigator.ts +++ b/core/navigator.ts @@ -6,8 +6,10 @@ import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {INavigationPolicy} from './interfaces/i_navigation_policy.js'; +import {BlockCommentNavigationPolicy} from './keyboard_nav/block_comment_navigation_policy.js'; import {BlockNavigationPolicy} from './keyboard_nav/block_navigation_policy.js'; import {CommentBarButtonNavigationPolicy} from './keyboard_nav/comment_bar_button_navigation_policy.js'; +import {CommentEditorNavigationPolicy} from './keyboard_nav/comment_editor_navigation_policy.js'; import {ConnectionNavigationPolicy} from './keyboard_nav/connection_navigation_policy.js'; import {FieldNavigationPolicy} from './keyboard_nav/field_navigation_policy.js'; import {IconNavigationPolicy} from './keyboard_nav/icon_navigation_policy.js'; @@ -33,6 +35,8 @@ export class Navigator { new IconNavigationPolicy(), new WorkspaceCommentNavigationPolicy(), new CommentBarButtonNavigationPolicy(), + new BlockCommentNavigationPolicy(), + new CommentEditorNavigationPolicy(), ]; /** diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index d713f11cf..4180c1099 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -22,6 +22,7 @@ import type {Block} from './block.js'; import type {BlockSvg} from './block_svg.js'; import type {BlocklyOptions} from './blockly_options.js'; import * as browserEvents from './browser_events.js'; +import {TextInputBubble} from './bubbles/textinput_bubble.js'; import {COMMENT_COLLAPSE_BAR_BUTTON_FOCUS_IDENTIFIER} from './comments/collapse_comment_bar_button.js'; import {COMMENT_EDITOR_FOCUS_IDENTIFIER} from './comments/comment_editor.js'; import {COMMENT_DELETE_BAR_BUTTON_FOCUS_IDENTIFIER} from './comments/delete_comment_bar_button.js'; @@ -2868,6 +2869,11 @@ export class WorkspaceSvg bubble.getFocusableElement().id === id ) { return bubble; + } else if ( + bubble instanceof TextInputBubble && + bubble.getEditor().getFocusableElement().id === id + ) { + return bubble.getEditor(); } } } From 2c6c2e1a9d3addd72c5c5cc697f7bd246d6cee1e Mon Sep 17 00:00:00 2001 From: Erik Pasternak Date: Fri, 11 Jul 2025 13:50:22 -0700 Subject: [PATCH 02/57] fix: Fix toolbox categories tests (Almost) This fixes the the toolbox categories tests except for dragging out the four empty statement blocks, which is fixed by https://github.com/google/blockly-samples/pull/2580 Once the latest version of samples is published the toolbox suite can be re-enabled. --- tests/browser/test/toolbox_drag_test.mjs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/browser/test/toolbox_drag_test.mjs b/tests/browser/test/toolbox_drag_test.mjs index 32c201406..d2a7ecce1 100644 --- a/tests/browser/test/toolbox_drag_test.mjs +++ b/tests/browser/test/toolbox_drag_test.mjs @@ -11,7 +11,7 @@ import * as chai from 'chai'; import {Key} from 'webdriverio'; import { - dragBlockTypeFromFlyout, + getBlockTypeFromCategory, getCategory, PAUSE_TIME, screenDirection, @@ -148,7 +148,12 @@ async function openCategories(browser, categoryList, directionMultiplier) { continue; } const blockType = await getNthBlockType(browser, categoryName, i); - dragBlockTypeFromFlyout(browser, categoryName, blockType, 50, 20); + const blockElem = await getBlockTypeFromCategory( + browser, + categoryName, + blockType, + ); + await blockElem.dragAndDrop({x: 50 * directionMultiplier, y: 20}); await browser.pause(PAUSE_TIME); // Should be one top level block on the workspace. const topBlockCount = await browser.execute(() => { From 802d3f887dd9e7559f9116b4ba6e42dd805dd168 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 08:35:50 -0700 Subject: [PATCH 03/57] chore(deps): bump google-closure-compiler (#9233) Bumps [google-closure-compiler](https://github.com/google/closure-compiler-npm) from 20250625.0.0 to 20250709.0.0. - [Release notes](https://github.com/google/closure-compiler-npm/releases) - [Commits](https://github.com/google/closure-compiler-npm/compare/v20250625.0.0...v20250709.0.0) --- updated-dependencies: - dependency-name: google-closure-compiler dependency-version: 20250709.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 56 ++++++++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 21178897d..6a60c9a36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "eslint-plugin-prettier": "^5.2.1", "glob": "^11.0.1", "globals": "^16.0.0", - "google-closure-compiler": "^20250625.0.0", + "google-closure-compiler": "^20250709.0.0", "gulp": "^5.0.0", "gulp-concat": "^2.6.1", "gulp-gzip": "^1.4.2", @@ -4949,13 +4949,14 @@ } }, "node_modules/google-closure-compiler": { - "version": "20250625.0.0", - "resolved": "https://registry.npmjs.org/google-closure-compiler/-/google-closure-compiler-20250625.0.0.tgz", - "integrity": "sha512-FQ6yKCRYwo4493Rq6lZrxpmWuJGZuuSruCdtArptkoThadzw4TM0YvQJvwRYnQDUpjj6/x7G14l2n/+8G39AIA==", + "version": "20250709.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler/-/google-closure-compiler-20250709.0.0.tgz", + "integrity": "sha512-FUdjG7vri7Pi/iswJj1bFcE3cYOcGLnez2nKaEK8qSailRFQlnp8j9vuT60EOU8FLzckEPI0Sf882Q7vJPilFg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "chalk": "5.x", - "google-closure-compiler-java": "^20250625.0.0", + "google-closure-compiler-java": "^20250709.0.0", "minimist": "1.x", "vinyl": "3.x", "vinyl-sourcemaps-apply": "^0.2.0" @@ -4967,67 +4968,72 @@ "node": ">=18" }, "optionalDependencies": { - "google-closure-compiler-linux": "^20250625.0.0", - "google-closure-compiler-linux-arm64": "^20250625.0.0", - "google-closure-compiler-macos": "^20250625.0.0", - "google-closure-compiler-windows": "^20250625.0.0" + "google-closure-compiler-linux": "^20250709.0.0", + "google-closure-compiler-linux-arm64": "^20250709.0.0", + "google-closure-compiler-macos": "^20250709.0.0", + "google-closure-compiler-windows": "^20250709.0.0" } }, "node_modules/google-closure-compiler-java": { - "version": "20250625.0.0", - "resolved": "https://registry.npmjs.org/google-closure-compiler-java/-/google-closure-compiler-java-20250625.0.0.tgz", - "integrity": "sha512-T916Kvb7JYaIiH9spiJXVKeualLV7PO/KXOJzMhLrW4M6etfvr3s2cTqlhUk+BrxvgxqWBWFbMDRUZbVGPnBaw==", - "dev": true + "version": "20250709.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-java/-/google-closure-compiler-java-20250709.0.0.tgz", + "integrity": "sha512-gyriPJ8nYxYVa5wqeMJZsOdFoDDcHSmGHG9VNYjQrcdIOWyxW9Ggcb2gtrI/MEa54CLoRbzUJ12ELO1mzePMlQ==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/google-closure-compiler-linux": { - "version": "20250625.0.0", - "resolved": "https://registry.npmjs.org/google-closure-compiler-linux/-/google-closure-compiler-linux-20250625.0.0.tgz", - "integrity": "sha512-2cOYLfG7RF49FnGG+yBGlEndE0es8D7+YIGgF8KnGIkxrfiZhOTyQftFx4z48TZ1Be/1JtM2eNXbD2fuR9nJdA==", + "version": "20250709.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-linux/-/google-closure-compiler-linux-20250709.0.0.tgz", + "integrity": "sha512-kpl9W+696vnGzpa/ewfwpsRR3t42g3CDQ5hFjQAitxtZpnejU7ik94+O8D+56049zS2O85LdWRDCbckvzEXw+w==", "cpu": [ "x32", "x64" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "linux" ] }, "node_modules/google-closure-compiler-linux-arm64": { - "version": "20250625.0.0", - "resolved": "https://registry.npmjs.org/google-closure-compiler-linux-arm64/-/google-closure-compiler-linux-arm64-20250625.0.0.tgz", - "integrity": "sha512-2vKY8UpL03CFe+k1qFma/HnUZnTM3V3K5ukxmk/Xwt3D7CTwn/039zA3AjxsGW5vLp4guVyLtqbS711KeGpLNA==", + "version": "20250709.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-linux-arm64/-/google-closure-compiler-linux-arm64-20250709.0.0.tgz", + "integrity": "sha512-3mLAD9JpAM0StUb2VTOw4L/rIxksTO7lOfuI0+OyexQfLIRLM8M9jeUgrOAPbmgDsyYZ8Q3pHX2qcnURexZsrw==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "linux" ] }, "node_modules/google-closure-compiler-macos": { - "version": "20250625.0.0", - "resolved": "https://registry.npmjs.org/google-closure-compiler-macos/-/google-closure-compiler-macos-20250625.0.0.tgz", - "integrity": "sha512-/S3d5/oKKw2pEu42Bn+fnoKR0cAjlhOQP1IM0D1aDqNS+jMUXo4bV7RSVB+NSVL65XxIVQOqbnkD5Cfoe8lbrw==", + "version": "20250709.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-macos/-/google-closure-compiler-macos-20250709.0.0.tgz", + "integrity": "sha512-2/MXSVgM+HmnzwbyWdfY2ZVjKgK8LFtCKhsQQhsSV/f2jnrHcuG9+RkzLrzQsO1zPpHaLcXAkizf4AUpCfuzBA==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "darwin" ] }, "node_modules/google-closure-compiler-windows": { - "version": "20250625.0.0", - "resolved": "https://registry.npmjs.org/google-closure-compiler-windows/-/google-closure-compiler-windows-20250625.0.0.tgz", - "integrity": "sha512-YBNRFTSuWXDJad1pJ1SPjPFpgImrQr7XeW1D9YrPCv1T5cfM8vy01jFkZIDuUha38kHsPvk7kG3rkYYrJpD8+Q==", + "version": "20250709.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-windows/-/google-closure-compiler-windows-20250709.0.0.tgz", + "integrity": "sha512-ZnmgRzx0qIVQu0zw7ZTJQz3tMFVhwzeODZfXRnYDLeNkJA7IBaWsNHTALA7pUcgPM+YDDr4ihQOexMc0u4s7LQ==", "cpu": [ "x32", "x64" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "win32" diff --git a/package.json b/package.json index d464ae5f1..4df4f9586 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "eslint-plugin-prettier": "^5.2.1", "glob": "^11.0.1", "globals": "^16.0.0", - "google-closure-compiler": "^20250625.0.0", + "google-closure-compiler": "^20250709.0.0", "gulp": "^5.0.0", "gulp-concat": "^2.6.1", "gulp-gzip": "^1.4.2", From 52634e4dec56de31c790d3e718cebffef147f27e Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 14 Jul 2025 08:55:13 -0700 Subject: [PATCH 04/57] fix: Focus the first element in flyouts. (#9228) * fix: Focus the first element in flyouts. * refactor: Adjust retrieval of flyout elements. --- core/workspace_svg.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 4180c1099..b666dc97a 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -2727,6 +2727,19 @@ export class WorkspaceSvg previousNode: IFocusableNode | null, ): IFocusableNode | null { if (!previousNode) { + const flyout = this.targetWorkspace?.getFlyout(); + if (this.isFlyout && flyout) { + // Return the first focusable item of the flyout. + return ( + flyout + .getContents() + .find((flyoutItem) => { + const element = flyoutItem.getElement(); + return isFocusableNode(element) && element.canBeFocused(); + }) + ?.getElement() ?? null + ); + } return this.getTopBlocks(true)[0] ?? null; } else return null; } From 9f66f0c59608d82547e06d5f6ec1b5228116b111 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 14 Jul 2025 09:53:17 -0700 Subject: [PATCH 05/57] fix: Fire a `VarTypeChange` event when changing a variable's type. (#9236) --- core/variable_map.ts | 10 ++++++++++ tests/mocha/variable_map_test.js | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/core/variable_map.ts b/core/variable_map.ts index 403893332..3dd4bf547 100644 --- a/core/variable_map.ts +++ b/core/variable_map.ts @@ -109,6 +109,9 @@ export class VariableMap variable: IVariableModel, newType: string, ): IVariableModel { + const oldType = variable.getType(); + if (oldType === newType) return variable; + this.variableMap.get(variable.getType())?.delete(variable.getId()); variable.setType(newType); const newTypeVariables = @@ -118,6 +121,13 @@ export class VariableMap if (!this.variableMap.has(newType)) { this.variableMap.set(newType, newTypeVariables); } + eventUtils.fire( + new (eventUtils.get(EventType.VAR_TYPE_CHANGE))( + variable, + oldType, + newType, + ), + ); return variable; } diff --git a/tests/mocha/variable_map_test.js b/tests/mocha/variable_map_test.js index c02887cea..2d6cee0b9 100644 --- a/tests/mocha/variable_map_test.js +++ b/tests/mocha/variable_map_test.js @@ -505,5 +505,26 @@ suite('Variable Map', function () { }); }); }); + + suite('variable type change events', function () { + test('are fired when a variable has its type changed', function () { + const variable = this.variableMap.createVariable( + 'name1', + 'type1', + 'id1', + ); + this.variableMap.changeVariableType(variable, 'type2'); + assertEventFired( + this.eventSpy, + Blockly.Events.VarTypeChange, + { + oldType: 'type1', + newType: 'type2', + varId: 'id1', + }, + this.workspace.id, + ); + }); + }); }); }); From 55bd1aff795e59ef56748735b3f45cb882be2433 Mon Sep 17 00:00:00 2001 From: Erik Pasternak Date: Mon, 14 Jul 2025 10:09:51 -0700 Subject: [PATCH 06/57] Enable categories test --- tests/browser/test/toolbox_drag_test.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/browser/test/toolbox_drag_test.mjs b/tests/browser/test/toolbox_drag_test.mjs index d2a7ecce1..66b74d9e2 100644 --- a/tests/browser/test/toolbox_drag_test.mjs +++ b/tests/browser/test/toolbox_drag_test.mjs @@ -179,9 +179,9 @@ async function openCategories(browser, categoryList, directionMultiplier) { chai.assert.equal(failureCount, 0); } -// TODO (#9217) These take too long to run and are very flakey. Need to find a -// better way to test whatever this is trying to test. -suite.skip('Open toolbox categories', function () { +// TODO (#9217) These take too long to run and are very flakey. Need to pull +// these out into their own test runner. +suite('Open toolbox categories', function () { this.timeout(0); test('opening every toolbox category in the category toolbox in LTR', async function () { From ec01df4adaa407301b4eac5d4cd9c0526c677a5c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 18:11:03 +0100 Subject: [PATCH 07/57] chore(deps): bump @microsoft/api-documenter from 7.26.26 to 7.26.29 (#9234) Bumps [@microsoft/api-documenter](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-documenter) from 7.26.26 to 7.26.29. - [Changelog](https://github.com/microsoft/rushstack/blob/main/apps/api-documenter/CHANGELOG.md) - [Commits](https://github.com/microsoft/rushstack/commits/@microsoft/api-documenter_v7.26.29/apps/api-documenter) --- updated-dependencies: - dependency-name: "@microsoft/api-documenter" dependency-version: 7.26.29 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6a60c9a36..1bdb22c75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -912,9 +912,9 @@ } }, "node_modules/@microsoft/api-documenter": { - "version": "7.26.26", - "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.26.26.tgz", - "integrity": "sha512-085FwdwQcXGvwtMJFajwhu5eZOQ3PXsyLIoq3WXAQr/7M6Vn59GMGjuB/+lIXqmWKkxzeFAX5f9sKqr9X7zI3g==", + "version": "7.26.29", + "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.26.29.tgz", + "integrity": "sha512-5gqnUCut1BeNmOZIE8hUJbzq3DxFcAyXL12oF6aFVtTDF8WiVs/J1HtlLYbxeIff6qbI1LfLnr16t+WOm9UVJw==", "dev": true, "license": "MIT", "dependencies": { From 7479c2b5c74b984dbf3d8ed638a7d4d03560d1c2 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 14 Jul 2025 10:22:09 -0700 Subject: [PATCH 08/57] fix: Fix order of arguments in `IVariableMap.createVariable()`. (#9231) --- core/interfaces/i_variable_map.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/interfaces/i_variable_map.ts b/core/interfaces/i_variable_map.ts index 6c21aa8e0..22b4eda90 100644 --- a/core/interfaces/i_variable_map.ts +++ b/core/interfaces/i_variable_map.ts @@ -43,7 +43,7 @@ export interface IVariableMap> { * Creates a new variable with the given name. If ID is not specified, the * variable map should create one. Returns the new variable. */ - createVariable(name: string, id?: string, type?: string | null): T; + createVariable(name: string, type?: string, id?: string | null): T; /* Adds a variable to this variable map. */ addVariable(variable: T): void; From c1c7ddb2938183b9b0a62fe4f618b3976c9ff414 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 11:17:27 -0700 Subject: [PATCH 09/57] chore(deps): bump chai from 5.2.0 to 5.2.1 (#9235) Bumps [chai](https://github.com/chaijs/chai) from 5.2.0 to 5.2.1. - [Release notes](https://github.com/chaijs/chai/releases) - [Changelog](https://github.com/chaijs/chai/blob/main/History.md) - [Commits](https://github.com/chaijs/chai/compare/v5.2.0...v5.2.1) --- updated-dependencies: - dependency-name: chai dependency-version: 5.2.1 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1bdb22c75..0db79293f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2766,9 +2766,9 @@ } }, "node_modules/chai": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", "dev": true, "license": "MIT", "dependencies": { @@ -2779,7 +2779,7 @@ "pathval": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/chalk": { From 908712e19d2d028df9352f55be9046a47eeae303 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 15 Jul 2025 11:06:27 -0700 Subject: [PATCH 10/57] fix: Refer to correct replacement method in deprecation warning. (#9237) --- core/workspace.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/workspace.ts b/core/workspace.ts index 5f2051939..88745f420 100644 --- a/core/workspace.ts +++ b/core/workspace.ts @@ -469,7 +469,7 @@ export class Workspace { 'Blockly.Workspace.getVariableUsesById', 'v12', 'v13', - 'Blockly.Workspace.getVariableMap().getVariableUsesById', + 'Blockly.Variables.getVariableUsesById', ); return getVariableUsesById(this, id); } From 3c7add57eed7e2564a69f650d8381db3d1f2eadd Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 18 Jul 2025 14:27:49 -0700 Subject: [PATCH 11/57] fix: Make non-autoclosing flyouts stay open. (#9245) * chore: Add tests for toolbox/flyout/focus autoclose behavior. * fix: Don't force-close non-autoclosing flyouts. --- core/toolbox/toolbox.ts | 10 ++------ core/workspace_svg.ts | 4 +-- tests/mocha/toolbox_test.js | 51 +++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index 4979fdfa4..f34034d33 100644 --- a/core/toolbox/toolbox.ts +++ b/core/toolbox/toolbox.ts @@ -22,10 +22,7 @@ import '../events/events_toolbox_item_select.js'; import {EventType} from '../events/type.js'; import * as eventUtils from '../events/utils.js'; import {getFocusManager} from '../focus_manager.js'; -import { - isAutoHideable, - type IAutoHideable, -} from '../interfaces/i_autohideable.js'; +import {type IAutoHideable} from '../interfaces/i_autohideable.js'; import type {ICollapsibleToolboxItem} from '../interfaces/i_collapsible_toolbox_item.js'; import {isDeletable} from '../interfaces/i_deletable.js'; import type {IDraggable} from '../interfaces/i_draggable.js'; @@ -1150,10 +1147,7 @@ export class Toolbox // If navigating to anything other than the toolbox's flyout then clear the // selection so that the toolbox's flyout can automatically close. if (!nextTree || nextTree !== this.flyout?.getWorkspace()) { - this.clearSelection(); - if (this.flyout && isAutoHideable(this.flyout)) { - this.flyout.autoHide(false); - } + this.autoHide(false); } } } diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index b666dc97a..6c6b59301 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -2908,11 +2908,9 @@ export class WorkspaceSvg // Only hide the flyout if the flyout's workspace is losing focus and that // focus isn't returning to the flyout itself, the toolbox, or ephemeral. if (getFocusManager().ephemeralFocusTaken()) return; - const flyout = this.targetWorkspace.getFlyout(); const toolbox = this.targetWorkspace.getToolbox(); if (toolbox && nextTree === toolbox) return; - if (toolbox) toolbox.clearSelection(); - if (flyout && isAutoHideable(flyout)) flyout.autoHide(false); + if (isAutoHideable(toolbox)) toolbox.autoHide(false); } } diff --git a/tests/mocha/toolbox_test.js b/tests/mocha/toolbox_test.js index f32319c67..4e92cd28f 100644 --- a/tests/mocha/toolbox_test.js +++ b/tests/mocha/toolbox_test.js @@ -183,6 +183,57 @@ suite('Toolbox', function () { }); }); + suite('focus management', function () { + setup(function () { + this.toolbox = getInjectedToolbox(); + }); + teardown(function () { + this.toolbox.dispose(); + }); + + test('Losing focus hides autoclosing flyout', function () { + // Focus the toolbox and select a category to open the flyout. + const target = this.toolbox.HtmlDiv.querySelector( + '.blocklyToolboxCategory', + ); + Blockly.getFocusManager().focusNode(this.toolbox); + target.dispatchEvent( + new PointerEvent('pointerdown', { + target, + bubbles: true, + }), + ); + assert.isTrue(this.toolbox.getFlyout().isVisible()); + + // Focus the workspace to trigger the toolbox to close the flyout. + Blockly.getFocusManager().focusNode(this.toolbox.getWorkspace()); + assert.isFalse(this.toolbox.getFlyout().isVisible()); + }); + + test('Losing focus does not hide non-autoclosing flyout', function () { + // Make the toolbox's flyout non-autoclosing. + this.toolbox.getFlyout().setAutoClose(false); + + // Focus the toolbox and select a category to open the flyout. + const target = this.toolbox.HtmlDiv.querySelector( + '.blocklyToolboxCategory', + ); + Blockly.getFocusManager().focusNode(this.toolbox); + target.dispatchEvent( + new PointerEvent('pointerdown', { + target, + bubbles: true, + }), + ); + assert.isTrue(this.toolbox.getFlyout().isVisible()); + + // Focus the workspace; this should *not* trigger the toolbox to close the + // flyout, which should remain visible. + Blockly.getFocusManager().focusNode(this.toolbox.getWorkspace()); + assert.isTrue(this.toolbox.getFlyout().isVisible()); + }); + }); + suite('onClick_', function () { setup(function () { this.toolbox = getInjectedToolbox(); From f37e7fede20ed81237f65626cdb1dd20c8f6d5e3 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Thu, 24 Jul 2025 12:28:18 -0400 Subject: [PATCH 12/57] chore: fix docs generation script (#9251) * chore: pin api-documenter to a version that uses markdown tables * chore: fix docs generation errors * chore: update patch for api-documenter --- package-lock.json | 346 +++++++++++++++--- package.json | 3 +- ...=> @microsoft+api-documenter+7.22.4.patch} | 25 +- scripts/gulpfiles/docs_tasks.mjs | 4 +- 4 files changed, 306 insertions(+), 72 deletions(-) rename patches/{@microsoft+api-documenter+7.26.26.patch => @microsoft+api-documenter+7.22.4.patch} (85%) diff --git a/package-lock.json b/package-lock.json index 0db79293f..28f32b019 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,8 +18,9 @@ "@blockly/theme-modern": "^6.0.3", "@hyperjump/browser": "^1.1.4", "@hyperjump/json-schema": "^1.5.0", - "@microsoft/api-documenter": "^7.22.4", + "@microsoft/api-documenter": "7.22.4", "@microsoft/api-extractor": "^7.29.5", + "ajv": "^8.17.1", "async-done": "^2.0.0", "chai": "^5.1.1", "concurrently": "^9.0.1", @@ -485,6 +486,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -498,6 +516,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/js": { "version": "9.30.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.0.tgz", @@ -912,17 +937,17 @@ } }, "node_modules/@microsoft/api-documenter": { - "version": "7.26.29", - "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.26.29.tgz", - "integrity": "sha512-5gqnUCut1BeNmOZIE8hUJbzq3DxFcAyXL12oF6aFVtTDF8WiVs/J1HtlLYbxeIff6qbI1LfLnr16t+WOm9UVJw==", + "version": "7.22.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.22.4.tgz", + "integrity": "sha512-d4htEhBd8UkFKff/+/nAi/z7rrspm1DanFmsRHLUp4gKMo/8hYDH/IQBWB4r9X/8X72jCv3I++VVWAfichL1rw==", "dev": true, "license": "MIT", "dependencies": { - "@microsoft/api-extractor-model": "7.30.6", - "@microsoft/tsdoc": "~0.15.1", - "@rushstack/node-core-library": "5.13.1", - "@rushstack/terminal": "0.15.3", - "@rushstack/ts-command-line": "5.0.1", + "@microsoft/api-extractor-model": "7.26.8", + "@microsoft/tsdoc": "0.14.2", + "@rushstack/node-core-library": "3.58.0", + "@rushstack/ts-command-line": "4.13.2", + "colors": "~1.2.1", "js-yaml": "~3.13.1", "resolve": "~1.22.1" }, @@ -930,6 +955,106 @@ "api-documenter": "bin/api-documenter" } }, + "node_modules/@microsoft/api-documenter/node_modules/@microsoft/api-extractor-model": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.26.8.tgz", + "integrity": "sha512-ESj3bBJkiMg/8tS0PW4+2rUgTVwOEfy41idTnFgdbVX+O50bN6S99MV6FIPlCZWCnRDcBfwxRXLdAkOQQ0JqGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.58.0" + } + }, + "node_modules/@microsoft/api-documenter/node_modules/@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/api-documenter/node_modules/@microsoft/tsdoc-config": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz", + "integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + } + }, + "node_modules/@microsoft/api-documenter/node_modules/@microsoft/tsdoc-config/node_modules/resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@microsoft/api-documenter/node_modules/@rushstack/node-core-library": { + "version": "3.58.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.58.0.tgz", + "integrity": "sha512-DHAZ3LTOEq2/EGURznpTJDnB3SNE2CKMDXuviQ6afhru6RykE3QoqXkeyjbpLb5ib5cpIRCPE/wykNe0xmQj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "colors": "~1.2.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.3.0", + "z-schema": "~5.0.2" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@microsoft/api-documenter/node_modules/@rushstack/ts-command-line": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.13.2.tgz", + "integrity": "sha512-bCU8qoL9HyWiciltfzg7GqdfODUeda/JpI0602kbN5YH22rzTxyqYvv7aRLENCM7XCQ1VRs7nMkEqgJUOU8Sag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "colors": "~1.2.1", + "string-argv": "~0.3.1" + } + }, + "node_modules/@microsoft/api-documenter/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@microsoft/api-documenter/node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -952,6 +1077,29 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@microsoft/api-documenter/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/api-documenter/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@microsoft/api-extractor": { "version": "7.52.8", "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.52.8.tgz", @@ -1056,12 +1204,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@microsoft/tsdoc-config/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1236,13 +1378,6 @@ "node": ">=14.14" } }, - "node_modules/@rushstack/node-core-library/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/@rushstack/rig-package": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.3.tgz", @@ -1955,16 +2090,16 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -2002,28 +2137,6 @@ } } }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.14.0.tgz", - "integrity": "sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.4.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/ansi-gray": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", @@ -2979,6 +3092,16 @@ "color-support": "bin.js" } }, + "node_modules/colors": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", + "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/commander": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", @@ -4068,6 +4191,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", @@ -4092,6 +4232,13 @@ "node": ">=10.13.0" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -4340,6 +4487,23 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fast-xml-parser": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", @@ -4598,6 +4762,41 @@ "node": ">=12.20.0" } }, + "node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-extra/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/fs-mkdirp-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", @@ -6198,9 +6397,9 @@ "dev": true }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, @@ -6486,6 +6685,14 @@ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -9207,6 +9414,16 @@ "node": ">= 10.13.0" } }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/value-or-function": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", @@ -9758,6 +9975,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, "node_modules/zip-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", diff --git a/package.json b/package.json index 4df4f9586..dfff0da7f 100644 --- a/package.json +++ b/package.json @@ -105,8 +105,9 @@ "@blockly/theme-modern": "^6.0.3", "@hyperjump/browser": "^1.1.4", "@hyperjump/json-schema": "^1.5.0", - "@microsoft/api-documenter": "^7.22.4", + "@microsoft/api-documenter": "7.22.4", "@microsoft/api-extractor": "^7.29.5", + "ajv": "^8.17.1", "async-done": "^2.0.0", "chai": "^5.1.1", "concurrently": "^9.0.1", diff --git a/patches/@microsoft+api-documenter+7.26.26.patch b/patches/@microsoft+api-documenter+7.22.4.patch similarity index 85% rename from patches/@microsoft+api-documenter+7.26.26.patch rename to patches/@microsoft+api-documenter+7.22.4.patch index de8e47c04..3cc97035d 100644 --- a/patches/@microsoft+api-documenter+7.26.26.patch +++ b/patches/@microsoft+api-documenter+7.22.4.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js b/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js -index 0f4e2ba..3af2014 100644 +index 5284d10..4f8b439 100644 --- a/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js +++ b/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js -@@ -893,12 +893,15 @@ class MarkdownDocumenter { +@@ -877,12 +877,14 @@ class MarkdownDocumenter { } _writeBreadcrumb(output, apiItem) { const configuration = this._tsdocConfiguration; @@ -19,28 +19,23 @@ index 0f4e2ba..3af2014 100644 + // linkText: 'Home', + // urlDestination: this._getLinkFilenameForApiItem(this._apiModel) + // })); -+ + let first = true; for (const hierarchyItem of apiItem.getHierarchy()) { switch (hierarchyItem.kind) { case api_extractor_model_1.ApiItemKind.Model: -@@ -908,18 +911,24 @@ class MarkdownDocumenter { +@@ -892,18 +894,23 @@ class MarkdownDocumenter { // this may change in the future. break; default: - output.appendNodesInParagraph([ -- new tsdoc_1.DocPlainText({ -- configuration, -- text: ' > ' -- }), + if (!first) { + // Only print the breadcrumb separator if it's not the first item we're printing. + output.appendNodeInParagraph( -+ new tsdoc_1.DocPlainText({ -+ configuration, -+ text: ' > ' -+ }) -+ ); + new tsdoc_1.DocPlainText({ + configuration, + text: ' > ' +- }), ++ })); + } + first = false; + output.appendNodeInParagraph( @@ -55,7 +50,7 @@ index 0f4e2ba..3af2014 100644 } } } -@@ -992,11 +1001,8 @@ class MarkdownDocumenter { +@@ -968,11 +975,8 @@ class MarkdownDocumenter { // For overloaded methods, add a suffix such as "MyClass.myMethod_2". let qualifiedName = Utilities_1.Utilities.getSafeFilenameForName(hierarchyItem.displayName); if (api_extractor_model_1.ApiParameterListMixin.isBaseClassOf(hierarchyItem)) { @@ -69,7 +64,7 @@ index 0f4e2ba..3af2014 100644 } switch (hierarchyItem.kind) { case api_extractor_model_1.ApiItemKind.Model: -@@ -1007,7 +1013,8 @@ class MarkdownDocumenter { +@@ -983,7 +987,8 @@ class MarkdownDocumenter { baseName = Utilities_1.Utilities.getSafeFilenameForName(node_core_library_1.PackageName.getUnscopedName(hierarchyItem.displayName)); break; default: diff --git a/scripts/gulpfiles/docs_tasks.mjs b/scripts/gulpfiles/docs_tasks.mjs index 63fdbe665..51abd480f 100644 --- a/scripts/gulpfiles/docs_tasks.mjs +++ b/scripts/gulpfiles/docs_tasks.mjs @@ -2,8 +2,8 @@ import {execSync} from 'child_process'; import {Extractor} from 'markdown-tables-to-json'; import * as fs from 'fs'; import * as gulp from 'gulp'; -import * as header from 'gulp-header'; -import * as replace from 'gulp-replace'; +import header from 'gulp-header'; +import replace from 'gulp-replace'; const DOCS_DIR = 'docs'; From 0de5b17c8a531f8582b83c436af186c8490c9f00 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:29:34 +0000 Subject: [PATCH 13/57] chore(deps): bump @blockly/theme-modern from 6.0.10 to 7.0.1 Bumps [@blockly/theme-modern](https://github.com/google/blockly-samples/tree/HEAD/plugins/theme-modern) from 6.0.10 to 7.0.1. - [Release notes](https://github.com/google/blockly-samples/releases) - [Changelog](https://github.com/google/blockly-samples/blob/master/plugins/theme-modern/CHANGELOG.md) - [Commits](https://github.com/google/blockly-samples/commits/@blockly/theme-modern@7.0.1/plugins/theme-modern) --- updated-dependencies: - dependency-name: "@blockly/theme-modern" dependency-version: 7.0.1 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package-lock.json | 11 ++++++----- package.json | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 28f32b019..95e9a39b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "devDependencies": { "@blockly/block-test": "^7.0.1", "@blockly/dev-tools": "^9.0.0", - "@blockly/theme-modern": "^6.0.3", + "@blockly/theme-modern": "^7.0.1", "@hyperjump/browser": "^1.1.4", "@hyperjump/json-schema": "^1.5.0", "@microsoft/api-documenter": "7.22.4", @@ -236,15 +236,16 @@ } }, "node_modules/@blockly/theme-modern": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@blockly/theme-modern/-/theme-modern-6.0.10.tgz", - "integrity": "sha512-xOVf5Vq5ACgbVsaNAKWb5cE0msUfBxj1G1asp0aBmWo1QCr3Yze4rUtFDaNIoeCd8EsRpuWZgBYg74zPL9eAow==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@blockly/theme-modern/-/theme-modern-7.0.1.tgz", + "integrity": "sha512-aMI3OBp8KCbLU1O14FLUlocK7IeMOyiSenlTJ4lwGcBmZntM2OIcx6o89oAIeq6HkmaH7vMlK+/AgqdB3k0y3A==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=8.17.0" }, "peerDependencies": { - "blockly": "^11.0.0" + "blockly": "^12.0.0" } }, "node_modules/@blockly/theme-tritanopia": { diff --git a/package.json b/package.json index dfff0da7f..6ae18bd4a 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "devDependencies": { "@blockly/block-test": "^7.0.1", "@blockly/dev-tools": "^9.0.0", - "@blockly/theme-modern": "^6.0.3", + "@blockly/theme-modern": "^7.0.1", "@hyperjump/browser": "^1.1.4", "@hyperjump/json-schema": "^1.5.0", "@microsoft/api-documenter": "7.22.4", From c9a8221a2d2cd01cb42aa162b7ce2b8418ec1a9c Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 28 Jul 2025 13:33:52 -0700 Subject: [PATCH 14/57] fix: Fix displaying HTML elements in `FieldDropdown`. (#9258) --- core/field_dropdown.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index 9c0d7f292..8b01ccdda 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -699,25 +699,30 @@ export class FieldDropdown extends Field { prefix?: string; suffix?: string; } { - let hasImages = false; + let hasNonTextContent = false; const trimmedOptions = options.map((option): MenuOption => { - if (option === FieldDropdown.SEPARATOR) return option; + if (option === FieldDropdown.SEPARATOR) { + hasNonTextContent = true; + return option; + } const [label, value] = option; if (typeof label === 'string') { return [parsing.replaceMessageReferences(label), value]; } - hasImages = true; + hasNonTextContent = true; // Copy the image properties so they're not influenced by the original. // NOTE: No need to deep copy since image properties are only 1 level deep. const imageLabel = isImageProperties(label) ? {...label, alt: parsing.replaceMessageReferences(label.alt)} - : {...label}; + : label; return [imageLabel, value]; }); - if (hasImages || options.length < 2) return {options: trimmedOptions}; + if (hasNonTextContent || options.length < 2) { + return {options: trimmedOptions}; + } const stringOptions = trimmedOptions as [string, string][]; const stringLabels = stringOptions.map(([label]) => label); @@ -793,7 +798,7 @@ export class FieldDropdown extends Field { } else if (typeof option[1] !== 'string') { foundError = true; console.error( - `Invalid option[${i}]: Each FieldDropdown option id must be a string. + `Invalid option[${i}]: Each FieldDropdown option id must be a string. Found ${option[1]} in: ${option}`, ); } else if ( @@ -806,7 +811,7 @@ export class FieldDropdown extends Field { ) { foundError = true; console.error( - `Invalid option[${i}]: Each FieldDropdown option must have a string + `Invalid option[${i}]: Each FieldDropdown option must have a string label, image description, or HTML element. Found ${option[0]} in: ${option}`, ); } From c661dd1c946d12a41b2fc14b0d1deb5e3e3115f3 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Mon, 28 Jul 2025 17:35:55 -0400 Subject: [PATCH 15/57] fix: dont save ids when copying blocks and comments (#9255) --- core/block_svg.ts | 1 + core/comments/rendered_workspace_comment.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/core/block_svg.ts b/core/block_svg.ts index 49b4a1ee6..1b85d38ce 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -962,6 +962,7 @@ export class BlockSvg blockState: blocks.save(this, { addCoordinates: true, addNextBlocks: false, + saveIds: false, }) as blocks.State, typeCounts: common.getBlockTypeCounts(this, true), }; diff --git a/core/comments/rendered_workspace_comment.ts b/core/comments/rendered_workspace_comment.ts index c4c1f3d4e..49c75e608 100644 --- a/core/comments/rendered_workspace_comment.ts +++ b/core/comments/rendered_workspace_comment.ts @@ -280,6 +280,7 @@ export class RenderedWorkspaceComment paster: WorkspaceCommentPaster.TYPE, commentState: commentSerialization.save(this, { addCoordinates: true, + saveIds: false, }), }; } From 8a578f5ce3b8e7e8f650369f1e9e19bcbb66e1ae Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 30 Jul 2025 08:58:30 -0700 Subject: [PATCH 16/57] fix: Allow overriding `VariableModel` via `options.plugins`. (#9257) --- core/registry.ts | 6 +++--- core/variable_map.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/registry.ts b/core/registry.ts index 2b00b775d..4980a5594 100644 --- a/core/registry.ts +++ b/core/registry.ts @@ -119,9 +119,9 @@ export class Type<_T> { /** @internal */ static PASTER = new Type>>('paster'); - static VARIABLE_MODEL = new Type>( - 'variableModel', - ); + static VARIABLE_MODEL = new Type< + IVariableModelStatic & IVariableModel + >('variableModel'); static VARIABLE_MAP = new Type>>( 'variableMap', diff --git a/core/variable_map.ts b/core/variable_map.ts index 3dd4bf547..ba36dcea6 100644 --- a/core/variable_map.ts +++ b/core/variable_map.ts @@ -255,9 +255,9 @@ export class VariableMap } const id = opt_id || idGenerator.genUid(); const type = opt_type || ''; - const VariableModel = registry.getObject( + const VariableModel = registry.getClassFromOptions( registry.Type.VARIABLE_MODEL, - registry.DEFAULT, + this.workspace.options, true, ); if (!VariableModel) { From c037e7d47ddd2c2a8ac51e6e648372adc0e14722 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 30 Jul 2025 09:48:00 -0700 Subject: [PATCH 17/57] fix: Scroll `CommentBarButton`s into view on selection. (#9259) --- core/keyboard_nav/line_cursor.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index a301c3b37..c621e3a89 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -14,6 +14,7 @@ */ import {BlockSvg} from '../block_svg.js'; +import {CommentBarButton} from '../comments/comment_bar_button.js'; import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js'; import {Field} from '../field.js'; import {getFocusManager} from '../focus_manager.js'; @@ -403,6 +404,9 @@ export class LineCursor extends Marker { ); } else if (newNode instanceof RenderedWorkspaceComment) { newNode.workspace.scrollBoundsIntoView(newNode.getBoundingRectangle()); + } else if (newNode instanceof CommentBarButton) { + const comment = newNode.getParentComment(); + comment.workspace.scrollBoundsIntoView(comment.getBoundingRectangle()); } } From d9421892fb1b8815fc3092ea3c18e3444c3973df Mon Sep 17 00:00:00 2001 From: michaela-mm <63740955+michaela-mm@users.noreply.github.com> Date: Fri, 1 Aug 2025 20:30:23 +0200 Subject: [PATCH 18/57] fix: Check for existing event group in cleanUp() (#9265) --- core/workspace_svg.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 6c6b59301..af395b077 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -1674,7 +1674,10 @@ export class WorkspaceSvg /** Clean up the workspace by ordering all the blocks in a column such that none overlap. */ cleanUp() { this.setResizesEnabled(false); - eventUtils.setGroup(true); + const existingGroup = eventUtils.getGroup(); + if (!existingGroup) { + eventUtils.setGroup(true); + } const topBlocks = this.getTopBlocks(true); const movableBlocks = topBlocks.filter((block) => block.isMovable()); @@ -1722,7 +1725,7 @@ export class WorkspaceSvg block.getHeightWidth().height + minBlockHeight; } - eventUtils.setGroup(false); + eventUtils.setGroup(existingGroup); this.setResizesEnabled(true); } From 71256d69a0794e5a21bc8f1cf8c691fc3a898b9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:22:09 +0000 Subject: [PATCH 19/57] chore(deps): bump eslint-plugin-jsdoc from 51.3.1 to 52.0.2 Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 51.3.1 to 52.0.2. - [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases) - [Changelog](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.releaserc) - [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v51.3.1...v52.0.2) --- updated-dependencies: - dependency-name: eslint-plugin-jsdoc dependency-version: 52.0.2 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package-lock.json | 9 +++++---- package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 95e9a39b2..8d4d84a1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "eslint": "^9.15.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^10.1.1", - "eslint-plugin-jsdoc": "^51.3.1", + "eslint-plugin-jsdoc": "^52.0.2", "eslint-plugin-prettier": "^5.2.1", "glob": "^11.0.1", "globals": "^16.0.0", @@ -4088,10 +4088,11 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "51.3.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-51.3.1.tgz", - "integrity": "sha512-9v/e6XyrLf1HIs/uPCgm3GcUpH4BeuGVZJk7oauKKyS7su7d5Q6zx4Fq6TiYh+w7+b4Svy7ZWVCcNZJNx3y52w==", + "version": "52.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-52.0.2.tgz", + "integrity": "sha512-fYrnc7OpRifxxKjH78Y9/D/EouQDYD3G++bpR1Y+A+fy+CMzKZAdGIiHTIxCd2U10hb2y1NxN5TJt9aupq1vmw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@es-joy/jsdoccomment": "~0.52.0", "are-docs-informative": "^0.0.2", diff --git a/package.json b/package.json index 6ae18bd4a..3535ae06d 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,7 @@ "eslint": "^9.15.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^10.1.1", - "eslint-plugin-jsdoc": "^51.3.1", + "eslint-plugin-jsdoc": "^52.0.2", "eslint-plugin-prettier": "^5.2.1", "glob": "^11.0.1", "globals": "^16.0.0", From 5e48e0db8c81262f45cf122a233afecbe41fe066 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 13:58:13 +0000 Subject: [PATCH 20/57] chore(deps): bump concurrently from 9.1.2 to 9.2.0 Bumps [concurrently](https://github.com/open-cli-tools/concurrently) from 9.1.2 to 9.2.0. - [Release notes](https://github.com/open-cli-tools/concurrently/releases) - [Commits](https://github.com/open-cli-tools/concurrently/compare/v9.1.2...v9.2.0) --- updated-dependencies: - dependency-name: concurrently dependency-version: 9.2.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 95e9a39b2..cbe2ca26e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3238,10 +3238,11 @@ } }, "node_modules/concurrently": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", - "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz", + "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.1.2", "lodash": "^4.17.21", From d5e347db44f0ef38d45f80be88505f73014b024e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:02:34 -0700 Subject: [PATCH 21/57] chore(deps): bump actions/first-interaction from 1 to 2 (#9274) Bumps [actions/first-interaction](https://github.com/actions/first-interaction) from 1 to 2. - [Release notes](https://github.com/actions/first-interaction/releases) - [Commits](https://github.com/actions/first-interaction/compare/v1...v2) --- updated-dependencies: - dependency-name: actions/first-interaction dependency-version: '2' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/welcome_new_contributors.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/welcome_new_contributors.yml b/.github/workflows/welcome_new_contributors.yml index 37ca9ef89..663f03205 100644 --- a/.github/workflows/welcome_new_contributors.yml +++ b/.github/workflows/welcome_new_contributors.yml @@ -9,7 +9,7 @@ jobs: permissions: pull-requests: write steps: - - uses: actions/first-interaction@v1 + - uses: actions/first-interaction@v2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} pr-message: > From 683a4357ff158121c119d2e6d49b24e639263b7a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:13:43 -0700 Subject: [PATCH 22/57] chore(deps): bump gulp-rename from 2.0.0 to 2.1.0 (#9277) Bumps [gulp-rename](https://github.com/hparra/gulp-rename) from 2.0.0 to 2.1.0. - [Release notes](https://github.com/hparra/gulp-rename/releases) - [Changelog](https://github.com/hparra/gulp-rename/blob/master/CHANGELOG.md) - [Commits](https://github.com/hparra/gulp-rename/compare/v2.0.0...v2.1.0) --- updated-dependencies: - dependency-name: gulp-rename dependency-version: 2.1.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ce3036193..b1ff8dcbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5536,10 +5536,11 @@ "dev": true }, "node_modules/gulp-rename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-2.0.0.tgz", - "integrity": "sha512-97Vba4KBzbYmR5VBs9mWmK+HwIf5mj+/zioxfZhOKeXtx5ZjBk57KFlePf5nxq9QsTtFl0ejnHE3zTC9MHXqyQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-2.1.0.tgz", + "integrity": "sha512-dGuzuH8jQGqCMqC544IEPhs5+O2l+IkdoSZsgd4kY97M1CxQeI3qrmweQBIrxLBbjbe/8uEWK8HHcNBc3OCy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } From f24940e7816891ee78add455d37edbdf7f17ab23 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Mon, 4 Aug 2025 16:14:44 -0400 Subject: [PATCH 23/57] fix: dont add comments to full block fields (#9263) * fix: dont add comments to full block fields * chore: remove some nonnull assertions --- core/block.ts | 2 +- core/contextmenu_items.ts | 23 +++++++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/core/block.ts b/core/block.ts index 9f7c11d4f..8ba61b1ca 100644 --- a/core/block.ts +++ b/core/block.ts @@ -1118,7 +1118,7 @@ export class Block { * * @yields A generator that can be used to iterate the fields on the block. */ - *getFields(): Generator { + *getFields(): Generator { for (const input of this.inputList) { for (const field of input.fieldRow) { yield field; diff --git a/core/contextmenu_items.ts b/core/contextmenu_items.ts index 774bfdde2..8bb71775f 100644 --- a/core/contextmenu_items.ts +++ b/core/contextmenu_items.ts @@ -25,6 +25,12 @@ import {StatementInput} from './renderers/zelos/zelos.js'; import {Coordinate} from './utils/coordinate.js'; import type {WorkspaceSvg} from './workspace_svg.js'; +function isFullBlockField(block?: BlockSvg) { + if (!block || !block.isSimpleReporter()) return false; + const firstField = block.getFields().next().value; + return firstField?.isFullBlockField(); +} + /** * Option to undo previous action. */ @@ -362,10 +368,15 @@ export function registerComment() { preconditionFn(scope: Scope) { const block = scope.block; if ( - !block!.isInFlyout && - block!.workspace.options.comments && - !block!.isCollapsed() && - block!.isEditable() + block && + !block.isInFlyout && + block.workspace.options.comments && + !block.isCollapsed() && + block.isEditable() && + // Either block already has a comment so let us remove it, + // or the block isn't just one full-block field block, which + // shouldn't be allowed to have comments as there's no way to read them. + (block.hasIcon(CommentIcon.TYPE) || !isFullBlockField(block)) ) { return 'enabled'; } @@ -373,8 +384,8 @@ export function registerComment() { }, callback(scope: Scope) { const block = scope.block; - if (block!.hasIcon(CommentIcon.TYPE)) { - block!.setCommentText(null); + if (block && block.hasIcon(CommentIcon.TYPE)) { + block.setCommentText(null); } else { block!.setCommentText(''); } From 88151fcadd00a5812efa67e9221def35da353f93 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 4 Aug 2025 13:18:31 -0700 Subject: [PATCH 24/57] fix: Display focused icons without transparency. (#9268) --- core/css.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/css.ts b/core/css.ts index 4f4a4daaf..30ee47fc5 100644 --- a/core/css.ts +++ b/core/css.ts @@ -241,7 +241,7 @@ let content = ` cursor: default; } -.blocklyIconGroup:not(:hover), +.blocklyIconGroup:not(:hover):not(:focus), .blocklyIconGroupReadonly { opacity: .6; } From 44e78b1456f969c2050099800be82a97e87433d7 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 5 Aug 2025 11:17:10 -0700 Subject: [PATCH 25/57] feat: Add an option to copy subsequent blocks when getting copy data from a block. (#9279) --- core/block_svg.ts | 7 +++++-- tests/mocha/clipboard_test.js | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index 1b85d38ce..c6065282a 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -951,9 +951,12 @@ export class BlockSvg /** * Encode a block for copying. * + * @param addNextBlocks If true, copy subsequent blocks attached to this one + * as well. + * * @returns Copy metadata, or null if the block is an insertion marker. */ - toCopyData(): BlockCopyData | null { + toCopyData(addNextBlocks = false): BlockCopyData | null { if (this.isInsertionMarker_) { return null; } @@ -961,7 +964,7 @@ export class BlockSvg paster: BlockPaster.TYPE, blockState: blocks.save(this, { addCoordinates: true, - addNextBlocks: false, + addNextBlocks, saveIds: false, }) as blocks.State, typeCounts: common.getBlockTypeCounts(this, true), diff --git a/tests/mocha/clipboard_test.js b/tests/mocha/clipboard_test.js index 85cdd2297..d58f78b9b 100644 --- a/tests/mocha/clipboard_test.js +++ b/tests/mocha/clipboard_test.js @@ -61,6 +61,31 @@ suite('Clipboard', function () { ); }); + test('pasting blocks includes next blocks if requested', function () { + const block = Blockly.serialization.blocks.append( + { + 'type': 'controls_if', + 'id': 'blockId', + 'next': { + 'block': { + 'type': 'controls_if', + 'id': 'blockId2', + }, + }, + }, + this.workspace, + ); + assert.equal(this.workspace.getBlocksByType('controls_if').length, 2); + // Both blocks should be copied + const data = block.toCopyData(true); + this.clock.runAll(); + + Blockly.clipboard.paste(data, this.workspace); + this.clock.runAll(); + // After pasting, we should have gone from 2 to 4 blocks. + assert.equal(this.workspace.getBlocksByType('controls_if').length, 4); + }); + test('copied from a mutator pastes them into the mutator', async function () { const block = Blockly.serialization.blocks.append( { From af57a3eaa387a4ebe374332eed912a1124639e3c Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 6 Aug 2025 12:45:17 -0700 Subject: [PATCH 26/57] refactor: Make `InsertionMarkerPreviewer`'s block serialization amenable to subclassing. (#9282) --- core/insertion_marker_previewer.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/core/insertion_marker_previewer.ts b/core/insertion_marker_previewer.ts index 2343b9adc..8b5b82468 100644 --- a/core/insertion_marker_previewer.ts +++ b/core/insertion_marker_previewer.ts @@ -150,8 +150,17 @@ export class InsertionMarkerPreviewer implements IConnectionPreviewer { return markerConn; } - private createInsertionMarker(origBlock: BlockSvg) { - const blockJson = blocks.save(origBlock, { + /** + * Transforms the given block into a JSON representation used to construct an + * insertion marker. + * + * @param block The block to serialize and use as an insertion marker. + * @returns A JSON-formatted string corresponding to a serialized + * representation of the given block suitable for use as an insertion + * marker. + */ + protected serializeBlockToInsertionMarker(block: BlockSvg) { + const blockJson = blocks.save(block, { addCoordinates: false, addInputBlocks: false, addNextBlocks: false, @@ -160,10 +169,15 @@ export class InsertionMarkerPreviewer implements IConnectionPreviewer { if (!blockJson) { throw new Error( - `Failed to serialize source block. ${origBlock.toDevString()}`, + `Failed to serialize source block. ${block.toDevString()}`, ); } + return blockJson; + } + + private createInsertionMarker(origBlock: BlockSvg) { + const blockJson = this.serializeBlockToInsertionMarker(origBlock); const result = blocks.append(blockJson, this.workspace) as BlockSvg; // Turn shadow blocks that are created programmatically during From 62f3b8914a1d3dfbb1f4a64c41adba56957663d5 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Wed, 6 Aug 2025 17:01:59 -0400 Subject: [PATCH 27/57] chore: add tests for clipboard (#9254) * chore: add tests for clipboard * chore: clean up --- tests/browser/test/clipboard_test.mjs | 611 ++++++++++++++++++++++++++ tests/browser/test/test_setup.mjs | 28 ++ 2 files changed, 639 insertions(+) create mode 100644 tests/browser/test/clipboard_test.mjs diff --git a/tests/browser/test/clipboard_test.mjs b/tests/browser/test/clipboard_test.mjs new file mode 100644 index 000000000..37dd359d3 --- /dev/null +++ b/tests/browser/test/clipboard_test.mjs @@ -0,0 +1,611 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as chai from 'chai'; +import {Key} from 'webdriverio'; +import { + PAUSE_TIME, + clickWorkspace, + focusOnBlock, + getAllBlocks, + getBlockTypeFromWorkspace, + getCategory, + getSelectedBlockId, + getSelectedBlockType, + openMutatorForBlock, + testFileLocations, + testSetup, +} from './test_setup.mjs'; + +const testBlockJson = { + 'blocks': { + 'languageVersion': 0, + 'blocks': [ + { + 'type': 'controls_repeat_ext', + 'id': 'controls_repeat_1', + 'x': 88, + 'y': 88, + 'inputs': { + 'TIMES': { + 'shadow': { + 'type': 'math_number', + 'id': 'math_number_shadow_1', + 'fields': { + 'NUM': 10, + }, + }, + }, + 'DO': { + 'block': { + 'type': 'controls_if', + 'id': 'controls_if_1', + 'inputs': { + 'IF0': { + 'block': { + 'type': 'logic_boolean', + 'id': 'logic_boolean_1', + 'fields': { + 'BOOL': 'TRUE', + }, + }, + }, + 'DO0': { + 'block': { + 'type': 'text_print', + 'id': 'text_print_1', + 'inputs': { + 'TEXT': { + 'shadow': { + 'type': 'text', + 'id': 'text_shadow_1', + 'fields': { + 'TEXT': 'abc', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + ], + }, +}; + +async function loadStartBlocks(browser) { + await browser.execute((stringifiedJson) => { + // Hangs forever if the json isn't stringified ¯\_(ツ)_/¯ + const testBlockJson = JSON.parse(stringifiedJson); + const workspace = Blockly.common.getMainWorkspace(); + Blockly.serialization.workspaces.load(testBlockJson, workspace); + }, JSON.stringify(testBlockJson)); + await browser.pause(PAUSE_TIME); +} + +suite('Clipboard test', async function () { + // Setting timeout to unlimited as these tests take longer time to run + this.timeout(0); + + // Clear the workspace and load start blocks + setup(async function () { + this.browser = await testSetup(testFileLocations.PLAYGROUND); + await this.browser.pause(PAUSE_TIME); + }); + + test('Paste block to/from main workspace', async function () { + await loadStartBlocks(this.browser); + // Select and copy the "true" block + await focusOnBlock(this.browser, 'logic_boolean_1'); + await this.browser.pause(PAUSE_TIME); + + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Check how many blocks there are before pasting + const allBlocksBeforePaste = await getAllBlocks(this.browser); + + // Paste the block while still in the main workspace + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check result + const allBlocksAfterPaste = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocksAfterPaste.length, + allBlocksBeforePaste.length + 1, + 'Expected there to be one additional block after paste', + ); + const focusedBlockId = await getSelectedBlockId(this.browser); + chai.assert.notEqual( + focusedBlockId, + 'logic_boolean_1', + 'Newly pasted block should be selected', + ); + const focusedBlockType = await getSelectedBlockType(this.browser); + chai.assert.equal( + focusedBlockType, + 'logic_boolean', + 'Newly pasted block should be selected', + ); + }); + + test('Copying a block also copies and pastes its children', async function () { + await loadStartBlocks(this.browser); + // Select and copy the "if/else" block which has children + await focusOnBlock(this.browser, 'controls_if_1'); + await this.browser.pause(PAUSE_TIME); + + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Check how many blocks there are before pasting + const allBlocksBeforePaste = await getAllBlocks(this.browser); + + // Paste the block while still in the main workspace + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check result + const allBlocksAfterPaste = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocksAfterPaste.length, + allBlocksBeforePaste.length + 4, + 'Expected there to be four additional blocks after paste', + ); + }); + + test('Paste shadow block to/from main workspace', async function () { + await loadStartBlocks(this.browser); + // Select and copy the shadow number block + await focusOnBlock(this.browser, 'math_number_shadow_1'); + await this.browser.pause(PAUSE_TIME); + + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Check how many blocks there are before pasting + const allBlocksBeforePaste = await getAllBlocks(this.browser); + + // Paste the block while still in the main workspace + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check result + const allBlocksAfterPaste = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocksAfterPaste.length, + allBlocksBeforePaste.length + 1, + 'Expected there to be one additional block after paste', + ); + const focusedBlockId = await getSelectedBlockId(this.browser); + chai.assert.notEqual( + focusedBlockId, + 'math_number_shadow_1', + 'Newly pasted block should be selected', + ); + const focusedBlockType = await getSelectedBlockType(this.browser); + chai.assert.equal( + focusedBlockType, + 'math_number', + 'Newly pasted block should be selected', + ); + const focusedBlockIsShadow = await this.browser.execute(() => { + return Blockly.common.getSelected().isShadow(); + }); + chai.assert.isFalse( + focusedBlockIsShadow, + 'Expected the pasted version of the block to not be a shadow block', + ); + }); + + test('Copy block from flyout, paste to main workspace', async function () { + // Open flyout + await getCategory(this.browser, 'Logic').then((category) => + category.click(), + ); + + // Focus on first block in flyout + await this.browser.execute(() => { + const ws = Blockly.getMainWorkspace().getFlyout().getWorkspace(); + const block = ws.getBlocksByType('controls_if')[0]; + Blockly.getFocusManager().focusNode(block); + }); + await this.browser.pause(PAUSE_TIME); + + // Copy + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Select the main workspace + await clickWorkspace(this.browser); + await this.browser.pause(PAUSE_TIME); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that the block is now on the workspace and selected + const allBlocks = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocks.length, + 1, + 'Expected there to be one block on main workspace after paste from flyout', + ); + + const focusedBlockType = await getSelectedBlockType(this.browser); + chai.assert.equal( + focusedBlockType, + 'controls_if', + 'Newly pasted block should be selected', + ); + }); + + test('Copy block from flyout, paste while flyout focused', async function () { + // Open flyout + await getCategory(this.browser, 'Logic').then((category) => + category.click(), + ); + + // Focus on first block in flyout + await this.browser.execute(() => { + const ws = Blockly.getMainWorkspace().getFlyout().getWorkspace(); + const block = ws.getBlocksByType('controls_if')[0]; + Blockly.getFocusManager().focusNode(block); + }); + await this.browser.pause(PAUSE_TIME); + + // Copy + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that the flyout is closed + const flyoutIsVisible = await this.browser + .$('.blocklyToolboxFlyout') + .then((elem) => elem.isDisplayed()); + chai.assert.isFalse(flyoutIsVisible, 'Expected flyout to not be open'); + + // Check that the block is now on the main workspace and selected + const allBlocks = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocks.length, + 1, + 'Expected there to be one block on main workspace after paste from flyout', + ); + + const focusedBlockType = await getSelectedBlockType(this.browser); + chai.assert.equal( + focusedBlockType, + 'controls_if', + 'Newly pasted block should be selected', + ); + }); + + test('Copy block from mutator flyout, paste to mutator workspace', async function () { + // Load the start blocks + await loadStartBlocks(this.browser); + + // Open the controls_if mutator + const block = await getBlockTypeFromWorkspace( + this.browser, + 'controls_if', + 0, + ); + await openMutatorForBlock(this.browser, block); + + // Select the first block in the mutator flyout + await this.browser.execute( + (blockId, mutatorBlockType) => { + const flyoutBlock = Blockly.getMainWorkspace() + .getBlockById(blockId) + .mutator.getWorkspace() + .getFlyout() + .getWorkspace() + .getBlocksByType(mutatorBlockType)[0]; + + Blockly.getFocusManager().focusNode(flyoutBlock); + }, + 'controls_if_1', + 'controls_if_elseif', + ); + await this.browser.pause(PAUSE_TIME); + + // Copy + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that the block is now in the mutator workspace and selected + const numberOfIfElseBlocks = await this.browser.execute( + (blockId, mutatorBlockType) => { + return Blockly.getMainWorkspace() + .getBlockById(blockId) + .mutator.getWorkspace() + .getBlocksByType(mutatorBlockType).length; + }, + 'controls_if_1', + 'controls_if_elseif', + ); + + chai.assert.equal( + numberOfIfElseBlocks, + 1, + 'Expected there to be one if_else block in mutator workspace', + ); + + const focusedBlockType = await getSelectedBlockType(this.browser); + chai.assert.equal( + focusedBlockType, + 'controls_if_elseif', + 'Newly pasted block should be selected', + ); + }); + + test('Copy block from mutator flyout, paste to main workspace while mutator open', async function () { + // Load the start blocks + await loadStartBlocks(this.browser); + + // Open the controls_if mutator + const block = await getBlockTypeFromWorkspace( + this.browser, + 'controls_if', + 0, + ); + await openMutatorForBlock(this.browser, block); + + // Select the first block in the mutator flyout + await this.browser.execute( + (blockId, mutatorBlockType) => { + const flyoutBlock = Blockly.getMainWorkspace() + .getBlockById(blockId) + .mutator.getWorkspace() + .getFlyout() + .getWorkspace() + .getBlocksByType(mutatorBlockType)[0]; + + Blockly.getFocusManager().focusNode(flyoutBlock); + }, + 'controls_if_1', + 'controls_if_elseif', + ); + await this.browser.pause(PAUSE_TIME); + + // Copy + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Click the main workspace + await clickWorkspace(this.browser); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that the block is now in the mutator workspace and selected + const numberOfIfElseBlocks = await this.browser.execute( + (blockId, mutatorBlockType) => { + return Blockly.getMainWorkspace() + .getBlockById(blockId) + .mutator.getWorkspace() + .getBlocksByType(mutatorBlockType).length; + }, + 'controls_if_1', + 'controls_if_elseif', + ); + + chai.assert.equal( + numberOfIfElseBlocks, + 1, + 'Expected there to be one if_else block in mutator workspace', + ); + + const focusedBlockType = await getSelectedBlockType(this.browser); + chai.assert.equal( + focusedBlockType, + 'controls_if_elseif', + 'Newly pasted block should be selected', + ); + + // Check that there are no new blocks on the main workspace + const numberOfIfElseBlocksOnMainWorkspace = await this.browser.execute( + (mutatorBlockType) => { + return Blockly.getMainWorkspace().getBlocksByType(mutatorBlockType) + .length; + }, + 'controls_if_elseif', + ); + chai.assert.equal( + numberOfIfElseBlocksOnMainWorkspace, + 0, + 'Mutator blocks should not appear on main workspace', + ); + }); + + test('Copy block from mutator flyout, paste to main workspace while mutator closed', async function () { + // Load the start blocks + await loadStartBlocks(this.browser); + + // Open the controls_if mutator + const block = await getBlockTypeFromWorkspace( + this.browser, + 'controls_if', + 0, + ); + await openMutatorForBlock(this.browser, block); + + // Select the first block in the mutator flyout + await this.browser.execute( + (blockId, mutatorBlockType) => { + const flyoutBlock = Blockly.getMainWorkspace() + .getBlockById(blockId) + .mutator.getWorkspace() + .getFlyout() + .getWorkspace() + .getBlocksByType(mutatorBlockType)[0]; + + Blockly.getFocusManager().focusNode(flyoutBlock); + }, + 'controls_if_1', + 'controls_if_elseif', + ); + await this.browser.pause(PAUSE_TIME); + + // Copy + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Close the mutator flyout (calling this method on open mutator closes it) + await openMutatorForBlock(this.browser, block); + + // Click the main workspace + await clickWorkspace(this.browser); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that there are no new blocks on the main workspace + const numberOfIfElseBlocksOnMainWorkspace = await this.browser.execute( + (mutatorBlockType) => { + return Blockly.getMainWorkspace().getBlocksByType(mutatorBlockType) + .length; + }, + 'controls_if_elseif', + ); + chai.assert.equal( + numberOfIfElseBlocksOnMainWorkspace, + 0, + 'Mutator blocks should not appear on main workspace', + ); + }); + + test('Copy workspace comment, paste to main workspace', async function () { + // Add a workspace comment to the workspace + await this.browser.execute(() => { + const workspace = Blockly.getMainWorkspace(); + const json = { + 'workspaceComments': [ + { + 'height': 100, + 'width': 120, + 'id': 'workspace_comment_1', + 'x': 13, + 'y': -12, + 'text': 'This is a comment', + }, + ], + }; + Blockly.serialization.workspaces.load(json, workspace); + }); + await this.browser.pause(PAUSE_TIME); + + // Select the workspace comment + await this.browser.execute(() => { + const comment = Blockly.getMainWorkspace().getCommentById( + 'workspace_comment_1', + ); + Blockly.getFocusManager().focusNode(comment); + }); + await this.browser.pause(PAUSE_TIME); + + // Copy + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Click the main workspace + await clickWorkspace(this.browser); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that there are 2 comments on the workspace + const numberOfComments = await this.browser.execute(() => { + return Blockly.getMainWorkspace().getTopComments().length; + }); + chai.assert.equal( + numberOfComments, + 2, + 'Expected 2 workspace comments after pasting', + ); + }); + + test('Cut block from main workspace, paste to main workspace', async function () { + await loadStartBlocks(this.browser); + // Select and cut the "true" block + await focusOnBlock(this.browser, 'logic_boolean_1'); + await this.browser.pause(PAUSE_TIME); + + await this.browser.keys([Key.Ctrl, 'x']); + await this.browser.pause(PAUSE_TIME); + + // Check that the "true" block was deleted + const trueBlock = await this.browser.execute(() => { + return Blockly.getMainWorkspace().getBlockById('logic_boolean_1') ?? null; + }); + chai.assert.isNull(trueBlock); + + // Check how many blocks there are before pasting + const allBlocksBeforePaste = await getAllBlocks(this.browser); + + // Paste the block while still in the main workspace + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check result + const allBlocksAfterPaste = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocksAfterPaste.length, + allBlocksBeforePaste.length + 1, + 'Expected there to be one additional block after paste', + ); + }); + + test('Cannot cut block from flyout', async function () { + // Open flyout + await getCategory(this.browser, 'Logic').then((category) => + category.click(), + ); + + // Focus on first block in flyout + await this.browser.execute(() => { + const ws = Blockly.getMainWorkspace().getFlyout().getWorkspace(); + const block = ws.getBlocksByType('controls_if')[0]; + Blockly.getFocusManager().focusNode(block); + }); + await this.browser.pause(PAUSE_TIME); + + // Cut + await this.browser.keys([Key.Ctrl, 'x']); + await this.browser.pause(PAUSE_TIME); + + // Select the main workspace + await clickWorkspace(this.browser); + await this.browser.pause(PAUSE_TIME); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that no block was pasted + const allBlocks = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocks.length, + 0, + 'Expected no blocks in the workspace because nothing to paste', + ); + }); +}); diff --git a/tests/browser/test/test_setup.mjs b/tests/browser/test/test_setup.mjs index 6cf4986fc..0a8998c3e 100644 --- a/tests/browser/test/test_setup.mjs +++ b/tests/browser/test/test_setup.mjs @@ -127,6 +127,23 @@ export const screenDirection = { LTR: 1, }; +/** + * Focuses and selects a block with the provided ID. + * + * This throws an error if no block exists for the specified ID. + * + * @param browser The active WebdriverIO Browser object. + * @param blockId The ID of the block to select. + */ +export async function focusOnBlock(browser, blockId) { + return await browser.execute((blockId) => { + const workspaceSvg = Blockly.getMainWorkspace(); + const block = workspaceSvg.getBlockById(blockId); + if (!block) throw new Error(`No block found with ID: ${blockId}.`); + Blockly.getFocusManager().focusNode(block); + }, blockId); +} + /** * @param browser The active WebdriverIO Browser object. * @return A Promise that resolves to the ID of the currently selected block. @@ -138,6 +155,17 @@ export async function getSelectedBlockId(browser) { }); } +/** + * @param browser The active WebdriverIO Browser object. + * @return A Promise that resolves to the ID of the currently selected block. + */ +export async function getSelectedBlockType(browser) { + return await browser.execute(() => { + // Note: selected is an ICopyable and I am assuming that it is a BlockSvg. + return Blockly.common.getSelected()?.type; + }); +} + /** * @param browser The active WebdriverIO Browser object. * @return A Promise that resolves to the selected block's root SVG element, From f9d0ec9d24d6ee78a0e4a52327dbd0e1688b7f1b Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 6 Aug 2025 14:04:12 -0700 Subject: [PATCH 28/57] refactor: Associate comment bar buttons with the comment view. (#9278) --- core/comments/collapse_comment_bar_button.ts | 9 +++++---- core/comments/comment_bar_button.ts | 16 +++++----------- core/comments/comment_view.ts | 11 ++++++++--- core/comments/delete_comment_bar_button.ts | 6 ++++-- .../comment_bar_button_navigation_policy.ts | 8 +++++--- core/keyboard_nav/line_cursor.ts | 13 +++++++++++-- 6 files changed, 38 insertions(+), 25 deletions(-) diff --git a/core/comments/collapse_comment_bar_button.ts b/core/comments/collapse_comment_bar_button.ts index b0738d707..304e2af81 100644 --- a/core/comments/collapse_comment_bar_button.ts +++ b/core/comments/collapse_comment_bar_button.ts @@ -10,6 +10,7 @@ import * as dom from '../utils/dom.js'; import {Svg} from '../utils/svg.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; import {CommentBarButton} from './comment_bar_button.js'; +import type {CommentView} from './comment_view.js'; /** * Magic string appended to the comment ID to create a unique ID for this button. @@ -42,8 +43,9 @@ export class CollapseCommentBarButton extends CommentBarButton { protected readonly id: string, protected readonly workspace: WorkspaceSvg, protected readonly container: SVGGElement, + protected readonly commentView: CommentView, ) { - super(id, workspace, container); + super(id, workspace, container, commentView); this.icon = dom.createSvgElement( Svg.IMAGE, @@ -86,14 +88,13 @@ export class CollapseCommentBarButton extends CommentBarButton { override performAction(e?: Event) { touch.clearTouchIdentifier(); - const comment = this.getParentComment(); - comment.view.bringToFront(); + this.getCommentView().bringToFront(); if (e && e instanceof PointerEvent && browserEvents.isRightButton(e)) { e.stopPropagation(); return; } - comment.setCollapsed(!comment.isCollapsed()); + this.getCommentView().setCollapsed(!this.getCommentView().isCollapsed()); this.workspace.hideChaff(); e?.stopPropagation(); diff --git a/core/comments/comment_bar_button.ts b/core/comments/comment_bar_button.ts index d78a7fd86..24a084ad2 100644 --- a/core/comments/comment_bar_button.ts +++ b/core/comments/comment_bar_button.ts @@ -7,7 +7,7 @@ import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; import {Rect} from '../utils/rect.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; -import type {RenderedWorkspaceComment} from './rendered_workspace_comment.js'; +import type {CommentView} from './comment_view.js'; /** * Button displayed on a comment's top bar. @@ -29,6 +29,7 @@ export abstract class CommentBarButton implements IFocusableNode { protected readonly id: string, protected readonly workspace: WorkspaceSvg, protected readonly container: SVGGElement, + protected readonly commentView: CommentView, ) {} /** @@ -39,17 +40,10 @@ export abstract class CommentBarButton implements IFocusableNode { } /** - * Returns the parent comment of this comment bar button. + * Returns the parent comment view of this comment bar button. */ - getParentComment(): RenderedWorkspaceComment { - const comment = this.workspace.getCommentById(this.id); - if (!comment) { - throw new Error( - `Comment bar button ${this.id} has no corresponding comment`, - ); - } - - return comment; + getCommentView(): CommentView { + return this.commentView; } /** Adjusts the position of this button within its parent container. */ diff --git a/core/comments/comment_view.ts b/core/comments/comment_view.ts index 936d74650..ca0c261c3 100644 --- a/core/comments/comment_view.ts +++ b/core/comments/comment_view.ts @@ -102,7 +102,7 @@ export class CommentView implements IRenderedElement { constructor( readonly workspace: WorkspaceSvg, - private commentId: string, + readonly commentId: string, ) { this.svgRoot = dom.createSvgElement(Svg.G, { 'class': 'blocklyComment blocklyEditable blocklyDraggable', @@ -176,12 +176,18 @@ export class CommentView implements IRenderedElement { this.commentId, this.workspace, topBarGroup, + this, ); const foldoutButton = new CollapseCommentBarButton( this.commentId, this.workspace, topBarGroup, + this, ); + this.addDisposeListener(() => { + deleteButton.dispose(); + foldoutButton.dispose(); + }); const textPreview = dom.createSvgElement( Svg.TEXT, { @@ -612,13 +618,12 @@ export class CommentView implements IRenderedElement { /** Disposes of this comment view. */ dispose() { this.disposing = true; - this.foldoutButton.dispose(); - this.deleteButton.dispose(); dom.removeNode(this.svgRoot); // Loop through listeners backwards in case they remove themselves. for (let i = this.disposeListeners.length - 1; i >= 0; i--) { this.disposeListeners[i](); } + this.disposeListeners.length = 0; this.disposed = true; } diff --git a/core/comments/delete_comment_bar_button.ts b/core/comments/delete_comment_bar_button.ts index 0b7dcd0ea..c61db9b9c 100644 --- a/core/comments/delete_comment_bar_button.ts +++ b/core/comments/delete_comment_bar_button.ts @@ -11,6 +11,7 @@ import * as dom from '../utils/dom.js'; import {Svg} from '../utils/svg.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; import {CommentBarButton} from './comment_bar_button.js'; +import type {CommentView} from './comment_view.js'; /** * Magic string appended to the comment ID to create a unique ID for this button. @@ -42,8 +43,9 @@ export class DeleteCommentBarButton extends CommentBarButton { protected readonly id: string, protected readonly workspace: WorkspaceSvg, protected readonly container: SVGGElement, + protected readonly commentView: CommentView, ) { - super(id, workspace, container); + super(id, workspace, container, commentView); this.icon = dom.createSvgElement( Svg.IMAGE, @@ -97,7 +99,7 @@ export class DeleteCommentBarButton extends CommentBarButton { return; } - this.getParentComment().dispose(); + this.getCommentView().dispose(); e?.stopPropagation(); getFocusManager().focusNode(this.workspace); } diff --git a/core/keyboard_nav/comment_bar_button_navigation_policy.ts b/core/keyboard_nav/comment_bar_button_navigation_policy.ts index f676f4655..6654d2d8f 100644 --- a/core/keyboard_nav/comment_bar_button_navigation_policy.ts +++ b/core/keyboard_nav/comment_bar_button_navigation_policy.ts @@ -31,7 +31,9 @@ export class CommentBarButtonNavigationPolicy * @returns The parent comment of the given CommentBarButton. */ getParent(current: CommentBarButton): IFocusableNode | null { - return current.getParentComment(); + return current + .getCommentView() + .workspace.getCommentById(current.getCommentView().commentId); } /** @@ -41,7 +43,7 @@ export class CommentBarButtonNavigationPolicy * @returns The next CommentBarButton, if any. */ getNextSibling(current: CommentBarButton): IFocusableNode | null { - const children = current.getParentComment().view.getCommentBarButtons(); + const children = current.getCommentView().getCommentBarButtons(); const currentIndex = children.indexOf(current); if (currentIndex >= 0 && currentIndex + 1 < children.length) { return children[currentIndex + 1]; @@ -56,7 +58,7 @@ export class CommentBarButtonNavigationPolicy * @returns The CommentBarButton's previous CommentBarButton, if any. */ getPreviousSibling(current: CommentBarButton): IFocusableNode | null { - const children = current.getParentComment().view.getCommentBarButtons(); + const children = current.getCommentView().getCommentBarButtons(); const currentIndex = children.indexOf(current); if (currentIndex > 0) { return children[currentIndex - 1]; diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index c621e3a89..13e5a729d 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -20,6 +20,7 @@ import {Field} from '../field.js'; import {getFocusManager} from '../focus_manager.js'; import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; import * as registry from '../registry.js'; +import {Rect} from '../utils/rect.js'; import {WorkspaceSvg} from '../workspace_svg.js'; import {Marker} from './marker.js'; @@ -405,8 +406,16 @@ export class LineCursor extends Marker { } else if (newNode instanceof RenderedWorkspaceComment) { newNode.workspace.scrollBoundsIntoView(newNode.getBoundingRectangle()); } else if (newNode instanceof CommentBarButton) { - const comment = newNode.getParentComment(); - comment.workspace.scrollBoundsIntoView(comment.getBoundingRectangle()); + const commentView = newNode.getCommentView(); + const xy = commentView.getRelativeToSurfaceXY(); + const size = commentView.getSize(); + const bounds = new Rect( + xy.y, + xy.y + size.height, + xy.x, + xy.x + size.width, + ); + commentView.workspace.scrollBoundsIntoView(bounds); } } From 7d1d745416621cc352c1048e6fb06c9f611cbcc4 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 6 Aug 2025 14:08:01 -0700 Subject: [PATCH 29/57] fix: Drag immovable and shadow blocks along with their parent. (#9281) --- core/block.ts | 42 ++++++++++++++++++++++++--------------- tests/mocha/block_test.js | 29 +++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/core/block.ts b/core/block.ts index 8ba61b1ca..d15700a7e 100644 --- a/core/block.ts +++ b/core/block.ts @@ -501,22 +501,32 @@ export class Block { // Detach this block from the parent's tree. this.previousConnection.disconnect(); } - const nextBlock = this.getNextBlock(); - if (opt_healStack && nextBlock && !nextBlock.isShadow()) { - // Disconnect the next statement. - const nextTarget = this.nextConnection?.targetConnection ?? null; - nextTarget?.disconnect(); - if ( - previousTarget && - this.workspace.connectionChecker.canConnect( - previousTarget, - nextTarget, - false, - ) - ) { - // Attach the next statement to the previous statement. - previousTarget.connect(nextTarget!); - } + + if (!opt_healStack) return; + + // Immovable or shadow next blocks need to move along with the block; keep + // going until we encounter a normal block or run off the end of the stack. + let nextBlock = this.getNextBlock(); + while (nextBlock && (nextBlock.isShadow() || !nextBlock.isMovable())) { + nextBlock = nextBlock.getNextBlock(); + } + if (!nextBlock) return; + + // Disconnect the next statement. + const nextTarget = + nextBlock.previousConnection?.targetBlock()?.nextConnection + ?.targetConnection ?? null; + nextTarget?.disconnect(); + if ( + previousTarget && + this.workspace.connectionChecker.canConnect( + previousTarget, + nextTarget, + false, + ) + ) { + // Attach the next statement to the previous statement. + previousTarget.connect(nextTarget!); } } diff --git a/tests/mocha/block_test.js b/tests/mocha/block_test.js index 62c61ce00..e3bd47090 100644 --- a/tests/mocha/block_test.js +++ b/tests/mocha/block_test.js @@ -201,6 +201,35 @@ suite('Blocks', function () { assertUnpluggedHealFailed(blocks); }); + test('Disconnect top of stack with immovable sibling', function () { + this.blocks.B.setMovable(false); + this.blocks.A.unplug(true); + assert.equal(this.blocks.A.nextConnection.targetBlock(), this.blocks.B); + assert.isNull(this.blocks.B.nextConnection.targetBlock()); + assert.isNull(this.blocks.C.previousConnection.targetBlock()); + }); + test('Heal with immovable sibling mid-stack', function () { + const blockD = this.workspace.newBlock('stack_block', 'd'); + this.blocks.C.nextConnection.connect(blockD.previousConnection); + this.blocks.C.setMovable(false); + this.blocks.B.unplug(true); + assert.equal(this.blocks.A.nextConnection.targetBlock(), blockD); + assert.equal(this.blocks.B.nextConnection.targetBlock(), this.blocks.C); + assert.isNull(this.blocks.C.nextConnection.targetBlock()); + }); + test('Heal with immovable sibling and shadow sibling mid-stack', function () { + const blockD = this.workspace.newBlock('stack_block', 'd'); + const blockE = this.workspace.newBlock('stack_block', 'e'); + this.blocks.C.nextConnection.connect(blockD.previousConnection); + blockD.nextConnection.connect(blockE.previousConnection); + this.blocks.C.setMovable(false); + blockD.setShadow(true); + this.blocks.B.unplug(true); + assert.equal(this.blocks.A.nextConnection.targetBlock(), blockE); + assert.equal(this.blocks.B.nextConnection.targetBlock(), this.blocks.C); + assert.equal(this.blocks.C.nextConnection.targetBlock(), blockD); + assert.isNull(blockD.nextConnection.targetBlock()); + }); test('Child is shadow', function () { const blocks = this.blocks; blocks.C.setShadow(true); From 2e252a4bd80d376ab6b93524931e00d61e91683b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:38:45 -0700 Subject: [PATCH 30/57] chore(deps): bump google-github-actions/deploy-appengine (#9273) Bumps [google-github-actions/deploy-appengine](https://github.com/google-github-actions/deploy-appengine) from 2.1.5 to 2.1.7. - [Release notes](https://github.com/google-github-actions/deploy-appengine/releases) - [Changelog](https://github.com/google-github-actions/deploy-appengine/blob/main/CHANGELOG.md) - [Commits](https://github.com/google-github-actions/deploy-appengine/compare/v2.1.5...v2.1.7) --- updated-dependencies: - dependency-name: google-github-actions/deploy-appengine dependency-version: 2.1.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/appengine_deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/appengine_deploy.yml b/.github/workflows/appengine_deploy.yml index 1dd6d2ffa..408c1b084 100644 --- a/.github/workflows/appengine_deploy.yml +++ b/.github/workflows/appengine_deploy.yml @@ -42,7 +42,7 @@ jobs: path: _deploy/ - name: Deploy to App Engine - uses: google-github-actions/deploy-appengine@v2.1.5 + uses: google-github-actions/deploy-appengine@v2.1.7 # For parameters see: # https://github.com/google-github-actions/deploy-appengine#inputs with: From 79d314049558cd776062afcd389aef6c651f095f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 17:39:48 +0100 Subject: [PATCH 31/57] chore(deps): bump actions/download-artifact from 4 to 5 (#9287) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/appengine_deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/appengine_deploy.yml b/.github/workflows/appengine_deploy.yml index 408c1b084..50afec240 100644 --- a/.github/workflows/appengine_deploy.yml +++ b/.github/workflows/appengine_deploy.yml @@ -36,7 +36,7 @@ jobs: needs: prepare steps: - name: Download prepared files - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: appengine_files path: _deploy/ From b211c02e3cac05d37126f6024afc060509b5328b Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Mon, 11 Aug 2025 10:10:10 -0700 Subject: [PATCH 32/57] Change browser test timeout to 2 hours --- .github/workflows/browser_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/browser_test.yml b/.github/workflows/browser_test.yml index 3675af7b0..e670af926 100644 --- a/.github/workflows/browser_test.yml +++ b/.github/workflows/browser_test.yml @@ -11,7 +11,7 @@ permissions: jobs: build: - timeout-minutes: 10 + timeout-minutes: 120 runs-on: ${{ matrix.os }} strategy: From fb63360b9f7dc646293ea6419545ab5a8942a933 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 12 Aug 2025 08:55:54 -0700 Subject: [PATCH 33/57] refactor: Remove duplicated method from contextmenu_items.ts. (#9289) --- core/contextmenu_items.ts | 39 +++------------------------------------ 1 file changed, 3 insertions(+), 36 deletions(-) diff --git a/core/contextmenu_items.ts b/core/contextmenu_items.ts index 8bb71775f..001a3c58e 100644 --- a/core/contextmenu_items.ts +++ b/core/contextmenu_items.ts @@ -23,6 +23,7 @@ import {CommentIcon} from './icons/comment_icon.js'; import {Msg} from './msg.js'; import {StatementInput} from './renderers/zelos/zelos.js'; import {Coordinate} from './utils/coordinate.js'; +import * as svgMath from './utils/svg_math.js'; import type {WorkspaceSvg} from './workspace_svg.js'; function isFullBlockField(block?: BlockSvg) { @@ -637,9 +638,9 @@ export function registerCommentCreate() { const comment = new RenderedWorkspaceComment(workspace); comment.setPlaceholderText(Msg['WORKSPACE_COMMENT_DEFAULT_TEXT']); comment.moveTo( - pixelsToWorkspaceCoords( - new Coordinate(location.x, location.y), + svgMath.screenToWsCoordinates( workspace, + new Coordinate(location.x, location.y), ), ); getFocusManager().focusNode(comment); @@ -652,40 +653,6 @@ export function registerCommentCreate() { ContextMenuRegistry.registry.register(createOption); } -/** - * Converts pixel coordinates (relative to the window) to workspace coordinates. - */ -function pixelsToWorkspaceCoords( - pixelCoord: Coordinate, - workspace: WorkspaceSvg, -): Coordinate { - const injectionDiv = workspace.getInjectionDiv(); - // Bounding rect coordinates are in client coordinates, meaning that they - // are in pixels relative to the upper left corner of the visible browser - // window. These coordinates change when you scroll the browser window. - const boundingRect = injectionDiv.getBoundingClientRect(); - - // The client coordinates offset by the injection div's upper left corner. - const clientOffsetPixels = new Coordinate( - pixelCoord.x - boundingRect.left, - pixelCoord.y - boundingRect.top, - ); - - // The offset in pixels between the main workspace's origin and the upper - // left corner of the injection div. - const mainOffsetPixels = workspace.getOriginOffsetInPixels(); - - // The position of the new comment in pixels relative to the origin of the - // main workspace. - const finalOffset = Coordinate.difference( - clientOffsetPixels, - mainOffsetPixels, - ); - // The position of the new comment in main workspace coordinates. - finalOffset.scale(1 / workspace.scale); - return finalOffset; -} - /** Registers all block-scoped context menu items. */ function registerBlockOptions_() { registerDuplicate(); From e74910c8a0b189fe01ac79d653008be0158e6374 Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Tue, 12 Aug 2025 10:32:32 -0700 Subject: [PATCH 34/57] Update block-test version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3535ae06d..d674b8637 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@blockly/block-test": "^7.0.1", + "@blockly/block-test": "^7.0.2", "@blockly/dev-tools": "^9.0.0", "@blockly/theme-modern": "^7.0.1", "@hyperjump/browser": "^1.1.4", From 4f4a450142923a9ba1f36c98836b549f556706f6 Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Wed, 13 Aug 2025 09:39:24 -0700 Subject: [PATCH 35/57] Update dev-tools version in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d674b8637..e7a496b31 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "license": "Apache-2.0", "devDependencies": { "@blockly/block-test": "^7.0.2", - "@blockly/dev-tools": "^9.0.0", + "@blockly/dev-tools": "^9.0.2", "@blockly/theme-modern": "^7.0.1", "@hyperjump/browser": "^1.1.4", "@hyperjump/json-schema": "^1.5.0", From 7b784b58c0192cb45aa4e62ef8df072e713238f8 Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Wed, 13 Aug 2025 11:06:10 -0700 Subject: [PATCH 36/57] Add a weekly schedule --- .github/workflows/browser_test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/browser_test.yml b/.github/workflows/browser_test.yml index e670af926..51ac0dffa 100644 --- a/.github/workflows/browser_test.yml +++ b/.github/workflows/browser_test.yml @@ -5,6 +5,8 @@ name: Run browser manually on: workflow_dispatch: + schedule: + - cron: '0 6 * * 1' # Runs every Monday at 06:00 UTC permissions: contents: read From 34ea176b88ce1d3f242a6930f9a58859e1d4480c Mon Sep 17 00:00:00 2001 From: Erik Pasternak Date: Wed, 13 Aug 2025 18:14:01 +0000 Subject: [PATCH 37/57] Update package-lock.json --- package-lock.json | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index b1ff8dcbd..e03f6d24d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,8 @@ "jsdom": "26.1.0" }, "devDependencies": { - "@blockly/block-test": "^7.0.1", - "@blockly/dev-tools": "^9.0.0", + "@blockly/block-test": "^7.0.2", + "@blockly/dev-tools": "^9.0.2", "@blockly/theme-modern": "^7.0.1", "@hyperjump/browser": "^1.1.4", "@hyperjump/json-schema": "^1.5.0", @@ -90,10 +90,11 @@ "license": "ISC" }, "node_modules/@blockly/block-test": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-7.0.1.tgz", - "integrity": "sha512-w91ZZbpJDKGQJVO7gKqQaM17ffcsW1ktrnSTz/OpDw5R4H+1q05NgWO5gYzGPzLfFdvPcrkc0v00KhD4UG7BRA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-7.0.2.tgz", + "integrity": "sha512-fwbJnMiH4EoX/CR0ZTGzSKaGfpRBn4nudquoWfvG4ekkhTjaNTldDdHvUSeyexzvwZZcT6M4I1Jtq3IoomTKEg==", "dev": true, + "license": "Apache 2.0", "engines": { "node": ">=8.17.0" }, @@ -102,13 +103,13 @@ } }, "node_modules/@blockly/dev-tools": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-9.0.1.tgz", - "integrity": "sha512-OnY24Up00owts0VtOaokUmOQdzH+K1PNcr3LC3huwa9PO0TlKiXTq4V5OuIqBS++enyj93gXQ8PhvFGudkogTQ==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-9.0.2.tgz", + "integrity": "sha512-Ic/+BkqEvLRZxzNQVW/FKXx1cB042xXXPTSmNlTv2qr4oY+hN2fwBtHj3PirBWAzWgMOF8VDTj/EXL36jH1/lg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@blockly/block-test": "^7.0.1", + "@blockly/block-test": "^7.0.2", "@blockly/theme-dark": "^8.0.1", "@blockly/theme-deuteranopia": "^7.0.1", "@blockly/theme-highcontrast": "^7.0.1", From 414f1056e8f576e86d7702ef4738f27bf9129311 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 14:26:32 +0000 Subject: [PATCH 38/57] chore(deps): bump actions/first-interaction from 2 to 3 Bumps [actions/first-interaction](https://github.com/actions/first-interaction) from 2 to 3. - [Release notes](https://github.com/actions/first-interaction/releases) - [Commits](https://github.com/actions/first-interaction/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/first-interaction dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/welcome_new_contributors.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/welcome_new_contributors.yml b/.github/workflows/welcome_new_contributors.yml index 663f03205..4c4860c25 100644 --- a/.github/workflows/welcome_new_contributors.yml +++ b/.github/workflows/welcome_new_contributors.yml @@ -9,7 +9,7 @@ jobs: permissions: pull-requests: write steps: - - uses: actions/first-interaction@v2 + - uses: actions/first-interaction@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} pr-message: > From 86da7dcbfab72a84182f633295ce0e904b1bb9bd Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 21 Aug 2025 14:21:02 -0700 Subject: [PATCH 39/57] release: Update version number to 12.3.0-beta.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e03f6d24d..c7f681f2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "blockly", - "version": "12.2.0", + "version": "12.3.0-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blockly", - "version": "12.2.0", + "version": "12.3.0-beta.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index e7a496b31..c122a32fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockly", - "version": "12.2.0", + "version": "12.3.0-beta.0", "description": "Blockly is a library for building visual programming editors.", "keywords": [ "blockly" From 84933b9119b0d874d80a9e7f0fa0b34d6d10fa71 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Tue, 19 Aug 2025 11:22:17 -0400 Subject: [PATCH 40/57] chore: lint error on only in mocha tests (#9300) --- eslint.config.mjs | 5 +++++ package-lock.json | 26 ++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 32 insertions(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index f018e525d..0560586cb 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,6 +1,7 @@ import eslint from '@eslint/js'; import googleStyle from 'eslint-config-google'; import jsdoc from 'eslint-plugin-jsdoc'; +import mochaPlugin from 'eslint-plugin-mocha'; import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; import globals from 'globals'; import tseslint from 'typescript-eslint'; @@ -200,6 +201,9 @@ export default [ }, { files: ['tests/**'], + plugins: { + mocha: mochaPlugin, + }, languageOptions: { globals: { 'Blockly': true, @@ -219,6 +223,7 @@ export default [ 'jsdoc/check-tag-names': ['warn', {'definedTags': ['record']}], 'jsdoc/tag-lines': ['off'], 'jsdoc/no-defaults': ['off'], + 'mocha/no-exclusive-tests': 'error', }, }, { diff --git a/package-lock.json b/package-lock.json index c7f681f2e..d30a69df6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^10.1.1", "eslint-plugin-jsdoc": "^52.0.2", + "eslint-plugin-mocha": "^11.1.0", "eslint-plugin-prettier": "^5.2.1", "glob": "^11.0.1", "globals": "^16.0.0", @@ -4137,6 +4138,31 @@ "spdx-license-ids": "^3.0.0" } }, + "node_modules/eslint-plugin-mocha": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-11.1.0.tgz", + "integrity": "sha512-rKntVWRsQFPbf8OkSgVNRVRrcVAPaGTyEgWCEyXaPDJkTl0v5/lwu1vTk5sWiUJU8l2sxwvGUZzSNrEKdVMeQw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.1", + "globals": "^15.14.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0" + } + }, + "node_modules/eslint-plugin-mocha/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-plugin-prettier": { "version": "5.5.1", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz", diff --git a/package.json b/package.json index c122a32fe..6cfd025c7 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^10.1.1", "eslint-plugin-jsdoc": "^52.0.2", + "eslint-plugin-mocha": "^11.1.0", "eslint-plugin-prettier": "^5.2.1", "glob": "^11.0.1", "globals": "^16.0.0", From 29d5b43cd3ba1d210472c064c16e9a1c9dd7cddb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 08:28:11 -0700 Subject: [PATCH 41/57] chore(deps): bump actions/checkout from 4 to 5 (#9320) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/appengine_deploy.yml | 2 +- .github/workflows/browser_test.yml | 2 +- .github/workflows/build.yml | 6 +++--- .github/workflows/keyboard_plugin_test.yml | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/appengine_deploy.yml b/.github/workflows/appengine_deploy.yml index 50afec240..efc6fe941 100644 --- a/.github/workflows/appengine_deploy.yml +++ b/.github/workflows/appengine_deploy.yml @@ -15,7 +15,7 @@ jobs: steps: # Checks-out the repository under $GITHUB_WORKSPACE. # When running manually this checks out the master branch. - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Prepare demo files # Install all dependencies, then copy all the files needed for demos. diff --git a/.github/workflows/browser_test.yml b/.github/workflows/browser_test.yml index 51ac0dffa..c2ce99136 100644 --- a/.github/workflows/browser_test.yml +++ b/.github/workflows/browser_test.yml @@ -26,7 +26,7 @@ jobs: # https://nodejs.org/en/about/releases/ steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4ab688f8..d7a4e786c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: # https://nodejs.org/en/about/releases/ steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false @@ -54,7 +54,7 @@ jobs: timeout-minutes: 5 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Use Node.js 20.x uses: actions/setup-node@v4 @@ -71,7 +71,7 @@ jobs: timeout-minutes: 5 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Use Node.js 20.x uses: actions/setup-node@v4 diff --git a/.github/workflows/keyboard_plugin_test.yml b/.github/workflows/keyboard_plugin_test.yml index 753d31dda..3d7d3d5d4 100644 --- a/.github/workflows/keyboard_plugin_test.yml +++ b/.github/workflows/keyboard_plugin_test.yml @@ -25,12 +25,12 @@ jobs: steps: - name: Checkout core Blockly - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: path: core-blockly - name: Checkout keyboard navigation plugin - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: 'google/blockly-keyboard-experimentation' ref: 'main' From 10b1d1e848239ee6eff6fb6a1dd3e2cf2ead590f Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 19 Aug 2025 08:32:31 -0700 Subject: [PATCH 42/57] fix: Fix positioning of pasted blocks and comments in RTL. (#9302) * fix: Fix positioning of pasted blocks in RTL. * fix: Clean up after temporarily making the workspace RTL. * fix: Remove .only. * fix: Fix positioning of pasted comments in RTL. * fix: Fix positioning of text preview on collapsed comments in RTL. --- core/clipboard/block_paster.ts | 3 ++ core/comments/comment_view.ts | 5 +++- core/xml.ts | 2 +- tests/mocha/clipboard_test.js | 51 ++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/core/clipboard/block_paster.ts b/core/clipboard/block_paster.ts index 750cedca1..e782cc0b0 100644 --- a/core/clipboard/block_paster.ts +++ b/core/clipboard/block_paster.ts @@ -83,6 +83,9 @@ export function moveBlockToNotConflict( block: BlockSvg, originalPosition: Coordinate, ) { + if (block.workspace.RTL) { + originalPosition.x = block.workspace.getWidth() - originalPosition.x; + } const workspace = block.workspace; const snapRadius = config.snapRadius; const bumpOffset = Coordinate.difference( diff --git a/core/comments/comment_view.ts b/core/comments/comment_view.ts index ca0c261c3..b1cd628f8 100644 --- a/core/comments/comment_view.ts +++ b/core/comments/comment_view.ts @@ -368,7 +368,10 @@ export class CommentView implements IRenderedElement { const textPreviewWidth = size.width - foldoutSize.getWidth() - deleteSize.getWidth(); - this.textPreview.setAttribute('x', `${foldoutSize.getWidth()}`); + this.textPreview.setAttribute( + 'x', + `${(this.workspace.RTL ? -1 : 1) * foldoutSize.getWidth()}`, + ); this.textPreview.setAttribute( 'y', `${textPreviewMargin + textPreviewSize.height / 2}`, diff --git a/core/xml.ts b/core/xml.ts index cc26d8c8a..362a59ab2 100644 --- a/core/xml.ts +++ b/core/xml.ts @@ -68,7 +68,7 @@ export function saveWorkspaceComment( if (!skipId) elem.setAttribute('id', comment.id); const workspace = comment.workspace; - const loc = comment.getRelativeToSurfaceXY(); + const loc = comment.getRelativeToSurfaceXY().clone(); loc.x = workspace.RTL ? workspace.getWidth() - loc.x : loc.x; elem.setAttribute('x', `${loc.x}`); elem.setAttribute('y', `${loc.y}`); diff --git a/tests/mocha/clipboard_test.js b/tests/mocha/clipboard_test.js index d58f78b9b..5a513b44a 100644 --- a/tests/mocha/clipboard_test.js +++ b/tests/mocha/clipboard_test.js @@ -157,6 +157,34 @@ suite('Clipboard', function () { ); }); + test('pasted blocks are bumped to not overlap in RTL', function () { + this.workspace.dispose(); + this.workspace = Blockly.inject('blocklyDiv', {rtl: true}); + const block = Blockly.serialization.blocks.append( + { + 'type': 'controls_if', + 'x': 38, + 'y': 13, + }, + this.workspace, + ); + const data = block.toCopyData(); + + const newBlock = Blockly.clipboard.paste(data, this.workspace); + const oldBlockXY = block.getRelativeToSurfaceXY(); + assert.deepEqual( + newBlock.getRelativeToSurfaceXY(), + new Blockly.utils.Coordinate( + oldBlockXY.x - Blockly.config.snapRadius, + oldBlockXY.y + Blockly.config.snapRadius * 2, + ), + ); + + // Restore an LTR workspace. + this.workspace.dispose(); + this.workspace = Blockly.inject('blocklyDiv'); + }); + test('pasted blocks are bumped to be outside the connection snap radius', function () { Blockly.serialization.workspaces.load( { @@ -208,5 +236,28 @@ suite('Clipboard', function () { new Blockly.utils.Coordinate(40, 40), ); }); + + test('pasted comments are bumped to not overlap in RTL', function () { + this.workspace.dispose(); + this.workspace = Blockly.inject('blocklyDiv', {rtl: true}); + Blockly.Xml.domToWorkspace( + Blockly.utils.xml.textToDom( + '', + ), + this.workspace, + ); + const comment = this.workspace.getTopComments(false)[0]; + const data = comment.toCopyData(); + + const newComment = Blockly.clipboard.paste(data, this.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'); + }); }); }); From 9e1db9e332b6b2c609c22fff27cede7bb8b3acc7 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 19 Aug 2025 14:56:59 -0700 Subject: [PATCH 43/57] chore: Fix documentation generation warnings. (#9325) * chore: Replace @yields with @returns. * fix: Update the ESLint config to not require @yields. * chore: Move docs onto getters. --- core/block.ts | 2 +- core/field.ts | 3 --- core/field_input.ts | 6 ++---- eslint.config.mjs | 3 ++- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/core/block.ts b/core/block.ts index d15700a7e..af44facda 100644 --- a/core/block.ts +++ b/core/block.ts @@ -1126,7 +1126,7 @@ export class Block { /** * Returns a generator that provides every field on the block. * - * @yields A generator that can be used to iterate the fields on the block. + * @returns A generator that can be used to iterate the fields on the block. */ *getFields(): Generator { for (const input of this.inputList) { diff --git a/core/field.ts b/core/field.ts index fdcb2d693..3d12880a9 100644 --- a/core/field.ts +++ b/core/field.ts @@ -119,9 +119,6 @@ export abstract class Field return this.size; } - /** - * Sets the size of this field. - */ protected set size_(newValue: Size) { this.size = newValue; } diff --git a/core/field_input.ts b/core/field_input.ts index b68530918..97ad0e959 100644 --- a/core/field_input.ts +++ b/core/field_input.ts @@ -102,11 +102,9 @@ export abstract class FieldInput extends Field< */ override SERIALIZABLE = true; - /** - * Sets the size of this field. Although this appears to be a no-op, it must - * exist since the getter is overridden below. - */ protected override set size_(newValue: Size) { + // Although this appears to be a no-op, it must exist since the getter is + // overridden below. super.size_ = newValue; } diff --git a/eslint.config.mjs b/eslint.config.mjs index 0560586cb..744e02b45 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -89,7 +89,8 @@ function buildTSOverride({files, tsconfig}) { '@typescript-eslint/no-explicit-any': ['off'], // We use this pattern extensively for block (e.g. controls_if) interfaces. '@typescript-eslint/no-empty-object-type': ['off'], - + // TSDoc doesn't support @yields, so don't require that we use it. + 'jsdoc/require-yields': ['off'], // params and returns docs are optional. 'jsdoc/require-param-description': ['off'], 'jsdoc/require-returns': ['off'], From 4a0b710f7c2f7e5f9596e1476f0dc9ffe168344b Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 20 Aug 2025 11:26:45 -0700 Subject: [PATCH 44/57] fix: Show the delete cursor when dragging a block by an editable field. (#9326) --- core/css.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/css.ts b/core/css.ts index 30ee47fc5..503b6362b 100644 --- a/core/css.ts +++ b/core/css.ts @@ -181,7 +181,8 @@ let content = ` cursor: -webkit-grabbing; } -.blocklyDragging.blocklyDraggingDelete { +.blocklyDragging.blocklyDraggingDelete, +.blocklyDragging.blocklyDraggingDelete .blocklyField { cursor: url("<<>>/handdelete.cur"), auto; } From cf93f0721b244e206c3b5287a65dae4cc0fd1eda Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 21 Aug 2025 11:05:43 -0700 Subject: [PATCH 45/57] fix: Correct the alignment of narrow text in input fields. (#9327) * fix: Correct the alignment of narrow text in input fields. * chore: Clarify purpose of first argument to positionTextElement_(). --- core/field.ts | 3 +-- core/field_dropdown.ts | 7 +++---- core/field_input.ts | 26 ++++++++++++++++++++++++-- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/core/field.ts b/core/field.ts index 3d12880a9..d993e197d 100644 --- a/core/field.ts +++ b/core/field.ts @@ -849,8 +849,7 @@ export abstract class Field totalHeight = Math.max(totalHeight, constants!.FIELD_BORDER_RECT_HEIGHT); } - this.size_.height = totalHeight; - this.size_.width = totalWidth; + this.size_ = new Size(totalWidth, totalHeight); this.positionTextElement_(xOffset, contentWidth); this.positionBorderRect_(); diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index 8b01ccdda..3be5c94c3 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -29,6 +29,7 @@ import * as aria from './utils/aria.js'; import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; import * as parsing from './utils/parsing.js'; +import {Size} from './utils/size.js'; import * as utilsString from './utils/string.js'; import {Svg} from './utils/svg.js'; @@ -553,8 +554,7 @@ export class FieldDropdown extends Field { } else { arrowWidth = dom.getTextWidth(this.arrow as SVGTSpanElement); } - this.size_.width = imageWidth + arrowWidth + xPadding * 2; - this.size_.height = height; + this.size_ = new Size(imageWidth + arrowWidth + xPadding * 2, height); let arrowX = 0; if (block.RTL) { @@ -595,8 +595,7 @@ export class FieldDropdown extends Field { height / 2 - this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE / 2, ); } - this.size_.width = textWidth + arrowWidth + xPadding * 2; - this.size_.height = height; + this.size_ = new Size(textWidth + arrowWidth + xPadding * 2, height); this.positionTextElement_(xPadding, textWidth); } diff --git a/core/field_input.ts b/core/field_input.ts index 97ad0e959..55383a4c1 100644 --- a/core/field_input.ts +++ b/core/field_input.ts @@ -45,6 +45,11 @@ import type {WorkspaceSvg} from './workspace_svg.js'; */ type InputTypes = string | number; +/** + * The minimum width of an input field. + */ +const MINIMUM_WIDTH = 14; + /** * Abstract class for an editable input field. * @@ -113,8 +118,8 @@ export abstract class FieldInput extends Field< */ protected override get size_() { const s = super.size_; - if (s.width < 14) { - s.width = 14; + if (s.width < MINIMUM_WIDTH) { + s.width = MINIMUM_WIDTH; } return s; @@ -730,6 +735,23 @@ export abstract class FieldInput extends Field< return true; } + /** + * Position a field's text element after a size change. This handles both LTR + * and RTL positioning. + * + * @param xMargin x offset to use when positioning the text element. + * @param contentWidth The content width. + */ + protected override positionTextElement_( + xMargin: number, + contentWidth: number, + ) { + const effectiveWidth = xMargin * 2 + contentWidth; + const delta = + effectiveWidth < MINIMUM_WIDTH ? (MINIMUM_WIDTH - effectiveWidth) / 2 : 0; + super.positionTextElement_(xMargin + delta, contentWidth); + } + /** * Use the `getText_` developer hook to override the field's text * representation. When we're currently editing, return the current HTML value From 4891659d6e6ff8c82ec09dfc36ecf0d5b5cfe7be Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 21 Aug 2025 11:15:07 -0700 Subject: [PATCH 46/57] fix: Fix bug that caused inadvertent scrolling when the `WidgetDiv` was shown. (#9291) * fix: Fix bug that caused inadvertent scrolling when the `WidgetDiv` was shown. * chore: Add test to verify that displaying the context menu does not scroll the page. * chore: Clarify comments. * fix: Remove errant `.only`. * chore: Add test to verify that actively focusing a node does not scroll the page. * fix: Remove inadvertent `.only`. --- core/focus_manager.ts | 8 +++- core/interfaces/i_focusable_node.ts | 3 ++ tests/browser/test/basic_playground_test.mjs | 39 ++++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/core/focus_manager.ts b/core/focus_manager.ts index 02e059107..47e432454 100644 --- a/core/focus_manager.ts +++ b/core/focus_manager.ts @@ -309,6 +309,8 @@ export class FocusManager { * Note that this may update the specified node's element's tabindex to ensure * that it can be properly read out by screenreaders while focused. * + * The focused node will not be automatically scrolled into view. + * * @param focusableNode The node that should receive active focus. */ focusNode(focusableNode: IFocusableNode): void { @@ -423,6 +425,8 @@ export class FocusManager { * the returned lambda is called. Additionally, only 1 ephemeral focus context * can be active at any given time (attempting to activate more than one * simultaneously will result in an error being thrown). + * + * This method does not scroll the ephemerally focused element into view. */ takeEphemeralFocus( focusableElement: HTMLElement | SVGElement, @@ -439,7 +443,7 @@ export class FocusManager { if (this.focusedNode) { this.passivelyFocusNode(this.focusedNode, null); } - focusableElement.focus(); + focusableElement.focus({preventScroll: true}); let hasFinishedEphemeralFocus = false; return () => { @@ -574,7 +578,7 @@ export class FocusManager { } this.setNodeToVisualActiveFocus(node); - elem.focus(); + elem.focus({preventScroll: true}); } /** diff --git a/core/interfaces/i_focusable_node.ts b/core/interfaces/i_focusable_node.ts index 24833328d..57ec1a126 100644 --- a/core/interfaces/i_focusable_node.ts +++ b/core/interfaces/i_focusable_node.ts @@ -59,6 +59,9 @@ export interface IFocusableNode { * they should avoid the following: * - Creating or removing DOM elements (including via the renderer or drawer). * - Affecting focus via DOM focus() calls or the FocusManager. + * + * Implementations may consider scrolling themselves into view here; that is + * not handled by the focus manager. */ onNodeFocus(): void; diff --git a/tests/browser/test/basic_playground_test.mjs b/tests/browser/test/basic_playground_test.mjs index 4c54523bd..c7c8a5a37 100644 --- a/tests/browser/test/basic_playground_test.mjs +++ b/tests/browser/test/basic_playground_test.mjs @@ -101,6 +101,45 @@ suite('Right Clicking on Blocks', function () { await contextMenuSelect(this.browser, this.block, 'Remove Comment'); chai.assert.isNull(await getCommentText(this.browser, this.block.id)); }); + + test('does not scroll the page when node is ephemerally focused', async function () { + const initialScroll = await this.browser.execute(() => { + return window.scrollY; + }); + // This left-right-left sequence was necessary to reproduce unintended + // scrolling; regardless of the number of clicks/context menu activations, + // the page should not scroll. + this.block.click({button: 2}); + this.block.click({button: 0}); + this.block.click({button: 2}); + await this.browser.pause(250); + const finalScroll = await this.browser.execute(() => { + return window.scrollY; + }); + + chai.assert.equal(initialScroll, finalScroll); + }); + + test('does not scroll the page when node is actively focused', async function () { + await this.browser.setWindowSize(500, 300); + await this.browser.setViewport({width: 500, height: 300}); + const initialScroll = await this.browser.execute((blockId) => { + window.scrollTo(0, document.body.scrollHeight); + return window.scrollY; + }, this.block.id); + await this.browser.execute(() => { + Blockly.getFocusManager().focusNode( + Blockly.getMainWorkspace().getToolbox(), + ); + }); + const finalScroll = await this.browser.execute(() => { + return window.scrollY; + }); + + chai.assert.equal(initialScroll, finalScroll); + await this.browser.setWindowSize(800, 600); + await this.browser.setViewport({width: 800, height: 600}); + }); }); suite('Disabling', function () { From c32f6db02287e0140350f7693fc15888ffae656e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Aug 2025 12:06:10 -0700 Subject: [PATCH 47/57] chore(deps): bump eslint-plugin-prettier from 5.5.1 to 5.5.4 (#9319) Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 5.5.1 to 5.5.4. - [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases) - [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.5.1...v5.5.4) --- updated-dependencies: - dependency-name: eslint-plugin-prettier dependency-version: 5.5.4 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d30a69df6..285fbba58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4164,10 +4164,11 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz", - "integrity": "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", "dev": true, + "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.11.7" From be5f5a2a0a3d8271c15eaf6ece4c73023a184d88 Mon Sep 17 00:00:00 2001 From: Ennis Nian Date: Fri, 22 Aug 2025 04:59:09 +0800 Subject: [PATCH 48/57] fix: pointercancel event is not handled (#9250) --- core/touch.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/core/touch.ts b/core/touch.ts index 8fb2cd229..9af3b1f94 100644 --- a/core/touch.ts +++ b/core/touch.ts @@ -46,6 +46,7 @@ export const TOUCH_MAP: {[key: string]: string[]} = { 'mouseup': ['pointerup', 'pointercancel'], 'touchend': ['pointerup'], 'touchcancel': ['pointercancel'], + 'pointerup': ['pointerup', 'pointercancel'], }; /** PID of queued long-press task. */ From e358f4e7eeeffe93b995d6d5df0d83523a5a388d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 20:01:03 +0100 Subject: [PATCH 49/57] chore(deps): bump eslint-config-prettier from 10.1.5 to 10.1.8 (#9321) Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 10.1.5 to 10.1.8. - [Release notes](https://github.com/prettier/eslint-config-prettier/releases) - [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/eslint-config-prettier/compare/v10.1.5...v10.1.8) --- updated-dependencies: - dependency-name: eslint-config-prettier dependency-version: 10.1.8 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 285fbba58..0f8f34104 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4076,10 +4076,11 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.1.5", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", - "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, + "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, From cb698928251738e2c912b197479813c535c2f57f Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 22 Aug 2025 14:55:07 -0700 Subject: [PATCH 50/57] fix: Allow reregistering fields. (#9290) --- core/field_registry.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/field_registry.ts b/core/field_registry.ts index 06bb9acd0..e02ece75c 100644 --- a/core/field_registry.ts +++ b/core/field_registry.ts @@ -56,11 +56,11 @@ export interface RegistrableField { * @param type The field type name as used in the JSON definition. * @param fieldClass The field class containing a fromJson function that can * construct an instance of the field. - * @throws {Error} if the type name is empty, the field is already registered, - * or the fieldClass is not an object containing a fromJson function. + * @throws {Error} if the type name is empty or the fieldClass is not an object + * containing a fromJson function. */ export function register(type: string, fieldClass: RegistrableField) { - registry.register(registry.Type.FIELD, type, fieldClass); + registry.register(registry.Type.FIELD, type, fieldClass, true); } /** From aeb3e5e1435e0fc5769cea62170de35c2b8e84a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 09:08:01 -0700 Subject: [PATCH 51/57] chore(deps): bump chai from 5.2.1 to 6.0.1 (#9330) * chore(deps): bump chai from 5.2.1 to 6.0.1 Bumps [chai](https://github.com/chaijs/chai) from 5.2.1 to 6.0.1. - [Release notes](https://github.com/chaijs/chai/releases) - [Changelog](https://github.com/chaijs/chai/blob/main/History.md) - [Commits](https://github.com/chaijs/chai/compare/v5.2.1...v6.0.1) --- updated-dependencies: - dependency-name: chai dependency-version: 6.0.1 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * fix: Fix Chai import path. --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Aaron Dodson --- package-lock.json | 60 ++----------------- package.json | 2 +- tests/mocha/block_json_test.js | 2 +- tests/mocha/block_test.js | 2 +- tests/mocha/blocks/lists_test.js | 2 +- tests/mocha/blocks/logic_ternary_test.js | 2 +- tests/mocha/blocks/loops_test.js | 2 +- tests/mocha/blocks/procedures_test.js | 2 +- tests/mocha/blocks/variables_test.js | 2 +- tests/mocha/clipboard_test.js | 2 +- tests/mocha/comment_deserialization_test.js | 2 +- tests/mocha/comment_test.js | 2 +- tests/mocha/comment_view_test.js | 2 +- tests/mocha/connection_checker_test.js | 2 +- tests/mocha/connection_db_test.js | 2 +- tests/mocha/connection_test.js | 2 +- tests/mocha/contextmenu_items_test.js | 2 +- tests/mocha/contextmenu_test.js | 2 +- tests/mocha/cursor_test.js | 2 +- tests/mocha/dialog_test.js | 2 +- tests/mocha/dropdowndiv_test.js | 2 +- tests/mocha/event_block_change_test.js | 2 +- tests/mocha/event_block_create_test.js | 2 +- tests/mocha/event_block_delete_test.js | 2 +- tests/mocha/event_block_drag_test.js | 2 +- ...nt_block_field_intermediate_change_test.js | 2 +- tests/mocha/event_block_move_test.js | 2 +- tests/mocha/event_bubble_open_test.js | 2 +- tests/mocha/event_click_test.js | 2 +- tests/mocha/event_comment_change_test.js | 2 +- tests/mocha/event_comment_collapse_test.js | 2 +- tests/mocha/event_comment_create_test.js | 2 +- tests/mocha/event_comment_delete_test.js | 2 +- tests/mocha/event_comment_drag_test.js | 2 +- tests/mocha/event_comment_move_test.js | 2 +- tests/mocha/event_comment_resize_test.js | 2 +- tests/mocha/event_selected_test.js | 2 +- tests/mocha/event_test.js | 2 +- tests/mocha/event_theme_change_test.js | 2 +- tests/mocha/event_toolbox_item_select_test.js | 2 +- tests/mocha/event_trashcan_open_test.js | 2 +- tests/mocha/event_var_create_test.js | 2 +- tests/mocha/event_var_delete_test.js | 2 +- tests/mocha/event_var_rename_test.js | 2 +- tests/mocha/event_var_type_change_test.js | 2 +- tests/mocha/event_viewport_test.js | 2 +- tests/mocha/extensions_test.js | 2 +- tests/mocha/field_checkbox_test.js | 2 +- tests/mocha/field_colour_test.js | 2 +- tests/mocha/field_dropdown_test.js | 2 +- tests/mocha/field_image_test.js | 2 +- tests/mocha/field_label_serializable_test.js | 2 +- tests/mocha/field_label_test.js | 2 +- tests/mocha/field_number_test.js | 2 +- tests/mocha/field_registry_test.js | 2 +- tests/mocha/field_test.js | 2 +- tests/mocha/field_textinput_test.js | 2 +- tests/mocha/field_variable_test.js | 2 +- tests/mocha/flyout_test.js | 2 +- tests/mocha/focus_manager_test.js | 2 +- tests/mocha/focusable_tree_traverser_test.js | 2 +- tests/mocha/generator_test.js | 2 +- tests/mocha/gesture_test.js | 2 +- tests/mocha/icon_test.js | 2 +- tests/mocha/input_test.js | 2 +- tests/mocha/insertion_marker_test.js | 2 +- tests/mocha/jso_deserialization_test.js | 2 +- tests/mocha/jso_serialization_test.js | 2 +- tests/mocha/json_test.js | 2 +- .../keyboard_navigation_controller_test.js | 2 +- tests/mocha/layering_test.js | 2 +- tests/mocha/metrics_test.js | 2 +- tests/mocha/mutator_test.js | 2 +- tests/mocha/names_test.js | 2 +- tests/mocha/navigation_test.js | 2 +- tests/mocha/old_workspace_comment_test.js | 2 +- tests/mocha/procedure_map_test.js | 2 +- tests/mocha/rect_test.js | 2 +- tests/mocha/registry_test.js | 2 +- tests/mocha/render_management_test.js | 2 +- tests/mocha/serializer_test.js | 2 +- tests/mocha/shortcut_items_test.js | 2 +- tests/mocha/shortcut_registry_test.js | 2 +- tests/mocha/test_helpers/code_generation.js | 2 +- tests/mocha/test_helpers/events.js | 2 +- tests/mocha/test_helpers/fields.js | 2 +- tests/mocha/test_helpers/procedures.js | 2 +- tests/mocha/test_helpers/serialization.js | 2 +- tests/mocha/test_helpers/variables.js | 2 +- tests/mocha/test_helpers/warnings.js | 2 +- tests/mocha/test_helpers/workspace.js | 2 +- tests/mocha/theme_test.js | 2 +- tests/mocha/toast_test.js | 2 +- tests/mocha/toolbox_test.js | 2 +- tests/mocha/tooltip_test.js | 2 +- tests/mocha/touch_test.js | 2 +- tests/mocha/trashcan_test.js | 2 +- tests/mocha/utils_test.js | 2 +- tests/mocha/variable_map_test.js | 2 +- tests/mocha/variable_model_test.js | 2 +- tests/mocha/widget_div_test.js | 2 +- tests/mocha/workspace_comment_test.js | 2 +- tests/mocha/workspace_svg_test.js | 2 +- tests/mocha/xml_test.js | 2 +- tests/mocha/zoom_controls_test.js | 2 +- 105 files changed, 108 insertions(+), 160 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0f8f34104..f7a8f9426 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@microsoft/api-extractor": "^7.29.5", "ajv": "^8.17.1", "async-done": "^2.0.0", - "chai": "^5.1.1", + "chai": "^6.0.1", "concurrently": "^9.0.1", "eslint": "^9.15.0", "eslint-config-google": "^0.14.0", @@ -2489,15 +2489,6 @@ "node": ">=0.10.0" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, "node_modules/assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -2882,18 +2873,11 @@ } }, "node_modules/chai": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", - "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.0.1.tgz", + "integrity": "sha512-/JOoU2//6p5vCXh00FpNgtlw0LjvhGttaWc+y7wpW9yjBm3ys0dI8tSKZxIOgNruz5J0RleccatSIC3uxEZP0g==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } @@ -2914,15 +2898,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", - "dev": true, - "engines": { - "node": ">= 16" - } - }, "node_modules/cheerio": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", @@ -3586,15 +3561,6 @@ "node": ">=0.10" } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -6794,15 +6760,6 @@ "dev": true, "license": "MIT" }, - "node_modules/loupe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", - "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.1" - } - }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -7731,15 +7688,6 @@ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", "dev": true }, - "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", - "dev": true, - "engines": { - "node": ">= 14.16" - } - }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", diff --git a/package.json b/package.json index 6cfd025c7..c52a79003 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "@microsoft/api-extractor": "^7.29.5", "ajv": "^8.17.1", "async-done": "^2.0.0", - "chai": "^5.1.1", + "chai": "^6.0.1", "concurrently": "^9.0.1", "eslint": "^9.15.0", "eslint-config-google": "^0.14.0", diff --git a/tests/mocha/block_json_test.js b/tests/mocha/block_json_test.js index 4baccef6b..31abd6e34 100644 --- a/tests/mocha/block_json_test.js +++ b/tests/mocha/block_json_test.js @@ -5,7 +5,7 @@ */ import {Align} from '../../build/src/core/inputs/align.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/block_test.js b/tests/mocha/block_test.js index e3bd47090..1f8f9b1ee 100644 --- a/tests/mocha/block_test.js +++ b/tests/mocha/block_test.js @@ -11,7 +11,7 @@ import {IconType} from '../../build/src/core/icons/icon_types.js'; import {EndRowInput} from '../../build/src/core/inputs/end_row_input.js'; import {isCommentIcon} from '../../build/src/core/interfaces/i_comment_icon.js'; import {Size} from '../../build/src/core/utils/size.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {createRenderedBlock} from './test_helpers/block_definitions.js'; import { createChangeListenerSpy, diff --git a/tests/mocha/blocks/lists_test.js b/tests/mocha/blocks/lists_test.js index 490109d22..e749fae90 100644 --- a/tests/mocha/blocks/lists_test.js +++ b/tests/mocha/blocks/lists_test.js @@ -5,7 +5,7 @@ */ import {ConnectionType} from '../../../build/src/core/connection_type.js'; -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; import {defineStatementBlock} from '../test_helpers/block_definitions.js'; import {runSerializationTestSuite} from '../test_helpers/serialization.js'; import { diff --git a/tests/mocha/blocks/logic_ternary_test.js b/tests/mocha/blocks/logic_ternary_test.js index 719209359..3d343a7ca 100644 --- a/tests/mocha/blocks/logic_ternary_test.js +++ b/tests/mocha/blocks/logic_ternary_test.js @@ -5,7 +5,7 @@ */ import * as eventUtils from '../../../build/src/core/events/utils.js'; -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; import {runSerializationTestSuite} from '../test_helpers/serialization.js'; import { sharedTestSetup, diff --git a/tests/mocha/blocks/loops_test.js b/tests/mocha/blocks/loops_test.js index f8d74916c..eb040c884 100644 --- a/tests/mocha/blocks/loops_test.js +++ b/tests/mocha/blocks/loops_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../../build/src/core/blockly.js'; -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/blocks/procedures_test.js b/tests/mocha/blocks/procedures_test.js index 4b20662cf..5ac651fe4 100644 --- a/tests/mocha/blocks/procedures_test.js +++ b/tests/mocha/blocks/procedures_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../../build/src/core/blockly.js'; -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; import {defineRowBlock} from '../test_helpers/block_definitions.js'; import { assertCallBlockStructure, diff --git a/tests/mocha/blocks/variables_test.js b/tests/mocha/blocks/variables_test.js index d12691dd4..a317fe11b 100644 --- a/tests/mocha/blocks/variables_test.js +++ b/tests/mocha/blocks/variables_test.js @@ -5,7 +5,7 @@ */ import {nameUsedWithConflictingParam} from '../../../build/src/core/variables.js'; -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; import { MockParameterModelWithVar, MockProcedureModel, diff --git a/tests/mocha/clipboard_test.js b/tests/mocha/clipboard_test.js index 5a513b44a..ff49c0e30 100644 --- a/tests/mocha/clipboard_test.js +++ b/tests/mocha/clipboard_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { assertEventFired, createChangeListenerSpy, diff --git a/tests/mocha/comment_deserialization_test.js b/tests/mocha/comment_deserialization_test.js index 54ee0b2ff..f834eb0f3 100644 --- a/tests/mocha/comment_deserialization_test.js +++ b/tests/mocha/comment_deserialization_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/comment_test.js b/tests/mocha/comment_test.js index 0ff1c239e..1f52df8fd 100644 --- a/tests/mocha/comment_test.js +++ b/tests/mocha/comment_test.js @@ -5,7 +5,7 @@ */ import {EventType} from '../../build/src/core/events/type.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {assertEventFired} from './test_helpers/events.js'; import { sharedTestSetup, diff --git a/tests/mocha/comment_view_test.js b/tests/mocha/comment_view_test.js index 57a247424..a60a7a973 100644 --- a/tests/mocha/comment_view_test.js +++ b/tests/mocha/comment_view_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/connection_checker_test.js b/tests/mocha/connection_checker_test.js index fee2966d7..bdbcb70a6 100644 --- a/tests/mocha/connection_checker_test.js +++ b/tests/mocha/connection_checker_test.js @@ -5,7 +5,7 @@ */ import {ConnectionType} from '../../build/src/core/connection_type.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/connection_db_test.js b/tests/mocha/connection_db_test.js index 04f685124..459c59e3a 100644 --- a/tests/mocha/connection_db_test.js +++ b/tests/mocha/connection_db_test.js @@ -6,7 +6,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 {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/connection_test.js b/tests/mocha/connection_test.js index cefea1784..b36f358ea 100644 --- a/tests/mocha/connection_test.js +++ b/tests/mocha/connection_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { defineRowBlock, defineStackBlock, diff --git a/tests/mocha/contextmenu_items_test.js b/tests/mocha/contextmenu_items_test.js index d9044ec7e..08ab5d526 100644 --- a/tests/mocha/contextmenu_items_test.js +++ b/tests/mocha/contextmenu_items_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/contextmenu_test.js b/tests/mocha/contextmenu_test.js index 65896112b..df5bf79dc 100644 --- a/tests/mocha/contextmenu_test.js +++ b/tests/mocha/contextmenu_test.js @@ -6,7 +6,7 @@ import {callbackFactory} from '../../build/src/core/contextmenu.js'; import * as xmlUtils from '../../build/src/core/utils/xml.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/cursor_test.js b/tests/mocha/cursor_test.js index 6f841ae09..02426ae26 100644 --- a/tests/mocha/cursor_test.js +++ b/tests/mocha/cursor_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {createRenderedBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/tests/mocha/dialog_test.js b/tests/mocha/dialog_test.js index f250ff0f8..7d4147d83 100644 --- a/tests/mocha/dialog_test.js +++ b/tests/mocha/dialog_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/dropdowndiv_test.js b/tests/mocha/dropdowndiv_test.js index fc792fbaf..495237f18 100644 --- a/tests/mocha/dropdowndiv_test.js +++ b/tests/mocha/dropdowndiv_test.js @@ -6,7 +6,7 @@ import {Rect} from '../../build/src/core/utils/rect.js'; import * as style from '../../build/src/core/utils/style.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_block_change_test.js b/tests/mocha/event_block_change_test.js index 7de0a23b6..9e1f9c310 100644 --- a/tests/mocha/event_block_change_test.js +++ b/tests/mocha/event_block_change_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineMutatorBlocks} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/tests/mocha/event_block_create_test.js b/tests/mocha/event_block_create_test.js index f59f9435e..1672b56bb 100644 --- a/tests/mocha/event_block_create_test.js +++ b/tests/mocha/event_block_create_test.js @@ -5,7 +5,7 @@ */ import {EventType} from '../../build/src/core/events/type.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import {assertEventFired} from './test_helpers/events.js'; import { diff --git a/tests/mocha/event_block_delete_test.js b/tests/mocha/event_block_delete_test.js index d74b6aa06..e2fb5b8ce 100644 --- a/tests/mocha/event_block_delete_test.js +++ b/tests/mocha/event_block_delete_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/tests/mocha/event_block_drag_test.js b/tests/mocha/event_block_drag_test.js index 9b0f2031a..cc71e3bf0 100644 --- a/tests/mocha/event_block_drag_test.js +++ b/tests/mocha/event_block_drag_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/tests/mocha/event_block_field_intermediate_change_test.js b/tests/mocha/event_block_field_intermediate_change_test.js index 0ff4e1bbf..d917dadcd 100644 --- a/tests/mocha/event_block_field_intermediate_change_test.js +++ b/tests/mocha/event_block_field_intermediate_change_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_block_move_test.js b/tests/mocha/event_block_move_test.js index b93457e14..6d1890eeb 100644 --- a/tests/mocha/event_block_move_test.js +++ b/tests/mocha/event_block_move_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/tests/mocha/event_bubble_open_test.js b/tests/mocha/event_bubble_open_test.js index 099a625f6..a445a6a78 100644 --- a/tests/mocha/event_bubble_open_test.js +++ b/tests/mocha/event_bubble_open_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineMutatorBlocks} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/tests/mocha/event_click_test.js b/tests/mocha/event_click_test.js index 6e1876948..5c4afbcad 100644 --- a/tests/mocha/event_click_test.js +++ b/tests/mocha/event_click_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/tests/mocha/event_comment_change_test.js b/tests/mocha/event_comment_change_test.js index ed5f4d9f6..edb539ef5 100644 --- a/tests/mocha/event_comment_change_test.js +++ b/tests/mocha/event_comment_change_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_comment_collapse_test.js b/tests/mocha/event_comment_collapse_test.js index e2d275307..5c3f61054 100644 --- a/tests/mocha/event_comment_collapse_test.js +++ b/tests/mocha/event_comment_collapse_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_comment_create_test.js b/tests/mocha/event_comment_create_test.js index df919541d..71ef8ed1b 100644 --- a/tests/mocha/event_comment_create_test.js +++ b/tests/mocha/event_comment_create_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_comment_delete_test.js b/tests/mocha/event_comment_delete_test.js index 2e2bb45c4..dd9f0dd22 100644 --- a/tests/mocha/event_comment_delete_test.js +++ b/tests/mocha/event_comment_delete_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_comment_drag_test.js b/tests/mocha/event_comment_drag_test.js index d214e0adb..f6685cc5b 100644 --- a/tests/mocha/event_comment_drag_test.js +++ b/tests/mocha/event_comment_drag_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_comment_move_test.js b/tests/mocha/event_comment_move_test.js index aae3fdfe6..b3acea990 100644 --- a/tests/mocha/event_comment_move_test.js +++ b/tests/mocha/event_comment_move_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_comment_resize_test.js b/tests/mocha/event_comment_resize_test.js index b74e1abb2..bed3e733a 100644 --- a/tests/mocha/event_comment_resize_test.js +++ b/tests/mocha/event_comment_resize_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_selected_test.js b/tests/mocha/event_selected_test.js index 1ce8306db..8731099ec 100644 --- a/tests/mocha/event_selected_test.js +++ b/tests/mocha/event_selected_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/tests/mocha/event_test.js b/tests/mocha/event_test.js index 7423f22f7..a5019c8a9 100644 --- a/tests/mocha/event_test.js +++ b/tests/mocha/event_test.js @@ -6,7 +6,7 @@ import * as Blockly from '../../build/src/core/blockly.js'; import * as eventUtils from '../../build/src/core/events/utils.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { assertEventEquals, assertNthCallEventArgEquals, diff --git a/tests/mocha/event_theme_change_test.js b/tests/mocha/event_theme_change_test.js index f20f745b6..396347c9e 100644 --- a/tests/mocha/event_theme_change_test.js +++ b/tests/mocha/event_theme_change_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_toolbox_item_select_test.js b/tests/mocha/event_toolbox_item_select_test.js index bf6a9a462..02484c35b 100644 --- a/tests/mocha/event_toolbox_item_select_test.js +++ b/tests/mocha/event_toolbox_item_select_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_trashcan_open_test.js b/tests/mocha/event_trashcan_open_test.js index 2c809f2df..47da09a07 100644 --- a/tests/mocha/event_trashcan_open_test.js +++ b/tests/mocha/event_trashcan_open_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_var_create_test.js b/tests/mocha/event_var_create_test.js index e374c4965..79af41281 100644 --- a/tests/mocha/event_var_create_test.js +++ b/tests/mocha/event_var_create_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_var_delete_test.js b/tests/mocha/event_var_delete_test.js index b06943d9a..93d9ef0ba 100644 --- a/tests/mocha/event_var_delete_test.js +++ b/tests/mocha/event_var_delete_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_var_rename_test.js b/tests/mocha/event_var_rename_test.js index 7fbd185ab..b6d77cb35 100644 --- a/tests/mocha/event_var_rename_test.js +++ b/tests/mocha/event_var_rename_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_var_type_change_test.js b/tests/mocha/event_var_type_change_test.js index d19b0421a..066c145a3 100644 --- a/tests/mocha/event_var_type_change_test.js +++ b/tests/mocha/event_var_type_change_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_viewport_test.js b/tests/mocha/event_viewport_test.js index edacc0da6..cd11079fa 100644 --- a/tests/mocha/event_viewport_test.js +++ b/tests/mocha/event_viewport_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/extensions_test.js b/tests/mocha/extensions_test.js index 66772cbea..8c41861d5 100644 --- a/tests/mocha/extensions_test.js +++ b/tests/mocha/extensions_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/field_checkbox_test.js b/tests/mocha/field_checkbox_test.js index 08190fed8..74357338a 100644 --- a/tests/mocha/field_checkbox_test.js +++ b/tests/mocha/field_checkbox_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import { assertFieldValue, diff --git a/tests/mocha/field_colour_test.js b/tests/mocha/field_colour_test.js index 262f978f2..975d5a01d 100644 --- a/tests/mocha/field_colour_test.js +++ b/tests/mocha/field_colour_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { createTestBlock, defineRowBlock, diff --git a/tests/mocha/field_dropdown_test.js b/tests/mocha/field_dropdown_test.js index e9bc15914..a1731e812 100644 --- a/tests/mocha/field_dropdown_test.js +++ b/tests/mocha/field_dropdown_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { createTestBlock, defineRowBlock, diff --git a/tests/mocha/field_image_test.js b/tests/mocha/field_image_test.js index a02b3f6b6..f0358703b 100644 --- a/tests/mocha/field_image_test.js +++ b/tests/mocha/field_image_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { assertFieldValue, runConstructorSuiteTests, diff --git a/tests/mocha/field_label_serializable_test.js b/tests/mocha/field_label_serializable_test.js index a83171341..443cc6d17 100644 --- a/tests/mocha/field_label_serializable_test.js +++ b/tests/mocha/field_label_serializable_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { createTestBlock, defineRowBlock, diff --git a/tests/mocha/field_label_test.js b/tests/mocha/field_label_test.js index cf5b49044..bae600aff 100644 --- a/tests/mocha/field_label_test.js +++ b/tests/mocha/field_label_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {createTestBlock} from './test_helpers/block_definitions.js'; import { assertFieldValue, diff --git a/tests/mocha/field_number_test.js b/tests/mocha/field_number_test.js index 768766bf0..3c12fed82 100644 --- a/tests/mocha/field_number_test.js +++ b/tests/mocha/field_number_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import {runTestCases} from './test_helpers/common.js'; import { diff --git a/tests/mocha/field_registry_test.js b/tests/mocha/field_registry_test.js index 26b33c16c..1f19477de 100644 --- a/tests/mocha/field_registry_test.js +++ b/tests/mocha/field_registry_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/field_test.js b/tests/mocha/field_test.js index 38f9662d6..422b04734 100644 --- a/tests/mocha/field_test.js +++ b/tests/mocha/field_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { addBlockTypeToCleanup, addMessageToCleanup, diff --git a/tests/mocha/field_textinput_test.js b/tests/mocha/field_textinput_test.js index 7dc105f72..7cafd00d9 100644 --- a/tests/mocha/field_textinput_test.js +++ b/tests/mocha/field_textinput_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { createTestBlock, defineRowBlock, diff --git a/tests/mocha/field_variable_test.js b/tests/mocha/field_variable_test.js index 2dc8d35a5..58a209775 100644 --- a/tests/mocha/field_variable_test.js +++ b/tests/mocha/field_variable_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { createTestBlock, defineRowBlock, diff --git a/tests/mocha/flyout_test.js b/tests/mocha/flyout_test.js index f6d3019df..998279a08 100644 --- a/tests/mocha/flyout_test.js +++ b/tests/mocha/flyout_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/focus_manager_test.js b/tests/mocha/focus_manager_test.js index 26dcb8dbe..490fa4301 100644 --- a/tests/mocha/focus_manager_test.js +++ b/tests/mocha/focus_manager_test.js @@ -8,7 +8,7 @@ import { FocusManager, getFocusManager, } from '../../build/src/core/focus_manager.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/focusable_tree_traverser_test.js b/tests/mocha/focusable_tree_traverser_test.js index 0f88e1106..a384dd4be 100644 --- a/tests/mocha/focusable_tree_traverser_test.js +++ b/tests/mocha/focusable_tree_traverser_test.js @@ -6,7 +6,7 @@ import {FocusManager} from '../../build/src/core/focus_manager.js'; import {FocusableTreeTraverser} from '../../build/src/core/utils/focusable_tree_traverser.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/generator_test.js b/tests/mocha/generator_test.js index 527448eac..3c377e7c1 100644 --- a/tests/mocha/generator_test.js +++ b/tests/mocha/generator_test.js @@ -10,7 +10,7 @@ import {JavascriptGenerator} from '../../build/src/generators/javascript/javascr import {LuaGenerator} from '../../build/src/generators/lua/lua_generator.js'; import {PhpGenerator} from '../../build/src/generators/php/php_generator.js'; import {PythonGenerator} from '../../build/src/generators/python/python_generator.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/gesture_test.js b/tests/mocha/gesture_test.js index 3f53b8894..af4c599fe 100644 --- a/tests/mocha/gesture_test.js +++ b/tests/mocha/gesture_test.js @@ -5,7 +5,7 @@ */ import {EventType} from '../../build/src/core/events/type.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineBasicBlockWithField} from './test_helpers/block_definitions.js'; import {assertEventFired, assertEventNotFired} from './test_helpers/events.js'; import { diff --git a/tests/mocha/icon_test.js b/tests/mocha/icon_test.js index 5855fcfc5..ba1b71160 100644 --- a/tests/mocha/icon_test.js +++ b/tests/mocha/icon_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineEmptyBlock} from './test_helpers/block_definitions.js'; import {MockIcon, MockSerializableIcon} from './test_helpers/icon_mocks.js'; import { diff --git a/tests/mocha/input_test.js b/tests/mocha/input_test.js index 0c2b0973e..dfa30858e 100644 --- a/tests/mocha/input_test.js +++ b/tests/mocha/input_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/insertion_marker_test.js b/tests/mocha/insertion_marker_test.js index 9ccf8a00b..f8215a847 100644 --- a/tests/mocha/insertion_marker_test.js +++ b/tests/mocha/insertion_marker_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/jso_deserialization_test.js b/tests/mocha/jso_deserialization_test.js index dfd3e62b7..f6b47d7de 100644 --- a/tests/mocha/jso_deserialization_test.js +++ b/tests/mocha/jso_deserialization_test.js @@ -5,7 +5,7 @@ */ import {EventType} from '../../build/src/core/events/type.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {assertEventFired} from './test_helpers/events.js'; import { MockParameterModel, diff --git a/tests/mocha/jso_serialization_test.js b/tests/mocha/jso_serialization_test.js index 7cf415e67..4a7d5e9e1 100644 --- a/tests/mocha/jso_serialization_test.js +++ b/tests/mocha/jso_serialization_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { defineRowBlock, defineStackBlock, diff --git a/tests/mocha/json_test.js b/tests/mocha/json_test.js index 471d2fb97..2e4b68df9 100644 --- a/tests/mocha/json_test.js +++ b/tests/mocha/json_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { addMessageToCleanup, sharedTestSetup, diff --git a/tests/mocha/keyboard_navigation_controller_test.js b/tests/mocha/keyboard_navigation_controller_test.js index c7abd863e..dd81e9e4b 100644 --- a/tests/mocha/keyboard_navigation_controller_test.js +++ b/tests/mocha/keyboard_navigation_controller_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/layering_test.js b/tests/mocha/layering_test.js index 1ef0ee697..b84f23252 100644 --- a/tests/mocha/layering_test.js +++ b/tests/mocha/layering_test.js @@ -3,7 +3,7 @@ * Copyright 2023 Google LLC * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/metrics_test.js b/tests/mocha/metrics_test.js index 860e80255..de9a5641a 100644 --- a/tests/mocha/metrics_test.js +++ b/tests/mocha/metrics_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/mutator_test.js b/tests/mocha/mutator_test.js index fb6d8caf0..72b17d0a4 100644 --- a/tests/mocha/mutator_test.js +++ b/tests/mocha/mutator_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { createRenderedBlock, defineMutatorBlocks, diff --git a/tests/mocha/names_test.js b/tests/mocha/names_test.js index 732e28cd5..e449a59fd 100644 --- a/tests/mocha/names_test.js +++ b/tests/mocha/names_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/navigation_test.js b/tests/mocha/navigation_test.js index 5bed2aaab..38dc88894 100644 --- a/tests/mocha/navigation_test.js +++ b/tests/mocha/navigation_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/old_workspace_comment_test.js b/tests/mocha/old_workspace_comment_test.js index 08a2523f5..5038d67ba 100644 --- a/tests/mocha/old_workspace_comment_test.js +++ b/tests/mocha/old_workspace_comment_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/procedure_map_test.js b/tests/mocha/procedure_map_test.js index eebd5a9f3..5e29c6ca0 100644 --- a/tests/mocha/procedure_map_test.js +++ b/tests/mocha/procedure_map_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {MockProcedureModel} from './test_helpers/procedures.js'; import { sharedTestSetup, diff --git a/tests/mocha/rect_test.js b/tests/mocha/rect_test.js index 37712dff3..652837eae 100644 --- a/tests/mocha/rect_test.js +++ b/tests/mocha/rect_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/registry_test.js b/tests/mocha/registry_test.js index 6bcb8b5b0..2d5e22543 100644 --- a/tests/mocha/registry_test.js +++ b/tests/mocha/registry_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/render_management_test.js b/tests/mocha/render_management_test.js index 4de063539..94fa48805 100644 --- a/tests/mocha/render_management_test.js +++ b/tests/mocha/render_management_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/serializer_test.js b/tests/mocha/serializer_test.js index a3a3761e9..efd1f308b 100644 --- a/tests/mocha/serializer_test.js +++ b/tests/mocha/serializer_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { TestCase, TestSuite, diff --git a/tests/mocha/shortcut_items_test.js b/tests/mocha/shortcut_items_test.js index d96ddbfea..dfbae3f09 100644 --- a/tests/mocha/shortcut_items_test.js +++ b/tests/mocha/shortcut_items_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineStackBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/tests/mocha/shortcut_registry_test.js b/tests/mocha/shortcut_registry_test.js index 5641e17c7..a06f01b9c 100644 --- a/tests/mocha/shortcut_registry_test.js +++ b/tests/mocha/shortcut_registry_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {createTestBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/tests/mocha/test_helpers/code_generation.js b/tests/mocha/test_helpers/code_generation.js index 95bd902cd..e61a45653 100644 --- a/tests/mocha/test_helpers/code_generation.js +++ b/tests/mocha/test_helpers/code_generation.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; import {runTestSuites} from './common.js'; /** diff --git a/tests/mocha/test_helpers/events.js b/tests/mocha/test_helpers/events.js index c074bdd77..3e0b1e95d 100644 --- a/tests/mocha/test_helpers/events.js +++ b/tests/mocha/test_helpers/events.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; /** * Creates spy for workspace fireChangeListener diff --git a/tests/mocha/test_helpers/fields.js b/tests/mocha/test_helpers/fields.js index e082abb4c..ab304a808 100644 --- a/tests/mocha/test_helpers/fields.js +++ b/tests/mocha/test_helpers/fields.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; import {runTestCases, TestCase} from './common.js'; /** diff --git a/tests/mocha/test_helpers/procedures.js b/tests/mocha/test_helpers/procedures.js index ecf8c13ad..16ef97335 100644 --- a/tests/mocha/test_helpers/procedures.js +++ b/tests/mocha/test_helpers/procedures.js @@ -6,7 +6,7 @@ import {ConnectionType} from '../../../build/src/core/connection_type.js'; import {VariableModel} from '../../../build/src/core/variable_model.js'; -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; /** * Asserts that the procedure definition or call block has the expected var diff --git a/tests/mocha/test_helpers/serialization.js b/tests/mocha/test_helpers/serialization.js index c99f508d4..c476eae3d 100644 --- a/tests/mocha/test_helpers/serialization.js +++ b/tests/mocha/test_helpers/serialization.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; import {runTestCases} from './common.js'; /** diff --git a/tests/mocha/test_helpers/variables.js b/tests/mocha/test_helpers/variables.js index 83f175f63..dd19b4904 100644 --- a/tests/mocha/test_helpers/variables.js +++ b/tests/mocha/test_helpers/variables.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; /** * Check if a variable with the given values exists. diff --git a/tests/mocha/test_helpers/warnings.js b/tests/mocha/test_helpers/warnings.js index 0e07f846c..d718a25c1 100644 --- a/tests/mocha/test_helpers/warnings.js +++ b/tests/mocha/test_helpers/warnings.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; /** * Captures the strings sent to console.warn() when calling a function. diff --git a/tests/mocha/test_helpers/workspace.js b/tests/mocha/test_helpers/workspace.js index 917ce6f62..b5f3cadab 100644 --- a/tests/mocha/test_helpers/workspace.js +++ b/tests/mocha/test_helpers/workspace.js @@ -5,7 +5,7 @@ */ import * as eventUtils from '../../../build/src/core/events/utils.js'; -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; import {workspaceTeardown} from './setup_teardown.js'; import {assertVariableValues} from './variables.js'; import {assertWarnings} from './warnings.js'; diff --git a/tests/mocha/theme_test.js b/tests/mocha/theme_test.js index 1f425dca6..f54641a34 100644 --- a/tests/mocha/theme_test.js +++ b/tests/mocha/theme_test.js @@ -5,7 +5,7 @@ */ import {EventType} from '../../build/src/core/events/type.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {assertEventFired} from './test_helpers/events.js'; import { sharedTestSetup, diff --git a/tests/mocha/toast_test.js b/tests/mocha/toast_test.js index 45e02ad5d..afb7f7f6c 100644 --- a/tests/mocha/toast_test.js +++ b/tests/mocha/toast_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/toolbox_test.js b/tests/mocha/toolbox_test.js index 4e92cd28f..480fdfdc6 100644 --- a/tests/mocha/toolbox_test.js +++ b/tests/mocha/toolbox_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineStackBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/tests/mocha/tooltip_test.js b/tests/mocha/tooltip_test.js index 1edc8ad6e..0695b9ebe 100644 --- a/tests/mocha/tooltip_test.js +++ b/tests/mocha/tooltip_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/touch_test.js b/tests/mocha/touch_test.js index 775665643..30a9fe727 100644 --- a/tests/mocha/touch_test.js +++ b/tests/mocha/touch_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/trashcan_test.js b/tests/mocha/trashcan_test.js index 5486326f1..d96e00f3a 100644 --- a/tests/mocha/trashcan_test.js +++ b/tests/mocha/trashcan_test.js @@ -6,7 +6,7 @@ import {EventType} from '../../build/src/core/events/type.js'; import * as eventUtils from '../../build/src/core/events/utils.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { defineBasicBlockWithField, defineMutatorBlocks, diff --git a/tests/mocha/utils_test.js b/tests/mocha/utils_test.js index b6228448d..accf164b7 100644 --- a/tests/mocha/utils_test.js +++ b/tests/mocha/utils_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/variable_map_test.js b/tests/mocha/variable_map_test.js index 2d6cee0b9..76dffbe9d 100644 --- a/tests/mocha/variable_map_test.js +++ b/tests/mocha/variable_map_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { assertEventFired, assertEventNotFired, diff --git a/tests/mocha/variable_model_test.js b/tests/mocha/variable_model_test.js index cd2a89db4..eee7ea9bf 100644 --- a/tests/mocha/variable_model_test.js +++ b/tests/mocha/variable_model_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/widget_div_test.js b/tests/mocha/widget_div_test.js index 61c942471..4ad31f96c 100644 --- a/tests/mocha/widget_div_test.js +++ b/tests/mocha/widget_div_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/workspace_comment_test.js b/tests/mocha/workspace_comment_test.js index 3ce276e85..bb87ad82a 100644 --- a/tests/mocha/workspace_comment_test.js +++ b/tests/mocha/workspace_comment_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { assertEventFired, createChangeListenerSpy, diff --git a/tests/mocha/workspace_svg_test.js b/tests/mocha/workspace_svg_test.js index 207cad45d..6193dda2d 100644 --- a/tests/mocha/workspace_svg_test.js +++ b/tests/mocha/workspace_svg_test.js @@ -5,7 +5,7 @@ */ import {EventType} from '../../build/src/core/events/type.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineStackBlock} from './test_helpers/block_definitions.js'; import { assertEventFired, diff --git a/tests/mocha/xml_test.js b/tests/mocha/xml_test.js index 218324197..cd1bce128 100644 --- a/tests/mocha/xml_test.js +++ b/tests/mocha/xml_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { addBlockTypeToCleanup, createGenUidStubWithReturns, diff --git a/tests/mocha/zoom_controls_test.js b/tests/mocha/zoom_controls_test.js index dedc36b75..d9bb0f91e 100644 --- a/tests/mocha/zoom_controls_test.js +++ b/tests/mocha/zoom_controls_test.js @@ -5,7 +5,7 @@ */ import {EventType} from '../../build/src/core/events/type.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {assertEventFired, assertEventNotFired} from './test_helpers/events.js'; import { sharedTestSetup, From 90580a8655f754b0c158be917b04925a1e83690d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 19:12:06 +0100 Subject: [PATCH 52/57] chore(deps): bump eslint from 9.30.0 to 9.34.0 (#9329) Bumps [eslint](https://github.com/eslint/eslint) from 9.30.0 to 9.34.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v9.30.0...v9.34.0) --- updated-dependencies: - dependency-name: eslint dependency-version: 9.34.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 60 +++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index f7a8f9426..847c0d795 100644 --- a/package-lock.json +++ b/package-lock.json @@ -445,19 +445,21 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -527,10 +529,11 @@ "license": "MIT" }, "node_modules/@eslint/js": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.0.tgz", - "integrity": "sha512-Wzw3wQwPvc9sHM+NjakWTcPx11mbZyiYHuwWa/QfZ7cIRX7WK54PSk7bdyXDaoaopUcMatv1zaQvOAAO8hCdww==", + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", + "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -548,30 +551,19 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", - "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@gulp-sourcemaps/identity-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", @@ -1528,7 +1520,8 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { "version": "20.16.3", @@ -3970,19 +3963,20 @@ } }, "node_modules/eslint": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.0.tgz", - "integrity": "sha512-iN/SiPxmQu6EVkf+m1qpBxzUhE12YqFLOSySuOyVLJLEF9nzTf+h/1AJYc1JWzCnktggeNrjvQGLngDzXirU6g==", + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", + "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.14.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.30.0", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.34.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", From f10454cb360321f3b9d64c2769e7bff4cc976756 Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Wed, 27 Aug 2025 19:18:20 +0100 Subject: [PATCH 53/57] chore: Add node.js v24 to CI build matrix (#9219) Node.js v24 has now been released, so add it to our build matrix. Node v18 is no longer in LTS but we want to continue to test on it until it is removed from package.json engines. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d7a4e786c..cdec53082 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: # TODO (#2114): re-enable osx build. # os: [ubuntu-latest, macos-latest] os: [ubuntu-latest] - node-version: [18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.x, 24.x] # See supported Node.js release schedule at # https://nodejs.org/en/about/releases/ From b0569c4a576e3918d8f8c646947331ba7bdb99ad Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 27 Aug 2025 12:28:06 -0700 Subject: [PATCH 54/57] fix: Prevent mocha tests failures when window does not have focus. (#9332) * chore: Add puppeteer-core as a dev dependency. * fix: Make mocha tests run in a fake-focused window. * fix: Make `test:mocha:interactive` use the same gulp codepath as `test`. --- gulpfile.mjs | 7 ++- package-lock.json | 89 +++++++++++++++++++++++++++----- package.json | 3 +- scripts/gulpfiles/test_tasks.mjs | 12 ++++- tests/mocha/webdriver.js | 18 ++++++- 5 files changed, 111 insertions(+), 18 deletions(-) diff --git a/gulpfile.mjs b/gulpfile.mjs index fd3de3bde..ad61bcb51 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -45,7 +45,11 @@ import { publishBeta, recompile, } from './scripts/gulpfiles/release_tasks.mjs'; -import {generators, test} from './scripts/gulpfiles/test_tasks.mjs'; +import { + generators, + interactiveMocha, + test, +} from './scripts/gulpfiles/test_tasks.mjs'; const clean = parallel(cleanBuildDir, cleanReleaseDir); @@ -80,6 +84,7 @@ export { clean, test, generators as testGenerators, + interactiveMocha, buildAdvancedCompilationTest, createRC as gitCreateRC, docs, diff --git a/package-lock.json b/package-lock.json index 847c0d795..08c105c69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "patch-package": "^8.0.0", "prettier": "^3.3.3", "prettier-plugin-organize-imports": "^4.0.0", + "puppeteer-core": "^24.17.0", "readline-sync": "^1.4.10", "rimraf": "^5.0.0", "typescript": "^5.3.3", @@ -1282,18 +1283,18 @@ } }, "node_modules/@puppeteer/browsers": { - "version": "2.10.4", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.4.tgz", - "integrity": "sha512-9DxbZx+XGMNdjBynIs4BRSz+M3iRDeB7qRcAr6UORFLphCIM2x3DXgOucvADiifcqCE4XePFUKcnaAMyGbrDlQ==", + "version": "2.10.7", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.7.tgz", + "integrity": "sha512-wHWLkQWBjHtajZeqCB74nsa/X70KheyOhySYBRmVQDJiNj0zjZR/naPCvdWjMhcG1LmjaMV/9WtTo5mpe8qWLw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.0", + "debug": "^4.4.1", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", - "semver": "^7.7.1", - "tar-fs": "^3.0.8", + "semver": "^7.7.2", + "tar-fs": "^3.1.0", "yargs": "^17.7.2" }, "bin": { @@ -2960,6 +2961,20 @@ "fsevents": "~2.3.2" } }, + "node_modules/chromium-bidi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-8.0.0.tgz", + "integrity": "sha512-d1VmE0FD7lxZQHzcDUCKZSNRtRwISXDsdg4HjdTR5+Ll5nQ/vzU12JeNmupD6VWffrPSlrnGhEWlLESKH3VO+g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, "node_modules/ci-info": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", @@ -3612,6 +3627,13 @@ "node": ">=0.10.0" } }, + "node_modules/devtools-protocol": { + "version": "0.0.1475386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1475386.tgz", + "integrity": "sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/diff": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", @@ -6895,6 +6917,13 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -7925,6 +7954,24 @@ "node": ">=6" } }, + "node_modules/puppeteer-core": { + "version": "24.17.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.17.0.tgz", + "integrity": "sha512-RYOBKFiF+3RdwIZTEacqNpD567gaFcBAOKTT7742FdB1icXudrPI7BlZbYTYWK2wgGQUXt9Zi1Yn+D5PmCs4CA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.10.7", + "chromium-bidi": "8.0.0", + "debug": "^4.4.1", + "devtools-protocol": "0.0.1475386", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -8993,9 +9040,9 @@ } }, "node_modules/tar-fs": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz", - "integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz", + "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==", "dev": true, "license": "MIT", "dependencies": { @@ -9212,6 +9259,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "dev": true, + "license": "MIT" + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -9773,9 +9827,10 @@ "dev": true }, "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -10029,6 +10084,16 @@ "dependencies": { "safe-buffer": "~5.2.0" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index c52a79003..9ba8ebe61 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "test": "gulp test", "test:browser": "cd tests/browser && npx mocha", "test:generators": "gulp testGenerators", - "test:mocha:interactive": "npm run build && concurrently -n tsc,server \"tsc --watch --preserveWatchOutput --outDir \"build/src\" --declarationDir \"build/declarations\"\" \"http-server ./ -o /tests/mocha/index.html -c-1\"", + "test:mocha:interactive": "npm run build && concurrently -n tsc,server \"tsc --watch --preserveWatchOutput --outDir \"build/src\" --declarationDir \"build/declarations\"\" \"gulp interactiveMocha\"", "test:compile:advanced": "gulp buildAdvancedCompilationTest --debug", "updateGithubPages": "npm ci && gulp gitUpdateGithubPages" }, @@ -138,6 +138,7 @@ "patch-package": "^8.0.0", "prettier": "^3.3.3", "prettier-plugin-organize-imports": "^4.0.0", + "puppeteer-core": "^24.17.0", "readline-sync": "^1.4.10", "rimraf": "^5.0.0", "typescript": "^5.3.3", diff --git a/scripts/gulpfiles/test_tasks.mjs b/scripts/gulpfiles/test_tasks.mjs index d4b73cdb3..37f988444 100644 --- a/scripts/gulpfiles/test_tasks.mjs +++ b/scripts/gulpfiles/test_tasks.mjs @@ -257,9 +257,9 @@ async function metadata() { * Run Mocha tests inside a browser. * @return {Promise} Asynchronous result. */ -async function mocha() { +async function mocha(exitOnCompletion = true) { return runTestTask('mocha', async () => { - const result = await runMochaTestsInBrowser().catch(e => { + const result = await runMochaTestsInBrowser(exitOnCompletion).catch(e => { throw e; }); if (result) { @@ -269,6 +269,14 @@ async function mocha() { }); } +/** + * Run Mocha tests inside a browser and keep the browser open upon completion. + * @return {Promise} Asynchronous result. + */ +export async function interactiveMocha() { + return mocha(false); +} + /** * Helper method for comparison file. * @param {string} file1 First target file. diff --git a/tests/mocha/webdriver.js b/tests/mocha/webdriver.js index 207917c5e..06e7a3e65 100644 --- a/tests/mocha/webdriver.js +++ b/tests/mocha/webdriver.js @@ -15,9 +15,12 @@ const {posixPath} = require('../../scripts/helpers'); * Runs the Mocha tests in this directory in Chrome. It uses webdriverio to * launch Chrome and load index.html. Outputs a summary of the test results * to the console. + * + * @param {boolean} exitOnCompletetion True if the browser should automatically + * quit after tests have finished running. * @return {number} 0 on success, 1 on failure. */ -async function runMochaTestsInBrowser() { +async function runMochaTestsInBrowser(exitOnCompletion = true) { const options = { capabilities: { browserName: 'chrome', @@ -45,6 +48,17 @@ async function runMochaTestsInBrowser() { console.log('Loading URL: ' + url); await browser.url(url); + // Toggle the devtools setting to emulate focus, so that the window will + // always act as if it has focus regardless of the state of the window manager + // or operating system. This improves the reliability of FocusManager-related + // tests. + const puppeteer = await browser.getPuppeteer(); + await browser.call(async () => { + const page = (await puppeteer.pages())[0]; + const session = await page.createCDPSession(); + await session.send('Emulation.setFocusEmulationEnabled', { enabled: true }); + }); + await browser.waitUntil(async() => { const elem = await browser.$('#failureCount'); const text = await elem.getAttribute('tests_failed'); @@ -74,7 +88,7 @@ async function runMochaTestsInBrowser() { if (parseInt(numOfFailure) !== 0) { return 1; } - await browser.deleteSession(); + if (exitOnCompletion) await browser.deleteSession(); return 0; } From e51efe4855f96554fd552b175e8ae10d15f912bf Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 28 Aug 2025 10:06:52 -0700 Subject: [PATCH 55/57] fix: Fix bug that could cause errant line when rendering. (#9333) --- core/renderers/common/drawer.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/renderers/common/drawer.ts b/core/renderers/common/drawer.ts index 7046406ad..c474bc8c3 100644 --- a/core/renderers/common/drawer.ts +++ b/core/renderers/common/drawer.ts @@ -122,9 +122,12 @@ export class Drawer { } else if (Types.isSpacer(elem)) { this.outlinePath_ += svgPaths.lineOnAxis('h', elem.width); } + // No branch for a square corner, because it's a no-op. } - // No branch for a square corner, because it's a no-op. - this.outlinePath_ += svgPaths.lineOnAxis('v', topRow.height); + this.outlinePath_ += svgPaths.lineOnAxis( + 'v', + topRow.height - topRow.ascenderHeight, + ); } /** From 5afc0d6692f1a2403dc9a6024920111f23e25487 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 28 Aug 2025 11:28:40 -0700 Subject: [PATCH 56/57] refactor: Make focusable elements responsible for scrolling themselves into bounds. (#9288) * refactor: Make focusable elements responsible for scrolling themselves into bounds. * chore: Add tests for scrolling focused elements into view. * fix: Removed inadvertent `.only`. * fix: Scroll parent block of connections into bounds on focus. --- core/block_svg.ts | 3 + core/bubbles/bubble.ts | 4 + core/comments/comment_bar_button.ts | 8 +- core/comments/comment_editor.ts | 13 +- core/comments/rendered_workspace_comment.ts | 1 + core/field.ts | 7 +- core/flyout_button.ts | 6 +- core/icons/icon.ts | 12 +- core/keyboard_nav/line_cursor.ts | 30 +-- core/rendered_connection.ts | 7 +- tests/browser/test/basic_playground_test.mjs | 221 +++++++++++++++++++ 11 files changed, 276 insertions(+), 36 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index c6065282a..b3fdeb2d6 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -1844,6 +1844,9 @@ export class BlockSvg /** See IFocusableNode.onNodeFocus. */ onNodeFocus(): void { this.select(); + this.workspace.scrollBoundsIntoView( + this.getBoundingRectangleWithoutChildren(), + ); } /** See IFocusableNode.onNodeBlur. */ diff --git a/core/bubbles/bubble.ts b/core/bubbles/bubble.ts index c42e60254..742d300ad 100644 --- a/core/bubbles/bubble.ts +++ b/core/bubbles/bubble.ts @@ -707,6 +707,10 @@ export abstract class Bubble implements IBubble, ISelectable, IFocusableNode { onNodeFocus(): void { this.select(); this.bringToFront(); + const xy = this.getRelativeToSurfaceXY(); + const size = this.getSize(); + const bounds = new Rect(xy.y, xy.y + size.height, xy.x, xy.x + size.width); + this.workspace.scrollBoundsIntoView(bounds); } /** See IFocusableNode.onNodeBlur. */ diff --git a/core/comments/comment_bar_button.ts b/core/comments/comment_bar_button.ts index 24a084ad2..be130b0e3 100644 --- a/core/comments/comment_bar_button.ts +++ b/core/comments/comment_bar_button.ts @@ -87,7 +87,13 @@ export abstract class CommentBarButton implements IFocusableNode { } /** Called when this button's focusable DOM element gains focus. */ - onNodeFocus() {} + onNodeFocus() { + const commentView = this.getCommentView(); + const xy = commentView.getRelativeToSurfaceXY(); + const size = commentView.getSize(); + const bounds = new Rect(xy.y, xy.y + size.height, xy.x, xy.x + size.width); + commentView.workspace.scrollBoundsIntoView(bounds); + } /** Called when this button's focusable DOM element loses focus. */ onNodeBlur() {} diff --git a/core/comments/comment_editor.ts b/core/comments/comment_editor.ts index ac1559c4b..b4c741ba1 100644 --- a/core/comments/comment_editor.ts +++ b/core/comments/comment_editor.ts @@ -10,8 +10,10 @@ import {IFocusableNode} from '../interfaces/i_focusable_node.js'; import {IFocusableTree} from '../interfaces/i_focusable_tree.js'; import * as touch from '../touch.js'; import * as dom from '../utils/dom.js'; +import {Rect} from '../utils/rect.js'; import {Size} from '../utils/size.js'; import {Svg} from '../utils/svg.js'; +import * as svgMath from '../utils/svg_math.js'; import {WorkspaceSvg} from '../workspace_svg.js'; /** @@ -188,7 +190,16 @@ export class CommentEditor implements IFocusableNode { getFocusableTree(): IFocusableTree { return this.workspace; } - onNodeFocus(): void {} + onNodeFocus(): void { + const bbox = Rect.from(this.foreignObject.getBoundingClientRect()); + this.workspace.scrollBoundsIntoView( + Rect.createFromPoint( + svgMath.screenToWsCoordinates(this.workspace, bbox.getOrigin()), + bbox.getWidth(), + bbox.getHeight(), + ), + ); + } onNodeBlur(): void {} canBeFocused(): boolean { if (this.id) return true; diff --git a/core/comments/rendered_workspace_comment.ts b/core/comments/rendered_workspace_comment.ts index 49c75e608..2903bff4b 100644 --- a/core/comments/rendered_workspace_comment.ts +++ b/core/comments/rendered_workspace_comment.ts @@ -347,6 +347,7 @@ export class RenderedWorkspaceComment this.select(); // Ensure that the comment is always at the top when focused. this.workspace.getLayerManager()?.append(this, layers.BLOCK); + this.workspace.scrollBoundsIntoView(this.getBoundingRectangle()); } /** See IFocusableNode.onNodeBlur. */ diff --git a/core/field.ts b/core/field.ts index d993e197d..e025efab7 100644 --- a/core/field.ts +++ b/core/field.ts @@ -1380,7 +1380,12 @@ export abstract class Field } /** See IFocusableNode.onNodeFocus. */ - onNodeFocus(): void {} + onNodeFocus(): void { + const block = this.getSourceBlock() as BlockSvg; + block.workspace.scrollBoundsIntoView( + block.getBoundingRectangleWithoutChildren(), + ); + } /** See IFocusableNode.onNodeBlur. */ onNodeBlur(): void {} diff --git a/core/flyout_button.ts b/core/flyout_button.ts index c9afb8b01..5a066a23b 100644 --- a/core/flyout_button.ts +++ b/core/flyout_button.ts @@ -398,7 +398,11 @@ export class FlyoutButton } /** See IFocusableNode.onNodeFocus. */ - onNodeFocus(): void {} + onNodeFocus(): void { + const xy = this.getPosition(); + const bounds = new Rect(xy.y, xy.y + this.height, xy.x, xy.x + this.width); + this.workspace.scrollBoundsIntoView(bounds); + } /** See IFocusableNode.onNodeBlur. */ onNodeBlur(): void {} diff --git a/core/icons/icon.ts b/core/icons/icon.ts index 8f8ff70fc..f5f766038 100644 --- a/core/icons/icon.ts +++ b/core/icons/icon.ts @@ -14,6 +14,7 @@ import * as tooltip from '../tooltip.js'; import {Coordinate} from '../utils/coordinate.js'; import * as dom from '../utils/dom.js'; import * as idGenerator from '../utils/idgenerator.js'; +import {Rect} from '../utils/rect.js'; import {Size} from '../utils/size.js'; import {Svg} from '../utils/svg.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; @@ -168,7 +169,16 @@ export abstract class Icon implements IIcon { } /** See IFocusableNode.onNodeFocus. */ - onNodeFocus(): void {} + onNodeFocus(): void { + const blockBounds = (this.sourceBlock as BlockSvg).getBoundingRectangle(); + const bounds = new Rect( + blockBounds.top + this.offsetInBlock.y, + blockBounds.top + this.offsetInBlock.y + this.getSize().height, + blockBounds.left + this.offsetInBlock.x, + blockBounds.left + this.offsetInBlock.x + this.getSize().width, + ); + (this.sourceBlock as BlockSvg).workspace.scrollBoundsIntoView(bounds); + } /** See IFocusableNode.onNodeBlur. */ onNodeBlur(): void {} diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index 13e5a729d..30770e47d 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -14,14 +14,11 @@ */ import {BlockSvg} from '../block_svg.js'; -import {CommentBarButton} from '../comments/comment_bar_button.js'; import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js'; -import {Field} from '../field.js'; import {getFocusManager} from '../focus_manager.js'; import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; import * as registry from '../registry.js'; -import {Rect} from '../utils/rect.js'; -import {WorkspaceSvg} from '../workspace_svg.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; import {Marker} from './marker.js'; /** @@ -392,31 +389,6 @@ export class LineCursor extends Marker { */ setCurNode(newNode: IFocusableNode) { getFocusManager().focusNode(newNode); - - // Try to scroll cursor into view. - if (newNode instanceof BlockSvg) { - newNode.workspace.scrollBoundsIntoView( - newNode.getBoundingRectangleWithoutChildren(), - ); - } else if (newNode instanceof Field) { - const block = newNode.getSourceBlock() as BlockSvg; - block.workspace.scrollBoundsIntoView( - block.getBoundingRectangleWithoutChildren(), - ); - } else if (newNode instanceof RenderedWorkspaceComment) { - newNode.workspace.scrollBoundsIntoView(newNode.getBoundingRectangle()); - } else if (newNode instanceof CommentBarButton) { - const commentView = newNode.getCommentView(); - const xy = commentView.getRelativeToSurfaceXY(); - const size = commentView.getSize(); - const bounds = new Rect( - xy.y, - xy.y + size.height, - xy.x, - xy.x + size.width, - ); - commentView.workspace.scrollBoundsIntoView(bounds); - } } /** diff --git a/core/rendered_connection.ts b/core/rendered_connection.ts index 84905eecc..4a53048bc 100644 --- a/core/rendered_connection.ts +++ b/core/rendered_connection.ts @@ -644,6 +644,9 @@ export class RenderedConnection /** See IFocusableNode.onNodeFocus. */ onNodeFocus(): void { this.highlight(); + this.getSourceBlock().workspace.scrollBoundsIntoView( + this.getSourceBlock().getBoundingRectangleWithoutChildren(), + ); } /** See IFocusableNode.onNodeBlur. */ @@ -656,12 +659,12 @@ export class RenderedConnection return true; } - private findHighlightSvg(): SVGElement | null { + private findHighlightSvg(): SVGPathElement | null { // This cast is valid as TypeScript's definition is wrong. See: // https://github.com/microsoft/TypeScript/issues/60996. return document.getElementById(this.id) as | unknown - | null as SVGElement | null; + | null as SVGPathElement | null; } } diff --git a/tests/browser/test/basic_playground_test.mjs b/tests/browser/test/basic_playground_test.mjs index c7c8a5a37..72b3894a6 100644 --- a/tests/browser/test/basic_playground_test.mjs +++ b/tests/browser/test/basic_playground_test.mjs @@ -238,3 +238,224 @@ suite('Disabling', function () { }, ); }); + +suite('Focused nodes are scrolled into bounds', function () { + // Setting timeout to unlimited as the webdriver takes a longer time to run + // than most mocha tests. + this.timeout(0); + + // Setup Selenium for all of the tests + suiteSetup(async function () { + this.browser = await testSetup(testFileLocations.PLAYGROUND); + await this.browser.execute(() => { + window.focusScrollTest = async (testcase) => { + const workspace = Blockly.getMainWorkspace(); + const metrics = workspace.getMetricsManager(); + const initialViewport = metrics.getViewMetrics(true); + const elementBounds = await testcase(workspace); + await Blockly.renderManagement.finishQueuedRenders(); + const scrolledViewport = metrics.getViewMetrics(true); + const workspaceBounds = new Blockly.utils.Rect( + scrolledViewport.top, + scrolledViewport.top + scrolledViewport.height, + scrolledViewport.left, + scrolledViewport.left + scrolledViewport.width, + ); + return { + changed: + JSON.stringify(initialViewport) !== + JSON.stringify(scrolledViewport), + intersects: elementBounds.intersects(workspaceBounds), + contains: workspaceBounds.contains( + elementBounds.getOrigin().x, + elementBounds.getOrigin().y, + ), + elementBounds, + workspaceBounds, + }; + }; + }); + }); + + setup(async function () { + await this.browser.execute(() => { + Blockly.serialization.blocks.append( + { + 'type': 'text', + 'x': -500, + 'y': -500, + }, + Blockly.getMainWorkspace(), + ); + Blockly.serialization.blocks.append( + { + 'type': 'controls_if', + 'x': 500, + 'y': 500, + }, + Blockly.getMainWorkspace(), + ); + Blockly.getMainWorkspace().zoomCenter(1); + }); + }); + + test('Focused blocks scroll into bounds', async function () { + const result = await this.browser.execute(async () => { + return await window.focusScrollTest(async (workspace) => { + const block = workspace.getTopBlocks()[0]; + Blockly.getFocusManager().focusNode(block); + return block.getBoundingRectangleWithoutChildren(); + }); + }); + chai.assert.isTrue(result.intersects); + chai.assert.isTrue(result.contains); + chai.assert.isTrue(result.changed); + }); + + test('Focused bubbles scroll into bounds', async function () { + const result = await this.browser.execute(async () => { + return await window.focusScrollTest(async (workspace) => { + const block = workspace.getTopBlocks()[0]; + block.setCommentText('hello world'); + const icon = block.getIcon(Blockly.icons.IconType.COMMENT); + icon.setBubbleVisible(true); + await Blockly.renderManagement.finishQueuedRenders(); + icon.setBubbleLocation(new Blockly.utils.Coordinate(-510, -510)); + Blockly.getFocusManager().focusNode(icon.getBubble()); + const xy = icon.getBubble().getRelativeToSurfaceXY(); + const size = icon.getBubble().getSize(); + return new Blockly.utils.Rect( + xy.y, + xy.y + size.height, + xy.x, + xy.x + size.width, + ); + }); + }); + + chai.assert.isTrue(result.intersects); + chai.assert.isTrue(result.contains); + chai.assert.isTrue(result.changed); + }); + + test('Comment bar buttons scroll into bounds', async function () { + const result = await this.browser.execute(async () => { + return await window.focusScrollTest(async (workspace) => { + const comment = new Blockly.comments.RenderedWorkspaceComment( + workspace, + ); + comment.moveTo(new Blockly.utils.Coordinate(-300, 500)); + const commentBarButton = comment.view.getCommentBarButtons()[0]; + Blockly.getFocusManager().focusNode(commentBarButton); + const xy = comment.view.getRelativeToSurfaceXY(); + const size = comment.view.getSize(); + // Comment bar buttons scroll their parent comment view into view. + return new Blockly.utils.Rect( + xy.y, + xy.y + size.height, + xy.x, + xy.x + size.width, + ); + }); + }); + + chai.assert.isTrue(result.intersects); + chai.assert.isTrue(result.contains); + chai.assert.isTrue(result.changed); + }); + + test('Comment editors scroll into bounds', async function () { + const result = await this.browser.execute(async () => { + return await window.focusScrollTest(async (workspace) => { + const comment = new Blockly.comments.RenderedWorkspaceComment( + workspace, + ); + comment.moveTo(new Blockly.utils.Coordinate(-300, 500)); + const commentEditor = comment.view.getEditorFocusableNode(); + Blockly.getFocusManager().focusNode(commentEditor); + // Comment editor bounds can't be calculated externally since they + // depend on private properties, but the comment view is a reasonable + // proxy. + const xy = comment.view.getRelativeToSurfaceXY(); + const size = comment.view.getSize(); + return new Blockly.utils.Rect( + xy.y, + xy.y + size.height, + xy.x, + xy.x + size.width, + ); + }); + }); + + chai.assert.isTrue(result.intersects); + chai.assert.isTrue(result.contains); + chai.assert.isTrue(result.changed); + }); + + test('Workspace comments scroll into bounds', async function () { + const result = await this.browser.execute(async () => { + return await window.focusScrollTest(async (workspace) => { + const comment = new Blockly.comments.RenderedWorkspaceComment( + workspace, + ); + comment.moveTo(new Blockly.utils.Coordinate(-500, 500)); + Blockly.getFocusManager().focusNode(comment); + return comment.getBoundingRectangle(); + }); + }); + + chai.assert.isTrue(result.intersects); + chai.assert.isTrue(result.contains); + chai.assert.isTrue(result.changed); + }); + + test('Icons scroll into bounds', async function () { + const result = await this.browser.execute(async () => { + return await window.focusScrollTest(async (workspace) => { + const block = workspace.getTopBlocks()[0]; + block.setWarningText('this is bad'); + const icon = block.getIcon(Blockly.icons.IconType.WARNING); + Blockly.getFocusManager().focusNode(icon); + // Icon bounds can't be calculated externally since they depend on + // protected properties, but the parent block is a reasonable proxy. + return block.getBoundingRectangleWithoutChildren(); + }); + }); + + chai.assert.isTrue(result.intersects); + chai.assert.isTrue(result.contains); + chai.assert.isTrue(result.changed); + }); + + test('Fields scroll into bounds', async function () { + const result = await this.browser.execute(async () => { + return await window.focusScrollTest(async (workspace) => { + const block = workspace.getTopBlocks()[0]; + const field = block.getField('TEXT'); + Blockly.getFocusManager().focusNode(field); + // Fields scroll their source block into view. + return block.getBoundingRectangleWithoutChildren(); + }); + }); + + chai.assert.isTrue(result.intersects); + chai.assert.isTrue(result.contains); + chai.assert.isTrue(result.changed); + }); + + test('Connections scroll into bounds', async function () { + const result = await this.browser.execute(async () => { + return await window.focusScrollTest(async (workspace) => { + const block = workspace.getBlocksByType('controls_if')[0]; + Blockly.getFocusManager().focusNode(block.nextConnection); + // Connection bounds can't be calculated externally since they depend on + // protected properties, but the parent block is a reasonable proxy. + return block.getBoundingRectangleWithoutChildren(); + }); + }); + + chai.assert.isTrue(result.intersects); + chai.assert.isTrue(result.contains); + chai.assert.isTrue(result.changed); + }); +}); From 5f21e9b15430c43dad8e139c7c09a0ff2537329d Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 28 Aug 2025 15:46:39 -0700 Subject: [PATCH 57/57] release: Update version number to 12.3.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 08c105c69..afd057ce6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "blockly", - "version": "12.3.0-beta.0", + "version": "12.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blockly", - "version": "12.3.0-beta.0", + "version": "12.3.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 9ba8ebe61..7b639a872 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockly", - "version": "12.3.0-beta.0", + "version": "12.3.0", "description": "Blockly is a library for building visual programming editors.", "keywords": [ "blockly"