Fix: don't visit connections with the cursor. (#9030)

This commit is contained in:
Aaron Dodson
2025-05-13 11:03:01 -07:00
committed by GitHub
parent e7af75e051
commit ece662a45f
6 changed files with 223 additions and 300 deletions

View File

@@ -4,10 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type {BlockSvg} from '../block_svg.js';
import {BlockSvg} from '../block_svg.js';
import type {Field} from '../field.js';
import type {INavigable} from '../interfaces/i_navigable.js';
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
import type {RenderedConnection} from '../rendered_connection.js';
import {WorkspaceSvg} from '../workspace_svg.js';
/**
* Set of rules controlling keyboard navigation from a block.
@@ -24,7 +25,8 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
for (const field of input.fieldRow) {
return field;
}
if (input.connection) return input.connection as RenderedConnection;
if (input.connection?.targetBlock())
return input.connection.targetBlock() as BlockSvg;
}
return null;
@@ -38,12 +40,14 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
* which it is attached.
*/
getParent(current: BlockSvg): INavigable<unknown> | null {
const topBlock = current.getTopStackBlock();
if (current.previousConnection?.targetBlock()) {
const surroundParent = current.getSurroundParent();
if (surroundParent) return surroundParent;
} else if (current.outputConnection?.targetBlock()) {
return current.outputConnection.targetBlock();
}
return (
(this.getParentConnection(topBlock)?.targetConnection?.getParentInput()
?.connection as RenderedConnection) ?? topBlock
);
return current.workspace;
}
/**
@@ -54,21 +58,40 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
* block, or its next connection.
*/
getNextSibling(current: BlockSvg): INavigable<unknown> | null {
const nextConnection = current.nextConnection;
if (!current.outputConnection?.targetConnection && !nextConnection) {
// If this block has no connected output connection and no next
// connection, it must be the last block in the stack, so its next sibling
// is the first block of the next stack on the workspace.
const topBlocks = current.workspace.getTopBlocks(true);
let targetIndex = topBlocks.indexOf(current.getRootBlock()) + 1;
if (targetIndex >= topBlocks.length) {
targetIndex = 0;
}
const previousBlock = topBlocks[targetIndex];
return this.getParentConnection(previousBlock) ?? previousBlock;
if (current.nextConnection?.targetBlock()) {
return current.nextConnection?.targetBlock();
}
return nextConnection;
const parent = this.getParent(current);
let navigatingCrossStacks = false;
let siblings: (BlockSvg | Field)[] = [];
if (parent instanceof BlockSvg) {
for (let i = 0, input; (input = parent.inputList[i]); i++) {
if (input.connection) {
siblings.push(...input.fieldRow);
const child = input.connection.targetBlock();
if (child) {
siblings.push(child as BlockSvg);
}
}
}
} else if (parent instanceof WorkspaceSvg) {
siblings = parent.getTopBlocks(true);
navigatingCrossStacks = true;
} else {
return null;
}
const currentIndex = siblings.indexOf(
navigatingCrossStacks ? current.getRootBlock() : current,
);
if (currentIndex >= 0 && currentIndex < siblings.length - 1) {
return siblings[currentIndex + 1];
} else if (currentIndex === siblings.length - 1 && navigatingCrossStacks) {
return siblings[0];
}
return null;
}
/**
@@ -79,40 +102,45 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
* connection/block of the previous block stack if it is a root block.
*/
getPreviousSibling(current: BlockSvg): INavigable<unknown> | null {
const parentConnection = this.getParentConnection(current);
if (parentConnection) return parentConnection;
// If this block has no output/previous connection, it must be a root block,
// so its previous sibling is the last connection of the last block of the
// previous stack on the workspace.
const topBlocks = current.workspace.getTopBlocks(true);
let targetIndex = topBlocks.indexOf(current.getRootBlock()) - 1;
if (targetIndex < 0) {
targetIndex = topBlocks.length - 1;
if (current.previousConnection?.targetBlock()) {
return current.previousConnection?.targetBlock();
}
const lastBlock = topBlocks[targetIndex]
.getDescendants(true)
.reverse()
.pop();
return lastBlock?.nextConnection ?? lastBlock ?? null;
}
/**
* Gets the parent connection on a block.
* This is either an output connection, previous connection or undefined.
* If both connections exist return the one that is actually connected
* to another block.
*
* @param block The block to find the parent connection on.
* @returns The connection connecting to the parent of the block.
*/
protected getParentConnection(block: BlockSvg) {
if (!block.outputConnection || block.previousConnection?.isConnected()) {
return block.previousConnection;
const parent = this.getParent(current);
let navigatingCrossStacks = false;
let siblings: (BlockSvg | Field)[] = [];
if (parent instanceof BlockSvg) {
for (let i = 0, input; (input = parent.inputList[i]); i++) {
if (input.connection) {
siblings.push(...input.fieldRow);
const child = input.connection.targetBlock();
if (child) {
siblings.push(child as BlockSvg);
}
}
}
} else if (parent instanceof WorkspaceSvg) {
siblings = parent.getTopBlocks(true);
navigatingCrossStacks = true;
} else {
return null;
}
return block.outputConnection;
const currentIndex = siblings.indexOf(current);
let result: INavigable<any> | null = null;
if (currentIndex >= 1) {
result = siblings[currentIndex - 1];
} else if (currentIndex === 0 && navigatingCrossStacks) {
result = siblings[siblings.length - 1];
}
// If navigating to a previous stack, our previous sibling is the last
// block in it.
if (navigatingCrossStacks && result instanceof BlockSvg) {
return result.lastConnectionInStack(false)?.getSourceBlock() ?? result;
}
return result;
}
/**

View File

@@ -8,7 +8,6 @@ import type {BlockSvg} from '../block_svg.js';
import type {Field} from '../field.js';
import type {INavigable} from '../interfaces/i_navigable.js';
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
import type {RenderedConnection} from '../rendered_connection.js';
/**
* Set of rules controlling keyboard navigation from a field.
@@ -52,8 +51,8 @@ export class FieldNavigationPolicy implements INavigationPolicy<Field<any>> {
const fieldRow = newInput.fieldRow;
if (fieldIdx < fieldRow.length) return fieldRow[fieldIdx];
fieldIdx = 0;
if (newInput.connection) {
return newInput.connection as RenderedConnection;
if (newInput.connection?.targetBlock()) {
return newInput.connection.targetBlock() as BlockSvg;
}
}
return null;
@@ -74,8 +73,8 @@ export class FieldNavigationPolicy implements INavigationPolicy<Field<any>> {
let fieldIdx = parentInput.fieldRow.indexOf(current) - 1;
for (let i = curIdx; i >= 0; i--) {
const input = block.inputList[i];
if (input.connection && input !== parentInput) {
return input.connection as RenderedConnection;
if (input.connection?.targetBlock() && input !== parentInput) {
return input.connection.targetBlock() as BlockSvg;
}
const fieldRow = input.fieldRow;
if (fieldIdx > -1) return fieldRow[fieldIdx];

View File

@@ -14,8 +14,6 @@
*/
import {BlockSvg} from '../block_svg.js';
import {ConnectionType} from '../connection_type.js';
import {Field} from '../field.js';
import {FieldCheckbox} from '../field_checkbox.js';
import {FieldDropdown} from '../field_dropdown.js';
import {FieldImage} from '../field_image.js';
@@ -146,7 +144,12 @@ export class LineCursor extends Marker {
}
const newNode = this.getNextNode(
curNode,
this.workspace.isFlyout ? () => true : this.validLineNode.bind(this),
(candidate: INavigable<any> | null) => {
return (
candidate instanceof BlockSvg &&
!candidate.outputConnection?.targetBlock()
);
},
true,
);
@@ -168,11 +171,8 @@ export class LineCursor extends Marker {
if (!curNode) {
return null;
}
const newNode = this.getNextNode(
curNode,
this.workspace.isFlyout ? () => true : this.validInLineNode.bind(this),
true,
);
const newNode = this.getNextNode(curNode, () => true, true);
if (newNode) {
this.setCurNode(newNode);
@@ -193,7 +193,12 @@ export class LineCursor extends Marker {
}
const newNode = this.getPreviousNode(
curNode,
this.workspace.isFlyout ? () => true : this.validLineNode.bind(this),
(candidate: INavigable<any> | null) => {
return (
candidate instanceof BlockSvg &&
!candidate.outputConnection?.targetBlock()
);
},
true,
);
@@ -215,11 +220,8 @@ export class LineCursor extends Marker {
if (!curNode) {
return null;
}
const newNode = this.getPreviousNode(
curNode,
this.workspace.isFlyout ? () => true : this.validInLineNode.bind(this),
true,
);
const newNode = this.getPreviousNode(curNode, () => true, true);
if (newNode) {
this.setCurNode(newNode);
@@ -229,102 +231,26 @@ export class LineCursor extends Marker {
/**
* Returns true iff the node to which we would navigate if in() were
* called, which will be a validInLineNode, is also a validLineNode
* - in effect, if the LineCursor is at the end of the 'current
* called is the same as the node to which we would navigate if next() were
* called - in effect, if the LineCursor is at the end of the 'current
* line' of the program.
*/
atEndOfLine(): boolean {
const curNode = this.getCurNode();
if (!curNode) return false;
const rightNode = this.getNextNode(
const inNode = this.getNextNode(curNode, () => true, true);
const nextNode = this.getNextNode(
curNode,
this.validInLineNode.bind(this),
false,
(candidate: INavigable<any> | null) => {
return (
candidate instanceof BlockSvg &&
!candidate.outputConnection?.targetBlock()
);
},
true,
);
return this.validLineNode(rightNode);
}
/**
* Returns true iff the given node represents the "beginning of a
* new line of code" (and thus can be visited by pressing the
* up/down arrow keys). Specifically, if the node is for:
*
* - Any block that is not a value block.
* - A top-level value block (one that is unconnected).
* - An unconnected next statement input.
* - An unconnected 'next' connection - the "blank line at the end".
* This is to facilitate connecting additional blocks to a
* stack/substack.
*
* If options.stackConnections is true (the default) then allow the
* cursor to visit all (useful) stack connection by additionally
* returning true for:
*
* - Any next statement input
* - Any 'next' connection.
* - An unconnected previous statement input.
*
* @param node The AST node to check.
* @returns True if the node should be visited, false otherwise.
*/
protected validLineNode(node: INavigable<any> | null): boolean {
if (!node) return false;
if (node instanceof BlockSvg) {
return !node.outputConnection?.isConnected();
} else if (node instanceof RenderedConnection) {
if (node.type === ConnectionType.NEXT_STATEMENT) {
return this.options.stackConnections || !node.isConnected();
} else if (node.type === ConnectionType.PREVIOUS_STATEMENT) {
return this.options.stackConnections && !node.isConnected();
}
}
return false;
}
/**
* Returns true iff the given node can be visited by the cursor when
* using the left/right arrow keys. Specifically, if the node is
* any node for which validLineNode would return true, plus:
*
* - Any block.
* - Any field that is not a full block field.
* - Any unconnected next or input connection. This is to
* facilitate connecting additional blocks.
*
* @param node The AST node to check whether it is valid.
* @returns True if the node should be visited, false otherwise.
*/
protected validInLineNode(node: INavigable<any> | null): boolean {
if (!node) return false;
if (this.validLineNode(node)) return true;
if (node instanceof BlockSvg || node instanceof Field) {
return true;
} else if (
node instanceof RenderedConnection &&
node.getParentInput() &&
(node.type === ConnectionType.INPUT_VALUE ||
node.type === ConnectionType.NEXT_STATEMENT)
) {
return !node.isConnected();
}
return false;
}
/**
* Returns true iff the given node can be visited by the cursor.
* Specifically, if the node is any for which validInLineNode would
* return true, or if it is a workspace node.
*
* @param node The AST node to check whether it is valid.
* @returns True if the node should be visited, false otherwise.
*/
protected validNode(node: INavigable<any> | null): boolean {
return (
!!node && (this.validInLineNode(node) || node instanceof WorkspaceSvg)
);
return inNode === nextNode;
}
/**
@@ -347,15 +273,15 @@ export class LineCursor extends Marker {
let newNode =
this.workspace.getNavigator().getFirstChild(node) ||
this.workspace.getNavigator().getNextSibling(node);
if (isValid(newNode)) return newNode;
if (newNode) {
visitedNodes.add(node);
return this.getNextNodeImpl(newNode, isValid, visitedNodes);
let target = node;
while (target && !newNode) {
const parent = this.workspace.getNavigator().getParent(target);
if (!parent) break;
newNode = this.workspace.getNavigator().getNextSibling(parent);
target = parent;
}
newNode = this.findSiblingOrParentSibling(
this.workspace.getNavigator().getParent(node),
);
if (isValid(newNode)) return newNode;
if (newNode) {
visitedNodes.add(node);
@@ -402,15 +328,12 @@ export class LineCursor extends Marker {
visitedNodes: Set<INavigable<any>> = new Set<INavigable<any>>(),
): INavigable<any> | null {
if (!node || visitedNodes.has(node)) return null;
let newNode: INavigable<any> | null = this.workspace
.getNavigator()
.getPreviousSibling(node);
if (newNode) {
newNode = this.getRightMostChild(newNode);
} else {
newNode = this.workspace.getNavigator().getParent(node);
}
const newNode =
this.getRightMostChild(
this.workspace.getNavigator().getPreviousSibling(node),
node,
) || this.workspace.getNavigator().getParent(node);
if (isValid(newNode)) return newNode;
if (newNode) {
@@ -441,24 +364,6 @@ export class LineCursor extends Marker {
return this.getPreviousNodeImpl(node, isValid);
}
/**
* From the given node find either the next valid sibling or the parent's
* next sibling.
*
* @param node The current position in the AST.
* @returns The next sibling node, the parent's next sibling, or null.
*/
private findSiblingOrParentSibling(
node: INavigable<any> | null,
): INavigable<any> | null {
if (!node) return null;
const nextNode = this.workspace.getNavigator().getNextSibling(node);
if (nextNode) return nextNode;
return this.findSiblingOrParentSibling(
this.workspace.getNavigator().getParent(node),
);
}
/**
* Get the right most child of a node.
*
@@ -466,17 +371,22 @@ export class LineCursor extends Marker {
* @returns The right most child of the given node, or the node if no child
* exists.
*/
private getRightMostChild(node: INavigable<any>): INavigable<any> | null {
getRightMostChild(
node: INavigable<any> | null,
stopIfFound: INavigable<any>,
): INavigable<any> | null {
if (!node) return node;
let newNode = this.workspace.getNavigator().getFirstChild(node);
if (!newNode) return node;
if (!newNode || newNode === stopIfFound) return node;
for (
let nextNode: INavigable<any> | null = newNode;
nextNode;
nextNode = this.workspace.getNavigator().getNextSibling(newNode)
) {
if (nextNode === stopIfFound) break;
newNode = nextNode;
}
return this.getRightMostChild(newNode);
return this.getRightMostChild(newNode, stopIfFound);
}
/**
@@ -537,10 +447,7 @@ export class LineCursor extends Marker {
this.potentialNodes = null;
if (!nodes) throw new Error('must call preDelete first');
for (const node of nodes) {
if (
this.validNode(node) &&
!this.getSourceBlockFromNode(node)?.disposed
) {
if (!this.getSourceBlockFromNode(node)?.disposed) {
this.setCurNode(node);
return;
}

View File

@@ -22,16 +22,7 @@ export class WorkspaceNavigationPolicy
*/
getFirstChild(current: WorkspaceSvg): INavigable<unknown> | null {
const blocks = current.getTopBlocks(true);
if (!blocks.length) return null;
const block = blocks[0];
let topConnection = block.outputConnection;
if (
!topConnection ||
(block.previousConnection && block.previousConnection.isConnected())
) {
topConnection = block.previousConnection;
}
return topConnection ?? block;
return blocks.length ? blocks[0] : null;
}
/**

View File

@@ -97,34 +97,34 @@ suite('Cursor', function () {
this.cursor.setCurNode(prevNode);
this.cursor.next();
const curNode = this.cursor.getCurNode();
assert.equal(curNode, this.blocks.B.getInput('NAME4').connection);
assert.equal(curNode, this.blocks.C);
});
test('In - From attached input connection', function () {
test('In - From field to attached input connection', function () {
const fieldBlock = this.blocks.E;
const inputConnectionNode = this.blocks.A.inputList[0].connection;
this.cursor.setCurNode(inputConnectionNode);
const fieldNode = this.blocks.A.getField('NAME2');
this.cursor.setCurNode(fieldNode);
this.cursor.in();
const curNode = this.cursor.getCurNode();
assert.equal(curNode, fieldBlock);
});
test('Prev - From previous connection does not skip over next connection', function () {
test('Prev - From previous connection does skip over next connection', function () {
const prevConnection = this.blocks.B.previousConnection;
const prevConnectionNode = prevConnection;
this.cursor.setCurNode(prevConnectionNode);
this.cursor.prev();
const curNode = this.cursor.getCurNode();
assert.equal(curNode, this.blocks.A.nextConnection);
assert.equal(curNode, this.blocks.A);
});
test('Prev - From first connection loop to last next connection', function () {
const prevConnection = this.blocks.A.previousConnection;
test('Prev - From first block loop to last block', function () {
const prevConnection = this.blocks.A;
const prevConnectionNode = prevConnection;
this.cursor.setCurNode(prevConnectionNode);
this.cursor.prev();
const curNode = this.cursor.getCurNode();
assert.equal(curNode, this.blocks.D.nextConnection);
assert.equal(curNode, this.blocks.D);
});
test('Out - From field does not skip over block node', function () {
@@ -225,11 +225,11 @@ suite('Cursor', function () {
});
test('getFirstNode', function () {
const node = this.cursor.getFirstNode();
assert.equal(node, this.blockA.previousConnection);
assert.equal(node, this.blockA);
});
test('getLastNode', function () {
const node = this.cursor.getLastNode();
assert.equal(node, this.blockA.nextConnection);
assert.equal(node, this.blockA);
});
});
@@ -242,11 +242,11 @@ suite('Cursor', function () {
});
test('getFirstNode', function () {
const node = this.cursor.getFirstNode();
assert.equal(node, this.blockA.outputConnection);
assert.equal(node, this.blockA);
});
test('getLastNode', function () {
const node = this.cursor.getLastNode();
assert.equal(node, this.blockA.inputList[0].connection);
assert.equal(node, this.blockA);
});
});
suite('one c-hat block', function () {
@@ -262,7 +262,7 @@ suite('Cursor', function () {
});
test('getLastNode', function () {
const node = this.cursor.getLastNode();
assert.equal(node, this.blockA.inputList[0].connection);
assert.equal(node, this.blockA);
});
});
@@ -295,12 +295,12 @@ suite('Cursor', function () {
test('getFirstNode', function () {
const node = this.cursor.getFirstNode();
const blockA = this.workspace.getBlockById('A');
assert.equal(node, blockA.previousConnection);
assert.equal(node, blockA);
});
test('getLastNode', function () {
const node = this.cursor.getLastNode();
const blockB = this.workspace.getBlockById('B');
assert.equal(node, blockB.nextConnection);
assert.equal(node, blockB);
});
});
@@ -335,12 +335,12 @@ suite('Cursor', function () {
test('getFirstNode', function () {
const node = this.cursor.getFirstNode();
const blockA = this.workspace.getBlockById('A');
assert.equal(node, blockA.outputConnection);
assert.equal(node, blockA);
});
test('getLastNode', function () {
const node = this.cursor.getLastNode();
const blockB = this.workspace.getBlockById('B');
assert.equal(node, blockB.inputList[0].connection);
assert.equal(node, blockB);
});
});
@@ -385,15 +385,14 @@ suite('Cursor', function () {
test('getFirstNode', function () {
const node = this.cursor.getFirstNode();
const location = node;
const previousConnection =
this.workspace.getBlockById('A').previousConnection;
assert.equal(location, previousConnection);
const blockA = this.workspace.getBlockById('A');
assert.equal(location, blockA);
});
test('getLastNode', function () {
const node = this.cursor.getLastNode();
const location = node;
const nextConnection = this.workspace.getBlockById('D').nextConnection;
assert.equal(location, nextConnection);
const blockD = this.workspace.getBlockById('D');
assert.equal(location, blockD);
});
});
});
@@ -439,8 +438,8 @@ suite('Cursor', function () {
this.cursor = this.workspace.getCursor();
this.neverValid = () => false;
this.alwaysValid = () => true;
this.isConnection = (node) => {
return node && node instanceof Blockly.RenderedConnection;
this.isBlock = (node) => {
return node && node instanceof Blockly.BlockSvg;
};
});
teardown(function () {
@@ -528,7 +527,7 @@ suite('Cursor', function () {
assert.equal(nextNode, this.blockB.getField('FIELD'));
});
test('Always valid - start at end', function () {
const startNode = this.blockC.nextConnection;
const startNode = this.blockC.getField('FIELD');
const nextNode = this.cursor.getNextNode(
startNode,
this.alwaysValid,
@@ -537,29 +536,29 @@ suite('Cursor', function () {
assert.isNull(nextNode);
});
test('Valid if connection - start at top', function () {
const startNode = this.blockA.previousConnection;
test('Valid if block - start at top', function () {
const startNode = this.blockA;
const nextNode = this.cursor.getNextNode(
startNode,
this.isConnection,
this.isBlock,
false,
);
assert.equal(nextNode, this.blockA.nextConnection);
assert.equal(nextNode, this.blockB);
});
test('Valid if connection - start in middle', function () {
test('Valid if block - start in middle', function () {
const startNode = this.blockB;
const nextNode = this.cursor.getNextNode(
startNode,
this.isConnection,
this.isBlock,
false,
);
assert.equal(nextNode, this.blockB.nextConnection);
assert.equal(nextNode, this.blockC);
});
test('Valid if connection - start at end', function () {
const startNode = this.blockC.nextConnection;
test('Valid if block - start at end', function () {
const startNode = this.blockC.getField('FIELD');
const nextNode = this.cursor.getNextNode(
startNode,
this.isConnection,
this.isBlock,
false,
);
assert.isNull(nextNode);
@@ -583,14 +582,10 @@ suite('Cursor', function () {
assert.equal(nextNode, this.blockA.previousConnection);
});
test('Valid if connection - start at end - with loopback', function () {
const startNode = this.blockC.nextConnection;
const nextNode = this.cursor.getNextNode(
startNode,
this.isConnection,
true,
);
assert.equal(nextNode, this.blockA.previousConnection);
test('Valid if block - start at end - with loopback', function () {
const startNode = this.blockC;
const nextNode = this.cursor.getNextNode(startNode, this.isBlock, true);
assert.equal(nextNode, this.blockA);
});
});
});
@@ -637,8 +632,8 @@ suite('Cursor', function () {
this.cursor = this.workspace.getCursor();
this.neverValid = () => false;
this.alwaysValid = () => true;
this.isConnection = (node) => {
return node && node instanceof Blockly.RenderedConnection;
this.isBlock = (node) => {
return node && node instanceof Blockly.BlockSvg;
};
});
teardown(function () {
@@ -708,7 +703,7 @@ suite('Cursor', function () {
});
test('Always valid - start at top', function () {
const startNode = this.blockA.previousConnection;
const startNode = this.blockA;
const previousNode = this.cursor.getPreviousNode(
startNode,
this.alwaysValid,
@@ -723,7 +718,7 @@ suite('Cursor', function () {
this.alwaysValid,
false,
);
assert.equal(previousNode, this.blockB.previousConnection);
assert.equal(previousNode, this.blockA.getField('FIELD'));
});
test('Always valid - start at end', function () {
const startNode = this.blockC.nextConnection;
@@ -735,32 +730,32 @@ suite('Cursor', function () {
assert.equal(previousNode, this.blockC.getField('FIELD'));
});
test('Valid if connection - start at top', function () {
const startNode = this.blockA.previousConnection;
test('Valid if block - start at top', function () {
const startNode = this.blockA;
const previousNode = this.cursor.getPreviousNode(
startNode,
this.isConnection,
this.isBlock,
false,
);
assert.isNull(previousNode);
});
test('Valid if connection - start in middle', function () {
test('Valid if block - start in middle', function () {
const startNode = this.blockB;
const previousNode = this.cursor.getPreviousNode(
startNode,
this.isConnection,
this.isBlock,
false,
);
assert.equal(previousNode, this.blockB.previousConnection);
assert.equal(previousNode, this.blockA);
});
test('Valid if connection - start at end', function () {
const startNode = this.blockC.nextConnection;
test('Valid if block - start at end', function () {
const startNode = this.blockC;
const previousNode = this.cursor.getPreviousNode(
startNode,
this.isConnection,
this.isBlock,
false,
);
assert.equal(previousNode, this.blockC.previousConnection);
assert.equal(previousNode, this.blockB);
});
test('Never valid - start at top - with loopback', function () {
const startNode = this.blockA.previousConnection;
@@ -780,14 +775,14 @@ suite('Cursor', function () {
);
assert.equal(previousNode, this.blockC.nextConnection);
});
test('Valid if connection - start at top - with loopback', function () {
const startNode = this.blockA.previousConnection;
test('Valid if block - start at top - with loopback', function () {
const startNode = this.blockA;
const previousNode = this.cursor.getPreviousNode(
startNode,
this.isConnection,
this.isBlock,
true,
);
assert.equal(previousNode, this.blockC.nextConnection);
assert.equal(previousNode, this.blockC);
});
});
});

View File

@@ -166,7 +166,7 @@ suite('Navigation', function () {
},
{
'type': 'fields_and_input2',
'message0': '%1 %2 %3 hi %4 bye',
'message0': '%1 %2 %3 %4 bye',
'args0': [
{
'type': 'input_value',
@@ -245,6 +245,7 @@ suite('Navigation', function () {
const outputNextBlock = this.workspace.newBlock('output_next');
this.blocks.secondBlock = secondBlock;
this.blocks.outputNextBlock = outputNextBlock;
this.workspace.cleanUp();
});
suite('Next', function () {
setup(function () {
@@ -261,12 +262,11 @@ suite('Navigation', function () {
const nextNode = this.navigator.getNextSibling(prevConnection);
assert.equal(nextNode, this.blocks.statementInput1);
});
test('fromBlockToNext', function () {
const nextConnection = this.blocks.statementInput1.nextConnection;
test('fromBlockToNextBlock', function () {
const nextNode = this.navigator.getNextSibling(
this.blocks.statementInput1,
);
assert.equal(nextNode, nextConnection);
assert.equal(nextNode, this.blocks.statementInput2);
});
test('fromNextToPrevious', function () {
const nextConnection = this.blocks.statementInput1.nextConnection;
@@ -304,12 +304,12 @@ suite('Navigation', function () {
const nextNode = this.navigator.getNextSibling(output);
assert.equal(nextNode, this.blocks.fieldWithOutput);
});
test('fromFieldToInput', function () {
test('fromFieldToNestedBlock', function () {
const field = this.blocks.statementInput1.inputList[0].fieldRow[1];
const inputConnection =
this.blocks.statementInput1.inputList[0].connection;
const nextNode = this.navigator.getNextSibling(field);
assert.equal(nextNode, inputConnection);
assert.equal(nextNode, this.blocks.fieldWithOutput);
});
test('fromFieldToField', function () {
const field = this.blocks.fieldAndInputs.inputList[0].fieldRow[0];
@@ -338,17 +338,17 @@ suite('Navigation', function () {
});
test('fromBlockToPrevious', function () {
const prevNode = this.navigator.getPreviousSibling(
this.blocks.statementInput1,
this.blocks.statementInput2,
);
const prevConnection = this.blocks.statementInput1.previousConnection;
assert.equal(prevNode, prevConnection);
const previousBlock = this.blocks.statementInput1;
assert.equal(prevNode, previousBlock);
});
test('fromBlockToOutput', function () {
test('fromOutputBlockToPreviousField', function () {
const prevNode = this.navigator.getPreviousSibling(
this.blocks.fieldWithOutput,
);
const outputConnection = this.blocks.fieldWithOutput.outputConnection;
assert.equal(prevNode, outputConnection);
assert.equal(prevNode, [...this.blocks.statementInput1.getFields()][1]);
});
test('fromNextToBlock', function () {
const nextConnection = this.blocks.statementInput1.nextConnection;
@@ -383,11 +383,16 @@ suite('Navigation', function () {
assert.isNull(prevNode);
});
test('fromFieldToInput', function () {
const outputBlock = this.workspace.newBlock('field_input');
this.blocks.fieldAndInputs2.inputList[0].connection.connect(
outputBlock.outputConnection,
);
const field = this.blocks.fieldAndInputs2.inputList[1].fieldRow[0];
const inputConnection =
this.blocks.fieldAndInputs2.inputList[0].connection;
const prevNode = this.navigator.getPreviousSibling(field);
assert.equal(prevNode, inputConnection);
assert.equal(prevNode, outputBlock);
});
test('fromFieldToField', function () {
const field = this.blocks.fieldAndInputs.inputList[1].fieldRow[0];
@@ -423,10 +428,10 @@ suite('Navigation', function () {
const inNode = this.navigator.getFirstChild(input.connection);
assert.equal(inNode, previousConnection);
});
test('fromBlockToInput', function () {
const input = this.blocks.valueInput.inputList[0];
test('fromBlockToField', function () {
const field = this.blocks.valueInput.getField('NAME');
const inNode = this.navigator.getFirstChild(this.blocks.valueInput);
assert.equal(inNode, input.connection);
assert.equal(inNode, field);
});
test('fromBlockToField', function () {
const inNode = this.navigator.getFirstChild(
@@ -440,12 +445,10 @@ suite('Navigation', function () {
assert.isNull(inNode);
});
test('fromBlockToInput_DummyInputValue', function () {
const inputConnection =
this.blocks.dummyInputValue.inputList[1].connection;
const inNode = this.navigator.getFirstChild(
this.blocks.dummyInputValue,
);
assert.equal(inNode, inputConnection);
assert.equal(inNode, null);
});
test('fromOuputToNull', function () {
const output = this.blocks.fieldWithOutput.outputConnection;
@@ -540,25 +543,25 @@ suite('Navigation', function () {
const outNode = this.navigator.getParent(next);
assert.equal(outNode, this.blocks.secondBlock.inputList[0].connection);
});
test('fromBlockToStack', function () {
test('fromBlockToWorkspace', function () {
const outNode = this.navigator.getParent(this.blocks.statementInput2);
assert.equal(outNode, this.blocks.statementInput1);
assert.equal(outNode, this.workspace);
});
test('fromBlockToInput', function () {
const input = this.blocks.statementInput2.inputList[1].connection;
test('fromBlockToEnclosingStatement', function () {
const enclosingStatement = this.blocks.statementInput2;
const outNode = this.navigator.getParent(this.blocks.statementInput3);
assert.equal(outNode, input);
assert.equal(outNode, enclosingStatement);
});
test('fromTopBlockToStack', function () {
test('fromTopBlockToWorkspace', function () {
const outNode = this.navigator.getParent(this.blocks.statementInput1);
assert.equal(outNode, this.blocks.statementInput1);
assert.equal(outNode, this.workspace);
});
test('fromBlockToStack_OutputConnection', function () {
test('fromOutputBlockToWorkspace', function () {
const outNode = this.navigator.getParent(this.blocks.fieldWithOutput2);
assert.equal(outNode, this.blocks.fieldWithOutput2);
assert.equal(outNode, this.workspace);
});
test('fromBlockToInput_OutputConnection', function () {
const inputConnection = this.blocks.secondBlock.inputList[0].connection;
test('fromOutputNextBlockToWorkspace', function () {
const inputConnection = this.blocks.secondBlock;
const outNode = this.navigator.getParent(this.blocks.outputNextBlock);
assert.equal(outNode, inputConnection);
});