diff --git a/.github/workflows/appengine_deploy.yml b/.github/workflows/appengine_deploy.yml index 1dd6d2ffa92..efc6fe9417d 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. @@ -36,13 +36,13 @@ jobs: needs: prepare steps: - name: Download prepared files - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: appengine_files 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: diff --git a/.github/workflows/browser_test.yml b/.github/workflows/browser_test.yml index 3675af7b042..c2ce9913635 100644 --- a/.github/workflows/browser_test.yml +++ b/.github/workflows/browser_test.yml @@ -5,13 +5,15 @@ name: Run browser manually on: workflow_dispatch: + schedule: + - cron: '0 6 * * 1' # Runs every Monday at 06:00 UTC permissions: contents: read jobs: build: - timeout-minutes: 10 + timeout-minutes: 120 runs-on: ${{ matrix.os }} strategy: @@ -24,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 c4ab688f8fd..cdec5308279 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,12 +18,12 @@ 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/ 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 753d31dda1e..3d7d3d5d444 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' diff --git a/.github/workflows/welcome_new_contributors.yml b/.github/workflows/welcome_new_contributors.yml index 37ca9ef89df..4c4860c25b0 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@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} pr-message: > diff --git a/core/block.ts b/core/block.ts index 9f7c11d4faf..af44facda5d 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!); } } @@ -1116,9 +1126,9 @@ 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 { + *getFields(): Generator { for (const input of this.inputList) { for (const field of input.fieldRow) { yield field; diff --git a/core/block_svg.ts b/core/block_svg.ts index 49b4a1ee6f6..b3fdeb2d6b6 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,8 @@ 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), }; @@ -1840,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 20e730abb18..742d300adf1 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; @@ -683,6 +707,10 @@ export abstract class Bubble implements IBubble, ISelectable { 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. */ @@ -694,4 +722,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 7479c06cfc5..0bad5fabce6 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/clipboard/block_paster.ts b/core/clipboard/block_paster.ts index 750cedca124..e782cc0b004 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/collapse_comment_bar_button.ts b/core/comments/collapse_comment_bar_button.ts index b0738d70705..304e2af8125 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 d78a7fd86a1..be130b0e335 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. */ @@ -93,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 69dadd884f5..b4c741ba1ad 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'; /** @@ -53,6 +55,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 +89,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, @@ -182,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/comment_view.ts b/core/comments/comment_view.ts index 936d746508f..b1cd628f8dd 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, { @@ -362,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}`, @@ -612,13 +621,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 0b7dcd0ea27..c61db9b9cd2 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/comments/rendered_workspace_comment.ts b/core/comments/rendered_workspace_comment.ts index 3457e611a7e..2903bff4bce 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(); - }, - ); } /** @@ -289,6 +280,7 @@ export class RenderedWorkspaceComment paster: WorkspaceCommentPaster.TYPE, commentState: commentSerialization.save(this, { addCoordinates: true, + saveIds: false, }), }; } @@ -355,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/contextmenu_items.ts b/core/contextmenu_items.ts index 774bfdde29b..001a3c58e25 100644 --- a/core/contextmenu_items.ts +++ b/core/contextmenu_items.ts @@ -23,8 +23,15 @@ 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) { + if (!block || !block.isSimpleReporter()) return false; + const firstField = block.getFields().next().value; + return firstField?.isFullBlockField(); +} + /** * Option to undo previous action. */ @@ -362,10 +369,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 +385,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(''); } @@ -626,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); @@ -641,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(); diff --git a/core/css.ts b/core/css.ts index 4f4a4daaf90..503b6362ba2 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; } @@ -241,7 +242,7 @@ let content = ` cursor: default; } -.blocklyIconGroup:not(:hover), +.blocklyIconGroup:not(:hover):not(:focus), .blocklyIconGroupReadonly { opacity: .6; } diff --git a/core/field.ts b/core/field.ts index fdcb2d693b9..e025efab709 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; } @@ -852,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_(); @@ -1384,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/field_dropdown.ts b/core/field_dropdown.ts index 9c0d7f29269..3be5c94c3e3 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); } @@ -699,25 +698,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 +797,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 +810,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}`, ); } diff --git a/core/field_input.ts b/core/field_input.ts index b685309183a..55383a4c1d2 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. * @@ -102,11 +107,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; } @@ -115,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; @@ -732,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 diff --git a/core/field_registry.ts b/core/field_registry.ts index 06bb9acd045..e02ece75c96 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); } /** diff --git a/core/flyout_button.ts b/core/flyout_button.ts index c9afb8b0159..5a066a23b11 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/focus_manager.ts b/core/focus_manager.ts index 02e0591070f..47e4324540d 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/icons/comment_icon.ts b/core/icons/comment_icon.ts index 959eb2500f5..8f5a82c0d15 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/icons/icon.ts b/core/icons/icon.ts index 8f8ff70fc32..f5f76603875 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/insertion_marker_previewer.ts b/core/insertion_marker_previewer.ts index 2343b9adc76..8b5b82468c5 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 diff --git a/core/interfaces/i_focusable_node.ts b/core/interfaces/i_focusable_node.ts index 24833328d7f..57ec1a126e1 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/core/interfaces/i_variable_map.ts b/core/interfaces/i_variable_map.ts index 6c21aa8e0cb..22b4eda9012 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; 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 00000000000..f2f1ab7e107 --- /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_bar_button_navigation_policy.ts b/core/keyboard_nav/comment_bar_button_navigation_policy.ts index f676f465582..6654d2d8fef 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/comment_editor_navigation_policy.ts b/core/keyboard_nav/comment_editor_navigation_policy.ts new file mode 100644 index 00000000000..456df8e97c8 --- /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 70631ce81af..112239d0655 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/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index a301c3b37e0..30770e47d2d 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -15,11 +15,10 @@ import {BlockSvg} from '../block_svg.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 {WorkspaceSvg} from '../workspace_svg.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; import {Marker} from './marker.js'; /** @@ -390,20 +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()); - } } /** diff --git a/core/navigator.ts b/core/navigator.ts index 2f095f6f962..9c7c22f5987 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/registry.ts b/core/registry.ts index 2b00b775dea..4980a559478 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/rendered_connection.ts b/core/rendered_connection.ts index 84905eeccc2..4a53048bc84 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/core/renderers/common/drawer.ts b/core/renderers/common/drawer.ts index 7046406adc7..c474bc8c339 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, + ); } /** diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index 4979fdfa40f..f34034d3399 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/touch.ts b/core/touch.ts index 8fb2cd2298c..9af3b1f9494 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. */ diff --git a/core/variable_map.ts b/core/variable_map.ts index 403893332e5..ba36dcea600 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; } @@ -245,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) { diff --git a/core/workspace.ts b/core/workspace.ts index 5f205193912..88745f4205c 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); } diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index d713f11cf43..af395b077e5 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'; @@ -1673,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()); @@ -1721,7 +1725,7 @@ export class WorkspaceSvg block.getHeightWidth().height + minBlockHeight; } - eventUtils.setGroup(false); + eventUtils.setGroup(existingGroup); this.setResizesEnabled(true); } @@ -2726,6 +2730,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; } @@ -2868,6 +2885,11 @@ export class WorkspaceSvg bubble.getFocusableElement().id === id ) { return bubble; + } else if ( + bubble instanceof TextInputBubble && + bubble.getEditor().getFocusableElement().id === id + ) { + return bubble.getEditor(); } } } @@ -2889,11 +2911,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/core/xml.ts b/core/xml.ts index cc26d8c8a2c..362a59ab287 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/eslint.config.mjs b/eslint.config.mjs index f018e525d87..744e02b45bf 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'; @@ -88,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'], @@ -200,6 +202,9 @@ export default [ }, { files: ['tests/**'], + plugins: { + mocha: mochaPlugin, + }, languageOptions: { globals: { 'Blockly': true, @@ -219,6 +224,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/gulpfile.mjs b/gulpfile.mjs index fd3de3bde8c..ad61bcb516d 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 21178897ded..afd057ce613 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,36 +1,38 @@ { "name": "blockly", - "version": "12.2.0", + "version": "12.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blockly", - "version": "12.2.0", + "version": "12.3.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "jsdom": "26.1.0" }, "devDependencies": { - "@blockly/block-test": "^7.0.1", - "@blockly/dev-tools": "^9.0.0", - "@blockly/theme-modern": "^6.0.3", + "@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", - "@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", + "chai": "^6.0.1", "concurrently": "^9.0.1", "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-mocha": "^11.1.0", "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", @@ -49,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", @@ -89,10 +92,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" }, @@ -101,13 +105,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", @@ -235,15 +239,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": { @@ -441,19 +446,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" }, @@ -485,6 +492,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,11 +522,19 @@ "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", - "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" }, @@ -520,30 +552,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", @@ -912,17 +933,17 @@ } }, "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.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 +951,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 +1073,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 +1200,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", @@ -1145,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": { @@ -1236,13 +1374,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", @@ -1390,7 +1521,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", @@ -1955,16 +2087,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 +2134,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", @@ -2373,15 +2483,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", @@ -2766,20 +2867,13 @@ } }, "node_modules/chai": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "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": ">=12" + "node": ">=18" } }, "node_modules/chalk": { @@ -2798,15 +2892,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", @@ -2876,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", @@ -2979,6 +3078,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", @@ -3114,10 +3223,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", @@ -3459,15 +3569,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", @@ -3526,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", @@ -3877,19 +3985,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", @@ -3949,10 +4058,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" }, @@ -3964,10 +4074,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", @@ -4010,11 +4121,37 @@ "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", - "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" @@ -4068,6 +4205,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 +4246,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 +4501,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 +4776,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", @@ -4949,13 +5162,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 +5181,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" @@ -5328,10 +5547,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" } @@ -6192,9 +6412,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" }, @@ -6480,6 +6700,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", @@ -6548,15 +6776,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", @@ -6698,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", @@ -7485,15 +7711,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", @@ -7737,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", @@ -8805,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": { @@ -9024,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", @@ -9201,6 +9443,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", @@ -9575,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" }, @@ -9752,6 +10005,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", @@ -9810,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 d464ae5f171..7b639a872ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockly", - "version": "12.2.0", + "version": "12.3.0", "description": "Blockly is a library for building visual programming editors.", "keywords": [ "blockly" @@ -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" }, @@ -100,24 +100,26 @@ }, "license": "Apache-2.0", "devDependencies": { - "@blockly/block-test": "^7.0.1", - "@blockly/dev-tools": "^9.0.0", - "@blockly/theme-modern": "^6.0.3", + "@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", - "@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", + "chai": "^6.0.1", "concurrently": "^9.0.1", "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-mocha": "^11.1.0", "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", @@ -136,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/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 de8e47c0419..3cc97035da6 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 63fdbe66536..51abd480f95 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'; diff --git a/scripts/gulpfiles/test_tasks.mjs b/scripts/gulpfiles/test_tasks.mjs index d4b73cdb3c1..37f9884440d 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/browser/test/basic_playground_test.mjs b/tests/browser/test/basic_playground_test.mjs index 4c54523bd7f..72b3894a6a3 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 () { @@ -199,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); + }); +}); diff --git a/tests/browser/test/clipboard_test.mjs b/tests/browser/test/clipboard_test.mjs new file mode 100644 index 00000000000..37dd359d3b9 --- /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 6cf4986fce5..0a8998c3efe 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, diff --git a/tests/browser/test/toolbox_drag_test.mjs b/tests/browser/test/toolbox_drag_test.mjs index 32c20140692..66b74d9e2c5 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(() => { @@ -174,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 () { diff --git a/tests/mocha/block_json_test.js b/tests/mocha/block_json_test.js index 4baccef6b7b..31abd6e3484 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 62c61ce004c..1f8f9b1ee41 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, @@ -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); diff --git a/tests/mocha/blocks/lists_test.js b/tests/mocha/blocks/lists_test.js index 490109d22ca..e749fae90a7 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 71920935981..3d343a7caec 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 f8d74916c29..eb040c884a7 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 4b20662cf93..5ac651fe4cd 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 d12691dd476..a317fe11b52 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 85cdd229777..ff49c0e303c 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, @@ -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( { @@ -132,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( { @@ -183,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'); + }); }); }); diff --git a/tests/mocha/comment_deserialization_test.js b/tests/mocha/comment_deserialization_test.js index 54ee0b2ff30..f834eb0f301 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 0ff1c239e30..1f52df8fd52 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 57a24742457..a60a7a973ff 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 fee2966d766..bdbcb70a6ec 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 04f685124ca..459c59e3ab4 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 cefea1784e7..b36f358eac3 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 d9044ec7e28..08ab5d5267b 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 65896112bb1..df5bf79dc35 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 6f841ae09c6..02426ae26b8 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 f250ff0f8aa..7d4147d83f8 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 fc792fbaf24..495237f18bc 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 7de0a23b607..9e1f9c3103e 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 f59f9435efd..1672b56bb98 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 d74b6aa062b..e2fb5b8ce88 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 9b0f2031ad0..cc71e3bf084 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 0ff4e1bbf3c..d917dadcdd5 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 b93457e14c1..6d1890eebeb 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 099a625f6e2..a445a6a7819 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 6e18769485b..5c4afbcadf9 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 ed5f4d9f6ae..edb539ef555 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 e2d27530708..5c3f61054a1 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 df919541d95..71ef8ed1b75 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 2e2bb45c491..dd9f0dd2286 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 d214e0adba1..f6685cc5bdf 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 aae3fdfe632..b3acea990a6 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 b74e1abb2bf..bed3e733a65 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 1ce8306db48..8731099ec96 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 7423f22f74b..a5019c8a978 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 f20f745b6a0..396347c9e13 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 bf6a9a46212..02484c35bc1 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 2c809f2dfad..47da09a075a 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 e374c496541..79af41281de 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 b06943d9a19..93d9ef0ba2d 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 7fbd185ab7b..b6d77cb35bd 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 d19b0421a33..066c145a3ef 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 edacc0da6cb..cd11079fa32 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 66772cbea4b..8c41861d5d8 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 08190fed823..74357338a5a 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 262f978f29d..975d5a01d4a 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 e9bc159146d..a1731e81281 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 a02b3f6b64a..f0358703bdf 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 a831713412c..443cc6d1753 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 cf5b4904493..bae600aff19 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 768766bf013..3c12fed820d 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 26b33c16c3d..1f19477dee1 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 38f9662d6d6..422b0473418 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 7dc105f72f0..7cafd00d948 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 2dc8d35a55c..58a20977521 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 f6d3019df55..998279a0874 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 26dcb8dbe68..490fa4301b8 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 0f88e1106f9..a384dd4be45 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 527448eacc9..3c377e7c1ab 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 3f53b8894b9..af4c599fea3 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 5855fcfc576..ba1b7116065 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 0c2b0973eaf..dfa30858e0e 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 9ccf8a00b0f..f8215a847eb 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 dfd3e62b7f2..f6b47d7de6a 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 7cf415e676a..4a7d5e9e1d3 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 471d2fb9711..2e4b68df9ec 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 c7abd863ec1..dd81e9e4b45 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 1ef0ee6973d..b84f23252b3 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 860e802550d..de9a5641ad7 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 fb6d8caf09b..72b17d0a4bb 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 732e28cd57a..e449a59fd6c 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 5bed2aaab8c..38dc88894b1 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 08a2523f50e..5038d67bad8 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 eebd5a9f326..5e29c6ca050 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 37712dff3a0..652837eaef6 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 6bcb8b5b077..2d5e22543ca 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 4de0635394b..94fa48805cf 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 a3a3761e9c6..efd1f308b01 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 d96ddbfeadc..dfbae3f0901 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 5641e17c7d1..a06f01b9c00 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 95bd902cd45..e61a45653b2 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 c074bdd77a4..3e0b1e95d8b 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 e082abb4ccc..ab304a808a7 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 ecf8c13adcc..16ef973350f 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 c99f508d4dc..c476eae3d6f 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 83f175f6327..dd19b4904c9 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 0e07f846c5a..d718a25c166 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 917ce6f629e..b5f3cadabd3 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 1f425dca6a3..f54641a348f 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 45e02ad5de8..afb7f7f6cb9 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 f32319c6779..480fdfdc6fc 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, @@ -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(); diff --git a/tests/mocha/tooltip_test.js b/tests/mocha/tooltip_test.js index 1edc8ad6e25..0695b9ebe03 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 775665643b7..30a9fe72724 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 5486326f1e0..d96e00f3a21 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 b6228448db7..accf164b79b 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 c02887ceaca..76dffbe9dc7 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, @@ -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, + ); + }); + }); }); }); diff --git a/tests/mocha/variable_model_test.js b/tests/mocha/variable_model_test.js index cd2a89db420..eee7ea9bf41 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/webdriver.js b/tests/mocha/webdriver.js index 207917c5e6b..06e7a3e6585 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; } diff --git a/tests/mocha/widget_div_test.js b/tests/mocha/widget_div_test.js index 61c94247110..4ad31f96c72 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 3ce276e8579..bb87ad82ac6 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 207cad45dc7..6193dda2db7 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 218324197bf..cd1bce128bb 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 dedc36b75b4..d9bb0f91e9b 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,