mirror of
https://github.com/google/blockly.git
synced 2026-06-17 08:35:12 +02:00
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:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: >
|
||||
|
||||
@@ -70,6 +70,8 @@ handlers:
|
||||
# Blockly files.
|
||||
- url: /static
|
||||
static_dir: static
|
||||
http_headers:
|
||||
Access-Control-Allow-Origin: "*"
|
||||
secure: always
|
||||
|
||||
# Storage API.
|
||||
|
||||
+27
-17
@@ -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) {
|
||||
|
||||
@@ -1924,6 +1924,9 @@ export class BlockSvg
|
||||
/** See IFocusableNode.onNodeFocus. */
|
||||
onNodeFocus(): void {
|
||||
this.select();
|
||||
this.workspace.scrollBoundsIntoView(
|
||||
this.getBoundingRectangleWithoutChildren(),
|
||||
);
|
||||
}
|
||||
|
||||
/** See IFocusableNode.onNodeBlur. */
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
@@ -181,7 +181,8 @@ let content = `
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
.blocklyDragging.blocklyDraggingDelete {
|
||||
.blocklyDragging.blocklyDraggingDelete,
|
||||
.blocklyDragging.blocklyDraggingDelete .blocklyField {
|
||||
cursor: url("<<<PATH>>>/handdelete.cur"), auto;
|
||||
}
|
||||
|
||||
|
||||
+7
-6
@@ -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 {}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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
@@ -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 {}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Generated
+160
-123
@@ -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
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {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,
|
||||
|
||||
@@ -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 {defineMutatorBlocks} 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 {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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user