chore: merge develop into add-screen-reader

chore: merge develop into add-screen-reader
Merge pull request #9352 from google/develop
This commit is contained in:
Maribeth Moffatt
2025-09-09 09:37:54 -07:00
committed by GitHub
149 changed files with 1499 additions and 390 deletions
+3 -3
View File
@@ -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:
+4 -2
View File
@@ -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
+4 -4
View File
@@ -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
+2 -2
View File
@@ -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: 'add-screen-reader-support-experimental'
@@ -9,7 +9,7 @@ jobs:
permissions:
pull-requests: write
steps:
- uses: actions/first-interaction@v2
- uses: actions/first-interaction@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pr-message: >
+2
View File
@@ -70,6 +70,8 @@ handlers:
# Blockly files.
- url: /static
static_dir: static
http_headers:
Access-Control-Allow-Origin: "*"
secure: always
# Storage API.
+27 -17
View File
@@ -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,7 +1126,7 @@ export class Block {
/**
* Returns a generator that provides every field on the block.
*
* @yields A generator that can be used to iterate the fields on the block.
* @returns A generator that can be used to iterate the fields on the block.
*/
*getFields(): Generator<Field, undefined, void> {
for (const input of this.inputList) {
+3
View File
@@ -1924,6 +1924,9 @@ export class BlockSvg
/** See IFocusableNode.onNodeFocus. */
onNodeFocus(): void {
this.select();
this.workspace.scrollBoundsIntoView(
this.getBoundingRectangleWithoutChildren(),
);
}
/** See IFocusableNode.onNodeBlur. */
+4
View File
@@ -710,6 +710,10 @@ export abstract class Bubble implements IBubble, ISelectable, IFocusableNode {
onNodeFocus(): void {
this.select();
this.bringToFront();
const xy = this.getRelativeToSurfaceXY();
const size = this.getSize();
const bounds = new Rect(xy.y, xy.y + size.height, xy.x, xy.x + size.width);
this.workspace.scrollBoundsIntoView(bounds);
}
/** See IFocusableNode.onNodeBlur. */
+3
View File
@@ -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(
+5 -4
View File
@@ -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.
@@ -43,8 +44,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,
@@ -92,14 +94,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();
+12 -12
View File
@@ -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;
}
abstract initAria(): void;
@@ -95,7 +89,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() {}
+12 -1
View File
@@ -11,8 +11,10 @@ import {IFocusableTree} from '../interfaces/i_focusable_tree.js';
import * as touch from '../touch.js';
import * as aria from '../utils/aria.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';
/**
@@ -191,7 +193,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;
+12 -4
View File
@@ -103,7 +103,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',
@@ -180,12 +180,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,
{
@@ -366,7 +372,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}`,
@@ -616,13 +625,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;
}
+4 -2
View File
@@ -12,6 +12,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.
@@ -43,8 +44,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,
@@ -103,7 +105,7 @@ export class DeleteCommentBarButton extends CommentBarButton {
return;
}
this.getParentComment().dispose();
this.getCommentView().dispose();
e?.stopPropagation();
getFocusManager().focusNode(this.workspace);
}
@@ -347,6 +347,7 @@ export class RenderedWorkspaceComment
this.select();
// Ensure that the comment is always at the top when focused.
this.workspace.getLayerManager()?.append(this, layers.BLOCK);
this.workspace.scrollBoundsIntoView(this.getBoundingRectangle());
}
/** See IFocusableNode.onNodeBlur. */
+3 -36
View File
@@ -23,6 +23,7 @@ import {CommentIcon} from './icons/comment_icon.js';
import {Msg} from './msg.js';
import {StatementInput} from './renderers/zelos/zelos.js';
import {Coordinate} from './utils/coordinate.js';
import * as svgMath from './utils/svg_math.js';
import type {WorkspaceSvg} from './workspace_svg.js';
function isFullBlockField(block?: BlockSvg) {
@@ -637,9 +638,9 @@ export function registerCommentCreate() {
const comment = new RenderedWorkspaceComment(workspace);
comment.setPlaceholderText(Msg['WORKSPACE_COMMENT_DEFAULT_TEXT']);
comment.moveTo(
pixelsToWorkspaceCoords(
new Coordinate(location.x, location.y),
svgMath.screenToWsCoordinates(
workspace,
new Coordinate(location.x, location.y),
),
);
getFocusManager().focusNode(comment);
@@ -652,40 +653,6 @@ export function registerCommentCreate() {
ContextMenuRegistry.registry.register(createOption);
}
/**
* Converts pixel coordinates (relative to the window) to workspace coordinates.
*/
function pixelsToWorkspaceCoords(
pixelCoord: Coordinate,
workspace: WorkspaceSvg,
): Coordinate {
const injectionDiv = workspace.getInjectionDiv();
// Bounding rect coordinates are in client coordinates, meaning that they
// are in pixels relative to the upper left corner of the visible browser
// window. These coordinates change when you scroll the browser window.
const boundingRect = injectionDiv.getBoundingClientRect();
// The client coordinates offset by the injection div's upper left corner.
const clientOffsetPixels = new Coordinate(
pixelCoord.x - boundingRect.left,
pixelCoord.y - boundingRect.top,
);
// The offset in pixels between the main workspace's origin and the upper
// left corner of the injection div.
const mainOffsetPixels = workspace.getOriginOffsetInPixels();
// The position of the new comment in pixels relative to the origin of the
// main workspace.
const finalOffset = Coordinate.difference(
clientOffsetPixels,
mainOffsetPixels,
);
// The position of the new comment in main workspace coordinates.
finalOffset.scale(1 / workspace.scale);
return finalOffset;
}
/** Registers all block-scoped context menu items. */
function registerBlockOptions_() {
registerDuplicate();
+2 -1
View File
@@ -181,7 +181,8 @@ let content = `
cursor: -webkit-grabbing;
}
.blocklyDragging.blocklyDraggingDelete {
.blocklyDragging.blocklyDraggingDelete,
.blocklyDragging.blocklyDraggingDelete .blocklyField {
cursor: url("<<<PATH>>>/handdelete.cur"), auto;
}
+7 -6
View File
@@ -120,9 +120,6 @@ export abstract class Field<T = any>
return this.size;
}
/**
* Sets the size of this field.
*/
protected set size_(newValue: Size) {
this.size = newValue;
}
@@ -854,8 +851,7 @@ export abstract class Field<T = any>
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_();
@@ -1386,7 +1382,12 @@ export abstract class Field<T = any>
}
/** See IFocusableNode.onNodeFocus. */
onNodeFocus(): void {}
onNodeFocus(): void {
const block = this.getSourceBlock() as BlockSvg;
block.workspace.scrollBoundsIntoView(
block.getBoundingRectangleWithoutChildren(),
);
}
/** See IFocusableNode.onNodeBlur. */
onNodeBlur(): void {}
+3 -4
View File
@@ -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';
@@ -561,8 +562,7 @@ export class FieldDropdown extends Field<string> {
} 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) {
@@ -603,8 +603,7 @@ export class FieldDropdown extends Field<string> {
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);
}
+26 -6
View File
@@ -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<T extends InputTypes> 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<T extends InputTypes> 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;
@@ -740,6 +743,23 @@ export abstract class FieldInput<T extends InputTypes> 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
+3 -3
View File
@@ -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);
}
/**
+5 -1
View File
@@ -402,7 +402,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 {}
+6 -2
View File
@@ -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});
}
/**
+11 -1
View File
@@ -15,6 +15,7 @@ import * as aria from '../utils/aria.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';
@@ -172,7 +173,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 {}
+3
View File
@@ -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;
@@ -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];
+1 -20
View File
@@ -14,13 +14,11 @@
*/
import {BlockSvg} from '../block_svg.js';
import {CommentBarButton} from '../comments/comment_bar_button.js';
import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js';
import {Field} from '../field.js';
import {getFocusManager} from '../focus_manager.js';
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import * as registry from '../registry.js';
import {WorkspaceSvg} from '../workspace_svg.js';
import type {WorkspaceSvg} from '../workspace_svg.js';
import {Marker} from './marker.js';
/**
@@ -391,23 +389,6 @@ export class LineCursor extends Marker {
*/
setCurNode(newNode: IFocusableNode) {
getFocusManager().focusNode(newNode);
// Try to scroll cursor into view.
if (newNode instanceof BlockSvg) {
newNode.workspace.scrollBoundsIntoView(
newNode.getBoundingRectangleWithoutChildren(),
);
} else if (newNode instanceof Field) {
const block = newNode.getSourceBlock() as BlockSvg;
block.workspace.scrollBoundsIntoView(
block.getBoundingRectangleWithoutChildren(),
);
} else if (newNode instanceof RenderedWorkspaceComment) {
newNode.workspace.scrollBoundsIntoView(newNode.getBoundingRectangle());
} else if (newNode instanceof CommentBarButton) {
const comment = newNode.getParentComment();
comment.workspace.scrollBoundsIntoView(comment.getBoundingRectangle());
}
}
/**
+5 -2
View File
@@ -647,6 +647,9 @@ export class RenderedConnection
/** See IFocusableNode.onNodeFocus. */
onNodeFocus(): void {
this.highlight();
this.getSourceBlock().workspace.scrollBoundsIntoView(
this.getSourceBlock().getBoundingRectangleWithoutChildren(),
);
}
/** See IFocusableNode.onNodeBlur. */
@@ -659,12 +662,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;
}
}
+5 -2
View File
@@ -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,
);
}
/**
+1
View File
@@ -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. */
+5 -1
View File
@@ -112,7 +112,11 @@ export class VariableMap
const oldType = variable.getType();
if (oldType === newType) return variable;
this.variableMap.get(variable.getType())?.delete(variable.getId());
const oldTypeVariables = this.variableMap.get(oldType);
oldTypeVariables?.delete(variable.getId());
if (oldTypeVariables?.size === 0) {
this.variableMap.delete(oldType);
}
variable.setType(newType);
const newTypeVariables =
this.variableMap.get(newType) ??
+1 -1
View File
@@ -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}`);
+7 -1
View File
@@ -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',
},
},
{
+6 -1
View File
@@ -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,
+8 -7
View File
@@ -1,11 +1,12 @@
{
"MATH_HUE": "230",
"LOOPS_HUE": "120",
"#": "Automatically generated, do not edit this file!",
"COLOUR_HUE": "20",
"LISTS_HUE": "260",
"LOGIC_HUE": "210",
"VARIABLES_HUE": "330",
"TEXTS_HUE": "160",
"LOOPS_HUE": "120",
"MATH_HUE": "230",
"PROCEDURES_HUE": "290",
"COLOUR_HUE": "20",
"VARIABLES_DYNAMIC_HUE": "310"
}
"TEXTS_HUE": "160",
"VARIABLES_DYNAMIC_HUE": "310",
"VARIABLES_HUE": "330"
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"@metadata": {
"author": "Ellen Spertus <ellen.spertus@gmail.com>",
"lastupdated": "2025-06-17 15:36:41.845826",
"lastupdated": "2025-09-08 16:26:57.642330",
"locale": "en",
"messagedocumentation" : "qqq"
},
+2 -2
View File
@@ -1,4 +1,5 @@
{
"#": "Automatically generated, do not edit this file!",
"CONTROLS_FOREACH_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO",
"CONTROLS_FOR_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO",
"CONTROLS_IF_ELSEIF_TITLE_ELSEIF": "CONTROLS_IF_MSG_ELSEIF",
@@ -7,7 +8,6 @@
"CONTROLS_IF_MSG_THEN": "CONTROLS_REPEAT_INPUT_DO",
"CONTROLS_WHILEUNTIL_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO",
"LISTS_CREATE_WITH_ITEM_TITLE": "VARIABLES_DEFAULT_NAME",
"LISTS_GET_INDEX_HELPURL": "LISTS_INDEX_OF_HELPURL",
"LISTS_GET_INDEX_INPUT_IN_LIST": "LISTS_INLIST",
"LISTS_GET_SUBLIST_INPUT_IN_LIST": "LISTS_INLIST",
"LISTS_INDEX_OF_INPUT_IN_LIST": "LISTS_INLIST",
@@ -19,4 +19,4 @@
"PROCEDURES_DEFRETURN_TITLE": "PROCEDURES_DEFNORETURN_TITLE",
"TEXT_APPEND_VARIABLE": "VARIABLES_DEFAULT_NAME",
"TEXT_CREATE_JOIN_ITEM_TITLE_ITEM": "VARIABLES_DEFAULT_NAME"
}
}
+2 -2
View File
@@ -85,10 +85,10 @@ Blockly.Msg.REMOVE_COMMENT = 'Remove Comment';
/// context menu - Make a copy of the selected workspace comment.\n{{Identical|Duplicate}}
Blockly.Msg.DUPLICATE_COMMENT = 'Duplicate Comment';
/** @type {string} */
/// context menu - Change from 'external' to 'inline' mode for displaying blocks used as inputs to the selected block. See [[Translating:Blockly#context_menus]].
/// context menu - Change from 'external' to 'inline' mode for displaying blocks used as inputs to the selected block. See [[Translating:Blockly#context_menus]].\n\nThe opposite of {{msg-blockly|INLINE INPUTS}}.
Blockly.Msg.EXTERNAL_INPUTS = 'External Inputs';
/** @type {string} */
/// context menu - Change from 'internal' to 'external' mode for displaying blocks used as inputs to the selected block. See [[Translating:Blockly#context_menus]].
/// context menu - Change from 'internal' to 'external' mode for displaying blocks used as inputs to the selected block. See [[Translating:Blockly#context_menus]].\n\nThe opposite of {{msg-blockly|EXTERNAL INPUTS}}.
Blockly.Msg.INLINE_INPUTS = 'Inline Inputs';
/** @type {string} */
/// context menu - Permanently delete the selected block.
+160 -123
View File
@@ -1,20 +1,20 @@
{
"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/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",
@@ -22,12 +22,13 @@
"@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": "^52.0.2",
"eslint-plugin-mocha": "^11.1.0",
"eslint-plugin-prettier": "^5.2.1",
"glob": "^11.0.1",
"globals": "^16.0.0",
@@ -50,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",
@@ -90,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"
},
@@ -102,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",
@@ -443,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"
},
@@ -525,10 +530,11 @@
"license": "MIT"
},
"node_modules/@eslint/js": {
"version": "9.30.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.0.tgz",
"integrity": "sha512-Wzw3wQwPvc9sHM+NjakWTcPx11mbZyiYHuwWa/QfZ7cIRX7WK54PSk7bdyXDaoaopUcMatv1zaQvOAAO8hCdww==",
"version": "9.34.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz",
"integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -546,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",
@@ -1288,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": {
@@ -1526,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",
@@ -2487,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",
@@ -2880,18 +2867,11 @@
}
},
"node_modules/chai": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz",
"integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.0.1.tgz",
"integrity": "sha512-/JOoU2//6p5vCXh00FpNgtlw0LjvhGttaWc+y7wpW9yjBm3ys0dI8tSKZxIOgNruz5J0RleccatSIC3uxEZP0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"assertion-error": "^2.0.1",
"check-error": "^2.1.1",
"deep-eql": "^5.0.1",
"loupe": "^3.1.0",
"pathval": "^2.0.0"
},
"engines": {
"node": ">=18"
}
@@ -2912,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",
@@ -2990,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",
@@ -3584,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",
@@ -3651,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",
@@ -4002,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",
@@ -4074,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"
},
@@ -4136,11 +4121,37 @@
"spdx-license-ids": "^3.0.0"
}
},
"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==",
"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.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"
@@ -6765,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",
@@ -6915,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",
@@ -7702,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",
@@ -7858,14 +7858,15 @@
}
},
"node_modules/prettier-plugin-organize-imports": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz",
"integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.2.0.tgz",
"integrity": "sha512-Zdy27UhlmyvATZi67BTnLcKTo8fm6Oik59Sz6H64PgZJVs6NJpPD1mT240mmJn62c98/QaL+r3kx9Q3gRpDajg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"prettier": ">=2.0",
"typescript": ">=2.9",
"vue-tsc": "^2.1.0"
"vue-tsc": "^2.1.0 || 3"
},
"peerDependenciesMeta": {
"vue-tsc": {
@@ -7954,6 +7955,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",
@@ -9022,9 +9041,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": {
@@ -9241,6 +9260,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",
@@ -9802,9 +9828,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"
},
@@ -10058,6 +10085,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"
}
}
}
}
+7 -5
View File
@@ -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,8 +100,8 @@
},
"license": "Apache-2.0",
"devDependencies": {
"@blockly/block-test": "^7.0.1",
"@blockly/dev-tools": "^9.0.0",
"@blockly/block-test": "^7.0.2",
"@blockly/dev-tools": "^9.0.2",
"@blockly/theme-modern": "^7.0.1",
"@hyperjump/browser": "^1.1.4",
"@hyperjump/json-schema": "^1.5.0",
@@ -109,12 +109,13 @@
"@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": "^52.0.2",
"eslint-plugin-mocha": "^11.1.0",
"eslint-plugin-prettier": "^5.2.1",
"glob": "^11.0.1",
"globals": "^16.0.0",
@@ -137,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",
+10 -2
View File
@@ -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.
@@ -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);
});
});
+611
View File
@@ -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',
);
});
});
+28
View File
@@ -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,
+1 -1
View File
@@ -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,
+30 -1
View File
@@ -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);
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+52 -1
View File
@@ -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,
@@ -157,6 +157,34 @@ suite('Clipboard', function () {
);
});
test('pasted blocks are bumped to not overlap in RTL', function () {
this.workspace.dispose();
this.workspace = Blockly.inject('blocklyDiv', {rtl: true});
const block = Blockly.serialization.blocks.append(
{
'type': 'controls_if',
'x': 38,
'y': 13,
},
this.workspace,
);
const data = block.toCopyData();
const newBlock = Blockly.clipboard.paste(data, this.workspace);
const oldBlockXY = block.getRelativeToSurfaceXY();
assert.deepEqual(
newBlock.getRelativeToSurfaceXY(),
new Blockly.utils.Coordinate(
oldBlockXY.x - Blockly.config.snapRadius,
oldBlockXY.y + Blockly.config.snapRadius * 2,
),
);
// Restore an LTR workspace.
this.workspace.dispose();
this.workspace = Blockly.inject('blocklyDiv');
});
test('pasted blocks are bumped to be outside the connection snap radius', function () {
Blockly.serialization.workspaces.load(
{
@@ -208,5 +236,28 @@ suite('Clipboard', function () {
new Blockly.utils.Coordinate(40, 40),
);
});
test('pasted comments are bumped to not overlap in RTL', function () {
this.workspace.dispose();
this.workspace = Blockly.inject('blocklyDiv', {rtl: true});
Blockly.Xml.domToWorkspace(
Blockly.utils.xml.textToDom(
'<xml><comment id="test" x=10 y=10/></xml>',
),
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');
});
});
});
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,

Some files were not shown because too many files have changed in this diff Show More