From bb46ad56e6fbdea909f1a80c897115ea8db912e7 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Thu, 7 Aug 2025 21:10:04 -0400 Subject: [PATCH 01/19] fix: clean up shortcut help dialog (#688) * chore: rearrange html to be a little nicer * fix: dont show shortcuts with no keys * fix: update shortcuts shown in dialog * fix: i18n for keyboard help header * fix: fix return type --- src/constants.ts | 20 +++++++--- src/shortcut_dialog.ts | 89 +++++++++++++++++++++--------------------- 2 files changed, 60 insertions(+), 49 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 4ef43f7d..9761a4cb 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -35,6 +35,7 @@ export enum SHORTCUT_NAMES { LEFT = 'left', NEXT_STACK = 'next_stack', PREVIOUS_STACK = 'previous_stack', + // Unused. INSERT = 'insert', EDIT_OR_CONFIRM = 'edit_or_confirm', DISCONNECT = 'disconnect', @@ -52,8 +53,16 @@ export enum SHORTCUT_NAMES { CREATE_WS_CURSOR = 'to_workspace', LIST_SHORTCUTS = 'list_shortcuts', CLEAN_UP = 'clean_up_workspace', + START_MOVE = 'start_move', } +export const SHORTCUT_NAMES_TO_DISPLAY_TEXT: Record = { + 'keyboard_nav_copy': Msg['Copy'] || 'Copy', + 'keyboard_nav_cut': Msg['Cut'] || 'Cut', + 'keyboard_nav_paste': Msg['Paste'] || 'Paste', + 'start_move': Msg['MOVE_BLOCK'] || 'Move', +}; + /** * Types of possible messages passed into the loggingCallback in the Navigation * class. @@ -73,7 +82,7 @@ export const SHORTCUT_CATEGORIES: Record< // Also allow undo/redo. Document the non-keyboard-nav versions of others for // better text because temporarily the name in the table is derived from // these id-like names. - Array + Array > = {}; SHORTCUT_CATEGORIES[Msg['SHORTCUTS_GENERAL']] = [ @@ -86,12 +95,12 @@ SHORTCUT_CATEGORIES[Msg['SHORTCUTS_GENERAL']] = [ ]; SHORTCUT_CATEGORIES[Msg['SHORTCUTS_EDITING']] = [ - SHORTCUT_NAMES.INSERT, 'delete', SHORTCUT_NAMES.DISCONNECT, - 'cut', - 'copy', - 'paste', + SHORTCUT_NAMES.START_MOVE, + SHORTCUT_NAMES.CUT, + SHORTCUT_NAMES.COPY, + SHORTCUT_NAMES.PASTE, SHORTCUT_NAMES.DUPLICATE, 'undo', 'redo', @@ -104,4 +113,5 @@ SHORTCUT_CATEGORIES[Msg['SHORTCUTS_CODE_NAVIGATION']] = [ SHORTCUT_NAMES.LEFT, SHORTCUT_NAMES.NEXT_STACK, SHORTCUT_NAMES.PREVIOUS_STACK, + SHORTCUT_NAMES.CREATE_WS_CURSOR, ]; diff --git a/src/shortcut_dialog.ts b/src/shortcut_dialog.ts index e122af78..49e20e5c 100644 --- a/src/shortcut_dialog.ts +++ b/src/shortcut_dialog.ts @@ -52,19 +52,6 @@ export class ShortcutDialog { } } - /** - * Update the modifier key to the user's specific platform. - */ - updatePlatformName() { - const platform = this.getPlatform(); - const platformEl = this.outputDiv - ? this.outputDiv.querySelector('.platform') - : null; - if (platformEl) { - platformEl.textContent = platform; - } - } - toggle(workspace: Blockly.WorkspaceSvg) { clearHelpHint(workspace); this.toggleInternal(); @@ -88,6 +75,9 @@ export class ShortcutDialog { * @returns A title case version of the name. */ getReadableShortcutName(shortcutName: string) { + if (Constants.SHORTCUT_NAMES_TO_DISPLAY_TEXT[shortcutName]) { + return Constants.SHORTCUT_NAMES_TO_DISPLAY_TEXT[shortcutName]; + } return upperCaseFirst(shortcutName.replace(/_/gi, ' ')); } @@ -95,47 +85,46 @@ export class ShortcutDialog { * List all currently registered shortcuts as a table. */ createModalContent() { - let modalContents = ``; + this.outputDiv.innerHTML = modalContents; this.modalContainer = this.outputDiv.querySelector('.modal-container'); this.shortcutDialog = this.outputDiv.querySelector('.shortcut-modal'); this.closeButton = this.outputDiv.querySelector('.close-modal'); - this.updatePlatformName(); - // Can we also intercept the Esc key to dismiss. if (this.closeButton) { this.closeButton.addEventListener('click', (e) => { this.toggleInternal(); @@ -144,13 +133,25 @@ export class ShortcutDialog { } } + private getTableRowForShortcut(keyboardShortcut: string) { + const name = this.getReadableShortcutName(keyboardShortcut); + const keys = this.actionShortcutsToHTML(keyboardShortcut); + if (!name || !keys) return ''; + return ` + + ${name} + ${keys} + `; + } + private actionShortcutsToHTML(action: string) { const shortcuts = getLongActionShortcutsAsKeys(action); - return shortcuts.map((keys) => this.actionShortcutToHTML(keys)).join(' / '); + return shortcuts.map((keys) => this.keysToHTML(keys)).join(' / '); } - private actionShortcutToHTML(keys: string[]) { + private keysToHTML(keys: string[]) { const separator = navigator.platform.startsWith('Mac') ? '' : ' + '; + if (!keys || !keys.length) return ''; return [ ``, ...keys.map((key, index) => { From 10d7bba9fdde4bc4cfd33c1bee778c3c4f30a2f0 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 8 Aug 2025 08:05:55 -0700 Subject: [PATCH 02/19] fix: Only calculate available connections once. (#690) --- src/keyboard_drag_strategy.ts | 44 ++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/src/keyboard_drag_strategy.ts b/src/keyboard_drag_strategy.ts index 09b3bcc2..d5729ad7 100644 --- a/src/keyboard_drag_strategy.ts +++ b/src/keyboard_drag_strategy.ts @@ -36,6 +36,9 @@ export class KeyboardDragStrategy extends dragging.BlockDragStrategy { /** Where a constrained movement should start when traversing the tree. */ private searchNode: RenderedConnection | null = null; + /** List of all connections available on the workspace. */ + private allConnections: RenderedConnection[] = []; + constructor( private block: BlockSvg, public moveType: MoveType, @@ -46,6 +49,22 @@ export class KeyboardDragStrategy extends dragging.BlockDragStrategy { override startDrag(e?: PointerEvent) { super.startDrag(e); + + for (const topBlock of this.block.workspace.getTopBlocks(true)) { + this.allConnections.push( + ...topBlock + .getDescendants(true) + .flatMap((block: BlockSvg) => block.getConnections_(false)) + .sort((a: RenderedConnection, b: RenderedConnection) => { + let delta = a.y - b.y; + if (delta === 0) { + delta = a.x - b.x; + } + return delta; + }), + ); + } + // Set position of the dragging block, so that it doesn't pop // to the top left of the workspace. // @ts-expect-error block and startLoc are private. @@ -91,6 +110,7 @@ export class KeyboardDragStrategy extends dragging.BlockDragStrategy { override endDrag(e?: PointerEvent) { super.endDrag(e); + this.allConnections = []; this.block.removeIcon(MoveIcon.type); } @@ -168,31 +188,17 @@ export class KeyboardDragStrategy extends dragging.BlockDragStrategy { const connectionChecker = draggingBlock.workspace.connectionChecker; let candidateConnection: ConnectionCandidate | null = null; let potential: RenderedConnection | null = this.searchNode; - const allConnections: RenderedConnection[] = []; - for (const topBlock of draggingBlock.workspace.getTopBlocks(true)) { - allConnections.push( - ...topBlock - .getDescendants(true) - .flatMap((block: BlockSvg) => block.getConnections_(false)) - .sort((a: RenderedConnection, b: RenderedConnection) => { - let delta = a.y - b.y; - if (delta === 0) { - delta = a.x - b.x; - } - return delta; - }), - ); - } const dir = this.currentDragDirection; while (potential && !candidateConnection) { - const potentialIndex = allConnections.indexOf(potential); + const potentialIndex = this.allConnections.indexOf(potential); if (dir === Direction.Up || dir === Direction.Left) { potential = - allConnections[potentialIndex - 1] ?? - allConnections[allConnections.length - 1]; + this.allConnections[potentialIndex - 1] ?? + this.allConnections[this.allConnections.length - 1]; } else if (dir === Direction.Down || dir === Direction.Right) { - potential = allConnections[potentialIndex + 1] ?? allConnections[0]; + potential = + this.allConnections[potentialIndex + 1] ?? this.allConnections[0]; } localConns.forEach((conn: RenderedConnection) => { From 9dac4bdfb405b12d21bd2847a4ff313c9b038624 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 12 Aug 2025 08:33:48 -0700 Subject: [PATCH 03/19] feat: Deploy GH pages with screen reader experiment (#689) Fixes #686 This updates the GH pages workflow to: - Automatically deploy with changes to both `main` and `add-screen-reader-support-experimental` (the screen reader experimental branch--see https://github.com/google/blockly/issues/9283). - Upload the screen reader version of the playground to a new `/screenreader` subdirectory in the plugin repo's GitHub pages deployment. This will approximately double the amount of time needed for the GH pages deployment since it requires building two distinct copies of core Blockly and the plugin (tip-of-tree versions and the screen reader experimentation versions). Conveniently, this will act as a fast way to see screen reader changes since they will go live a few minutes after a change to the plugin. Unfortunately, that does not reciprocate for changes to core Blockly (where most of the screen reader changes will actually occur), so oftentimes a manual run of the workflow will be needed (which should suffice in those cases). Note that this needs to be approached in this way (building and uploading both projects for changes to either branch) since the action being used for deployment doesn't support partial deployment. See: https://github.com/actions/deploy-pages/issues/349. --- .github/workflows/pages.yml | 58 +++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index c86ebec2..3b70b9a0 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -2,14 +2,16 @@ name: Deploy static content to Pages on: - # Runs on pushes targeting the default branch + # Runs on pushes targeting the default and experimentation branches. push: - branches: ['main'] + branches: + - main + - add-screen-reader-support-experimental - # Allows you to run this workflow manually from the Actions tab + # Allows the workflow to be manually run from the Actions tab. workflow_dispatch: -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages. permissions: contents: read pages: write @@ -26,45 +28,77 @@ jobs: build: runs-on: ubuntu-latest steps: - - name: Checkout blockly + - name: Checkout tip-of-tree core Blockly uses: actions/checkout@v4 with: path: blockly repository: google/blockly ref: develop - - name: Checkout blockly-keyboard-experimentation + - name: Checkout add-screen-reader-support-experimental core Blockly + uses: actions/checkout@v4 + with: + path: blockly-add-screen-reader-support-experimental + repository: google/blockly + ref: add-screen-reader-support-experimental + + - name: Checkout tip-of-tree blockly-keyboard-experimentation uses: actions/checkout@v4 with: path: blockly-keyboard-experimentation + - name: Checkout add-screen-reader-support-experimental blockly-keyboard-experimentation + uses: actions/checkout@v4 + with: + path: blockly-keyboard-experimentation-add-screen-reader-support-experimental + ref: add-screen-reader-support-experimental + - name: Setup Node uses: actions/setup-node@v4 with: node-version: 20.x - - name: Build blockly + - name: Build tip-of-tree core Blockly run: | cd blockly npm ci npm run package cd dist npm link - cd ../.. - - name: Build blockly-keyboard-experimentation + - name: Build tip-of-tree blockly-keyboard-experimentation run: | cd blockly-keyboard-experimentation npm ci npm link blockly npm run ghpages - cd .. + mkdir ../ghpages + cp -r build/* ../ghpages/ + + - name: Build add-screen-reader-support-experimental core Blockly + run: | + cd blockly/dist + npm unlink -g + cd ../../blockly-add-screen-reader-support-experimental + npm ci + npm run package + cd dist + npm link + + - name: Build add-screen-reader-support-experimental blockly-keyboard-experimentation + run: | + cd blockly-keyboard-experimentation-add-screen-reader-support-experimental + npm ci + npm link blockly + npm run ghpages + mkdir ../ghpages/screenreader + cp -r build/* ../ghpages/screenreader/ - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: - # Upload build folder - path: './blockly-keyboard-experimentation/build' + # Upload configured GH pages site files. + path: './ghpages' deploy: environment: From e16f5aa031a29bcb3b252f3bb137e858bc2e41f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 11:06:27 -0700 Subject: [PATCH 04/19] chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#683) Bumps the npm_and_yarn group with 2 updates in the / directory: [on-headers](https://github.com/jshttp/on-headers) and [compression](https://github.com/expressjs/compression). Updates `on-headers` from 1.0.2 to 1.1.0 - [Release notes](https://github.com/jshttp/on-headers/releases) - [Changelog](https://github.com/jshttp/on-headers/blob/master/HISTORY.md) - [Commits](https://github.com/jshttp/on-headers/compare/v1.0.2...v1.1.0) Updates `compression` from 1.8.0 to 1.8.1 - [Release notes](https://github.com/expressjs/compression/releases) - [Changelog](https://github.com/expressjs/compression/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/compression/compare/1.8.0...v1.8.1) --- updated-dependencies: - dependency-name: on-headers dependency-version: 1.1.0 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: compression dependency-version: 1.8.1 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: RoboErikG --- package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4a68f389..2adddc54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3769,9 +3769,9 @@ } }, "node_modules/compression": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", - "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dev": true, "license": "MIT", "dependencies": { @@ -3779,7 +3779,7 @@ "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", - "on-headers": "~1.0.2", + "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" }, @@ -7554,9 +7554,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "dev": true, "license": "MIT", "engines": { From 7991855ff90ce3202a668968bae6ebb2cee9f3a3 Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Tue, 19 Aug 2025 12:48:04 +0100 Subject: [PATCH 05/19] test: Make tests faster, more robust, more consistent (#692) * fix(tests): Restore window size after scroll tests * docs(tests): "Selenium" -> "WebdriverIO" We aren't using Selenium, so we shouldn't be saying we do. * chore(tests): Make timeout(0) calls only when PAUSE_TIME is nonzero With PAUSE_TIME set to zero, on my relatively fast laptop none of these tests run slow enough to fail due to the default 2s timeous, and even with PAUSE_TIME set to 50ms the slowest test is 1024ms (of which 700ms is spent in brower.pause calls inside sendKeyAndWait). We do want to disable timeouts when debugging tests, both because we might set PAUSE_TIME to a larger value and because we might use the debugger to pause things entirely, but we don't need to disable timeouts when running tests just to check their results. * fix(tests): Set PAUSE_TIME to 0 It's very useful to be able to make tests run more slowly so you can watch them, but they should run as fast as possible by default. This cuts total test execution time on a 2021 MacBook Pro M1 approximately in half, from 42s to 22s. * refactor(tests): Use sendKeyAndWait where appropriate In most places we were already following browser.keys with a browser.pause(PAUSE_TIME), so using sendKeysAndWait is more succinct; in other places we didn't have the pause but it is not harmful to add a pause (especially now the default PAUSE_TIME is zero) and could be helpful when watching tests run with a non-zero PAUSE_TIME. * fix(tests): Make tabNavigateToWorkspace idempotent Previously this function would just send a bunch of tabs, which depended on focus state being as-on-document-load. Some tests (notably the ones in basic_test.ts) that have only a suiteSetup and not a (per-test) setup method were only were only passing because of the combination of: * Due to issue #632, pressing tab when the workspace is focused (and there are no further focusable elements on the page) incorrectly causes focus to move to the first focusable element on the page instead of (as would normally be the case) to the browser controls, and * The fact that the index.html had exactly one additional focusable div on the page, preceding the injection div. This meant that calling tabNavigateToWorkspace when the workspace was already focused would, only by sheer coincidence, result in the focus remaining on the workspace. By explicitly focusing a known element, tabNavigateToWorkspace should work correctly regardless of how many focusable elements are on the page and which one (if any) was focused before the call. * chore(tests): Remove unneeded tabNavigateToWorkspace calls Any time a tabNavigateToWorkspace call is followed by a call to focusOnBlock the former can be removed with no discernable effect except to make tests run slightly faster and with less flashing of the flyout. * fix(tests): Increase timeout for certain slow tests * docs(tests): Fix typo * chore(tests): Add missing timeout(0) calls (when PAUSE_TIME is nonzero) These two files were inadvertently omitted from commit 14d619c. * fix(tests): Lint * fix(tests): Add missing import --- test/webdriverio/test/actions_test.ts | 32 ++++----- test/webdriverio/test/basic_test.ts | 71 ++++--------------- test/webdriverio/test/block_comment_test.ts | 7 +- test/webdriverio/test/clipboard_test.ts | 25 +++---- test/webdriverio/test/delete_test.ts | 59 +++++---------- test/webdriverio/test/duplicate_test.ts | 12 ++-- test/webdriverio/test/flyout_test.ts | 8 ++- test/webdriverio/test/insert_test.ts | 29 ++++---- test/webdriverio/test/keyboard_mode_test.ts | 28 +++----- test/webdriverio/test/move_test.ts | 19 ++--- test/webdriverio/test/mutator_test.ts | 30 ++++---- test/webdriverio/test/scroll_test.ts | 40 ++++++++--- test/webdriverio/test/stack_navigation.ts | 18 +++-- test/webdriverio/test/styling_test.ts | 6 +- test/webdriverio/test/test_setup.ts | 49 +++++++++---- test/webdriverio/test/toast_test.ts | 4 ++ .../test/workspace_comment_test.ts | 7 +- 17 files changed, 203 insertions(+), 241 deletions(-) diff --git a/test/webdriverio/test/actions_test.ts b/test/webdriverio/test/actions_test.ts index 7f9b27e2..6b0a86c6 100644 --- a/test/webdriverio/test/actions_test.ts +++ b/test/webdriverio/test/actions_test.ts @@ -14,27 +14,31 @@ import { tabNavigateToWorkspace, testFileLocations, testSetup, + sendKeyAndWait, keyRight, contextMenuItems, } from './test_setup.js'; suite('Menus test', function () { - // Setting timeout to unlimited as these tests take longer time to run - this.timeout(0); + // Disable timeouts when non-zero PAUSE_TIME is used to watch tests run. + if (PAUSE_TIME) this.timeout(0); - // Clear the workspace and load start blocks + // Clear the workspace and load start blocks. setup(async function () { + // This is the first test suite, which must wait for Chrome + + // chromedriver to start up, which can be slow—perhaps a few + // seconds. Allow 30s just in case. + this.timeout(30000); + this.browser = await testSetup(testFileLocations.BASE); await this.browser.pause(PAUSE_TIME); }); test('Menu on block', async function () { // Navigate to draw_circle_1. - await tabNavigateToWorkspace(this.browser); await focusOnBlock(this.browser, 'draw_circle_1'); await this.browser.pause(PAUSE_TIME); - await this.browser.keys([Key.Ctrl, Key.Return]); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, [Key.Ctrl, Key.Return]); chai.assert.deepEqual( process.platform === 'darwin' @@ -70,14 +74,12 @@ suite('Menus test', function () { test('Menu on block in the toolbox', async function () { // Navigate to draw_circle_1. - await tabNavigateToWorkspace(this.browser); await focusOnBlock(this.browser, 'draw_circle_1'); // Navigate to a toolbox category await moveToToolboxCategory(this.browser, 'Functions'); // Move to flyout. await keyRight(this.browser); - await this.browser.keys([Key.Ctrl, Key.Return]); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, [Key.Ctrl, Key.Return]); chai.assert.deepEqual( process.platform === 'darwin' @@ -100,9 +102,8 @@ suite('Menus test', function () { test('Menu on workspace', async function () { // Navigate to draw_circle_1. await tabNavigateToWorkspace(this.browser); - await this.browser.keys('w'); - await this.browser.keys([Key.Ctrl, Key.Return]); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, 'w'); + await sendKeyAndWait(this.browser, [Key.Ctrl, Key.Return]); chai.assert.deepEqual( process.platform === 'darwin' @@ -132,12 +133,11 @@ suite('Menus test', function () { test('Menu on block during drag is not shown', async function () { // Navigate to draw_circle_1. - await tabNavigateToWorkspace(this.browser); await focusOnBlock(this.browser, 'draw_circle_1'); // Start moving the block - await this.browser.keys('m'); - await this.browser.keys([Key.Ctrl, Key.Return]); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, 'm'); + await sendKeyAndWait(this.browser, [Key.Ctrl, Key.Return]); + chai.assert.isTrue( await contextMenuExists(this.browser, 'Collapse Block', true), 'The menu should not be openable during a move', diff --git a/test/webdriverio/test/basic_test.ts b/test/webdriverio/test/basic_test.ts index abfd7dde..98912a38 100644 --- a/test/webdriverio/test/basic_test.ts +++ b/test/webdriverio/test/basic_test.ts @@ -16,6 +16,7 @@ import { testSetup, testFileLocations, PAUSE_TIME, + sendKeyAndWait, tabNavigateToWorkspace, keyLeft, keyRight, @@ -25,10 +26,10 @@ import { import {Key} from 'webdriverio'; suite('Keyboard navigation on Blocks', function () { - // Setting timeout to unlimited as these tests take a longer time to run than most mocha test - this.timeout(0); + // Disable timeouts when non-zero PAUSE_TIME is used to watch tests run. + if (PAUSE_TIME) this.timeout(0); - // Setup Selenium for all of the tests + // Clear the workspace and load start blocks. suiteSetup(async function () { this.browser = await testSetup(testFileLocations.NAVIGATION_TEST_BLOCKS); }); @@ -53,8 +54,6 @@ suite('Keyboard navigation on Blocks', function () { }); test('Down from statement block selects next block across stacks', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'p5_canvas_1'); await this.browser.pause(PAUSE_TIME); await keyDown(this.browser); @@ -65,8 +64,6 @@ suite('Keyboard navigation on Blocks', function () { }); test('Up from statement block selects previous block', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'simple_circle_1'); await this.browser.pause(PAUSE_TIME); await keyUp(this.browser); @@ -77,8 +74,6 @@ suite('Keyboard navigation on Blocks', function () { }); test('Down from parent block selects first child block', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'p5_setup_1'); await this.browser.pause(PAUSE_TIME); await keyDown(this.browser); @@ -88,8 +83,6 @@ suite('Keyboard navigation on Blocks', function () { }); test('Up from child block selects parent block', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'p5_canvas_1'); await this.browser.pause(PAUSE_TIME); await keyUp(this.browser); @@ -99,8 +92,6 @@ suite('Keyboard navigation on Blocks', function () { }); test('Right from block selects first field', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'p5_canvas_1'); await this.browser.pause(PAUSE_TIME); await keyRight(this.browser); @@ -113,8 +104,6 @@ suite('Keyboard navigation on Blocks', function () { }); test('Right from block selects first inline input', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'simple_circle_1'); await this.browser.pause(PAUSE_TIME); await keyRight(this.browser); @@ -126,8 +115,6 @@ suite('Keyboard navigation on Blocks', function () { }); test('Up from inline input selects statement block', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'math_number_2'); await this.browser.pause(PAUSE_TIME); await keyUp(this.browser); @@ -139,8 +126,6 @@ suite('Keyboard navigation on Blocks', function () { }); test('Left from first inline input selects block', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'math_number_2'); await this.browser.pause(PAUSE_TIME); await keyLeft(this.browser); @@ -152,8 +137,6 @@ suite('Keyboard navigation on Blocks', function () { }); test('Right from first inline input selects second inline input', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'math_number_2'); await this.browser.pause(PAUSE_TIME); await keyRight(this.browser); @@ -165,8 +148,6 @@ suite('Keyboard navigation on Blocks', function () { }); test('Left from second inline input selects first inline input', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'math_number_3'); await this.browser.pause(PAUSE_TIME); await keyLeft(this.browser); @@ -178,8 +159,6 @@ suite('Keyboard navigation on Blocks', function () { }); test('Right from last inline input selects next block', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'colour_picker_1'); await this.browser.pause(PAUSE_TIME); await keyRight(this.browser); @@ -190,8 +169,6 @@ suite('Keyboard navigation on Blocks', function () { }); test('Down from inline input selects next block', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'colour_picker_1'); await this.browser.pause(PAUSE_TIME); await keyDown(this.browser); @@ -202,8 +179,6 @@ suite('Keyboard navigation on Blocks', function () { }); test("Down from inline input selects block's child block", async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'logic_boolean_1'); await this.browser.pause(PAUSE_TIME); await keyDown(this.browser); @@ -214,8 +189,6 @@ suite('Keyboard navigation on Blocks', function () { }); test('Right from text block selects shadow block then field', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'text_print_1'); await this.browser.pause(PAUSE_TIME); await keyRight(this.browser); @@ -236,33 +209,27 @@ suite('Keyboard navigation on Blocks', function () { }); test('Losing focus cancels move', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'text_print_1'); - await this.browser.keys('m'); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, 'm'); chai.assert.isTrue(await isDragging(this.browser)); - await this.browser.keys(Key.Tab); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, Key.Tab); chai.assert.isFalse(await isDragging(this.browser)); }); }); suite('Keyboard navigation on Fields', function () { - // Setting timeout to unlimited as these tests take a longer time to run than most mocha test - this.timeout(0); + // Disable timeouts when non-zero PAUSE_TIME is used to watch tests run. + if (PAUSE_TIME) this.timeout(0); - // Setup Selenium for all of the tests + // Clear the workspace and load start blocks. suiteSetup(async function () { this.browser = await testSetup(testFileLocations.NAVIGATION_TEST_BLOCKS); }); test('Up from first field selects block', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlockField(this.browser, 'p5_canvas_1', 'WIDTH'); await this.browser.pause(PAUSE_TIME); await keyUp(this.browser); @@ -274,8 +241,6 @@ suite('Keyboard navigation on Fields', function () { }); test('Left from first field selects block', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlockField(this.browser, 'p5_canvas_1', 'WIDTH'); await this.browser.pause(PAUSE_TIME); await keyLeft(this.browser); @@ -287,8 +252,6 @@ suite('Keyboard navigation on Fields', function () { }); test('Right from first field selects second field', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlockField(this.browser, 'p5_canvas_1', 'WIDTH'); await this.browser.pause(PAUSE_TIME); await keyRight(this.browser); @@ -301,8 +264,6 @@ suite('Keyboard navigation on Fields', function () { }); test('Left from second field selects first field', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlockField(this.browser, 'p5_canvas_1', 'HEIGHT'); await this.browser.pause(PAUSE_TIME); await keyLeft(this.browser); @@ -315,8 +276,6 @@ suite('Keyboard navigation on Fields', function () { }); test('Right from second field selects next block', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlockField(this.browser, 'p5_canvas_1', 'HEIGHT'); await this.browser.pause(PAUSE_TIME); await keyRight(this.browser); @@ -327,8 +286,6 @@ suite('Keyboard navigation on Fields', function () { }); test('Down from field selects next block', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlockField(this.browser, 'p5_canvas_1', 'WIDTH'); await this.browser.pause(PAUSE_TIME); await keyDown(this.browser); @@ -339,8 +296,6 @@ suite('Keyboard navigation on Fields', function () { }); test("Down from field selects block's child block", async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlockField(this.browser, 'controls_repeat_1', 'TIMES'); await this.browser.pause(PAUSE_TIME); await keyDown(this.browser); @@ -354,8 +309,7 @@ suite('Keyboard navigation on Fields', function () { // Open a field editor dropdown await focusOnBlockField(this.browser, 'logic_boolean_1', 'BOOL'); await this.browser.pause(PAUSE_TIME); - await this.browser.keys(Key.Enter); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, Key.Enter); // Try to navigate to a different block await keyRight(this.browser); @@ -368,13 +322,12 @@ suite('Keyboard navigation on Fields', function () { // Open colour picker await focusOnBlockField(this.browser, 'colour_picker_1', 'COLOUR'); await this.browser.pause(PAUSE_TIME); - await this.browser.keys(Key.Enter); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, Key.Enter); // Move right to pick a new colour. await keyRight(this.browser); // Enter to choose. - await this.browser.keys(Key.Enter); + await sendKeyAndWait(this.browser, Key.Enter); // Focus seems to take longer than a single pause to settle. await this.browser.waitUntil( diff --git a/test/webdriverio/test/block_comment_test.ts b/test/webdriverio/test/block_comment_test.ts index e965f3ff..08e7af9e 100644 --- a/test/webdriverio/test/block_comment_test.ts +++ b/test/webdriverio/test/block_comment_test.ts @@ -13,14 +13,15 @@ import { sendKeyAndWait, testFileLocations, keyRight, + PAUSE_TIME, } from './test_setup.js'; import {Key} from 'webdriverio'; suite('Block comment navigation', function () { - // Setting timeout to unlimited as these tests take a longer time to run than most mocha test - this.timeout(0); + // Disable timeouts when non-zero PAUSE_TIME is used to watch tests run. + if (PAUSE_TIME) this.timeout(0); - // Setup Selenium for all of the tests + // Clear the workspace and load start blocks. setup(async function () { this.browser = await testSetup(testFileLocations.NAVIGATION_TEST_BLOCKS); await this.browser.execute(() => { diff --git a/test/webdriverio/test/clipboard_test.ts b/test/webdriverio/test/clipboard_test.ts index b9e9f42e..afe4253a 100644 --- a/test/webdriverio/test/clipboard_test.ts +++ b/test/webdriverio/test/clipboard_test.ts @@ -13,19 +13,19 @@ import { getBlockElementById, getSelectedBlockId, ElementWithId, - tabNavigateToWorkspace, focusOnBlock, focusOnBlockField, blockIsPresent, getFocusedBlockType, + sendKeyAndWait, } from './test_setup.js'; import {Key, KeyAction, PointerAction, WheelAction} from 'webdriverio'; suite('Clipboard test', function () { - // Setting timeout to unlimited as these tests take longer time to run - this.timeout(0); + // Disable timeouts when non-zero PAUSE_TIME is used to watch tests run. + if (PAUSE_TIME) this.timeout(0); - // Clear the workspace and load start blocks + // Clear the workspace and load start blocks. setup(async function () { this.browser = await testSetup(testFileLocations.BASE); await this.browser.pause(PAUSE_TIME); @@ -33,13 +33,11 @@ suite('Clipboard test', function () { test('Copy and paste while block selected', async function () { // Navigate to draw_circle_1. - await tabNavigateToWorkspace(this.browser); await focusOnBlock(this.browser, 'draw_circle_1'); // Copy and paste - await this.browser.keys([Key.Ctrl, 'c']); - await this.browser.keys([Key.Ctrl, 'v']); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, [Key.Ctrl, 'c']); + await sendKeyAndWait(this.browser, [Key.Ctrl, 'v']); const block = await getBlockElementById(this.browser, 'draw_circle_1'); const blocks = await getSameBlocks(this.browser, block); @@ -54,15 +52,13 @@ suite('Clipboard test', function () { test('Cut and paste while block selected', async function () { // Navigate to draw_circle_1. - await tabNavigateToWorkspace(this.browser); await focusOnBlock(this.browser, 'draw_circle_1'); const block = await getBlockElementById(this.browser, 'draw_circle_1'); // Cut and paste - await this.browser.keys([Key.Ctrl, 'x']); + await sendKeyAndWait(this.browser, [Key.Ctrl, 'x']); await block.waitForExist({reverse: true}); - await this.browser.keys([Key.Ctrl, 'v']); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, [Key.Ctrl, 'v']); const focusedType = await getFocusedBlockType(this.browser); @@ -117,11 +113,10 @@ suite('Clipboard test', function () { // Open a field editor await focusOnBlockField(this.browser, 'draw_circle_1_color', 'COLOUR'); await this.browser.pause(PAUSE_TIME); - await this.browser.keys(Key.Enter); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, Key.Enter); // Try to cut block while field editor is open - await this.browser.keys([Key.Ctrl, 'x']); + await sendKeyAndWait(this.browser, [Key.Ctrl, 'x']); // Block is not deleted chai.assert.isTrue( diff --git a/test/webdriverio/test/delete_test.ts b/test/webdriverio/test/delete_test.ts index 8564fabd..0503b45b 100644 --- a/test/webdriverio/test/delete_test.ts +++ b/test/webdriverio/test/delete_test.ts @@ -15,23 +15,23 @@ import { testFileLocations, PAUSE_TIME, tabNavigateToWorkspace, + sendKeyAndWait, keyRight, focusOnBlockField, } from './test_setup.js'; import {Key} from 'webdriverio'; suite('Deleting Blocks', function () { - // Setting timeout to unlimited as these tests take a longer time to run than most mocha test - this.timeout(0); + // Disable timeouts when non-zero PAUSE_TIME is used to watch tests run. + if (PAUSE_TIME) this.timeout(0); + // Clear the workspace and load start blocks. setup(async function () { this.browser = await testSetup(testFileLocations.NAVIGATION_TEST_BLOCKS); await this.browser.pause(PAUSE_TIME); }); test('Deleting block selects parent block', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'controls_if_2'); await this.browser.pause(PAUSE_TIME); @@ -39,8 +39,7 @@ suite('Deleting Blocks', function () { .expect(await blockIsPresent(this.browser, 'controls_if_2')) .equal(true); - await this.browser.keys(Key.Backspace); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, Key.Backspace); chai .expect(await blockIsPresent(this.browser, 'controls_if_2')) @@ -52,8 +51,6 @@ suite('Deleting Blocks', function () { }); test('Cutting block selects parent block', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'controls_if_2'); await this.browser.pause(PAUSE_TIME); @@ -61,8 +58,7 @@ suite('Deleting Blocks', function () { .expect(await blockIsPresent(this.browser, 'controls_if_2')) .equal(true); - await this.browser.keys([Key.Ctrl, 'x']); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, [Key.Ctrl, 'x']); chai .expect(await blockIsPresent(this.browser, 'controls_if_2')) @@ -74,8 +70,6 @@ suite('Deleting Blocks', function () { }); test('Deleting block also deletes children and inputs', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'controls_if_2'); await this.browser.pause(PAUSE_TIME); @@ -84,8 +78,7 @@ suite('Deleting Blocks', function () { .equal(true); chai.expect(await blockIsPresent(this.browser, 'text_print_1')).equal(true); - await this.browser.keys(Key.Backspace); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, Key.Backspace); chai .expect(await blockIsPresent(this.browser, 'logic_boolean_1')) @@ -96,8 +89,6 @@ suite('Deleting Blocks', function () { }); test('Cutting block also removes children and inputs', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'controls_if_2'); await this.browser.pause(PAUSE_TIME); @@ -106,8 +97,7 @@ suite('Deleting Blocks', function () { .equal(true); chai.expect(await blockIsPresent(this.browser, 'text_print_1')).equal(true); - await this.browser.keys([Key.Ctrl, 'x']); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, [Key.Ctrl, 'x']); chai .expect(await blockIsPresent(this.browser, 'logic_boolean_1')) @@ -118,8 +108,6 @@ suite('Deleting Blocks', function () { }); test('Deleting inline input selects parent block', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'logic_boolean_1'); await this.browser.pause(PAUSE_TIME); @@ -127,8 +115,7 @@ suite('Deleting Blocks', function () { .expect(await blockIsPresent(this.browser, 'logic_boolean_1')) .equal(true); - await this.browser.keys(Key.Backspace); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, Key.Backspace); chai .expect(await blockIsPresent(this.browser, 'logic_boolean_1')) @@ -140,8 +127,6 @@ suite('Deleting Blocks', function () { }); test('Cutting inline input selects parent block', async function () { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'logic_boolean_1'); await this.browser.pause(PAUSE_TIME); @@ -149,8 +134,7 @@ suite('Deleting Blocks', function () { .expect(await blockIsPresent(this.browser, 'logic_boolean_1')) .equal(true); - await this.browser.keys([Key.Ctrl, 'x']); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, [Key.Ctrl, 'x']); chai .expect(await blockIsPresent(this.browser, 'logic_boolean_1')) @@ -176,16 +160,13 @@ suite('Deleting Blocks', function () { // Move to flyout. await keyRight(this.browser); // Select number block. - await this.browser.keys(Key.Enter); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, Key.Enter); // Confirm move. - await this.browser.keys(Key.Enter); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, Key.Enter); chai.assert.equal('math_number', await getFocusedBlockType(this.browser)); - await this.browser.keys(Key.Backspace); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, Key.Backspace); chai.assert.equal( await getCurrentFocusedBlockId(this.browser), @@ -203,16 +184,13 @@ suite('Deleting Blocks', function () { // Move to flyout. await keyRight(this.browser); // Select number block. - await this.browser.keys(Key.Enter); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, Key.Enter); // Confirm move. - await this.browser.keys(Key.Enter); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, Key.Enter); chai.assert.equal('math_number', await getFocusedBlockType(this.browser)); - await this.browser.keys([Key.Ctrl, 'x']); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, [Key.Ctrl, 'x']); chai.assert.equal( await getCurrentFocusedBlockId(this.browser), @@ -224,11 +202,10 @@ suite('Deleting Blocks', function () { // Open a field editor await focusOnBlockField(this.browser, 'colour_picker_1', 'COLOUR'); await this.browser.pause(PAUSE_TIME); - await this.browser.keys(Key.Enter); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, Key.Enter); // Try to delete block while field editor is open - await this.browser.keys(Key.Backspace); + await sendKeyAndWait(this.browser, Key.Backspace); // Block is not deleted chai.assert.isTrue(await blockIsPresent(this.browser, 'colour_picker_1')); diff --git a/test/webdriverio/test/duplicate_test.ts b/test/webdriverio/test/duplicate_test.ts index 2cef7808..93e33a4c 100644 --- a/test/webdriverio/test/duplicate_test.ts +++ b/test/webdriverio/test/duplicate_test.ts @@ -14,13 +14,14 @@ import { tabNavigateToWorkspace, testFileLocations, testSetup, + sendKeyAndWait, } from './test_setup.js'; suite('Duplicate test', function () { - // Setting timeout to unlimited as these tests take longer time to run - this.timeout(0); + // Disable timeouts when non-zero PAUSE_TIME is used to watch tests run. + if (PAUSE_TIME) this.timeout(0); - // Clear the workspace and load start blocks + // Clear the workspace and load start blocks. setup(async function () { this.browser = await testSetup(testFileLocations.BASE); await this.browser.pause(PAUSE_TIME); @@ -32,8 +33,7 @@ suite('Duplicate test', function () { await focusOnBlock(this.browser, 'draw_circle_1'); // Duplicate - await this.browser.keys('d'); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, 'd'); // Check a different block of the same type has focus. chai.assert.notEqual( @@ -65,7 +65,7 @@ suite('Duplicate test', function () { await this.browser.pause(PAUSE_TIME); // Duplicate. - await this.browser.keys('d'); + await sendKeyAndWait(this.browser, 'd'); // Assert we have two comments with the same text. const commentTexts = await this.browser.execute(() => diff --git a/test/webdriverio/test/flyout_test.ts b/test/webdriverio/test/flyout_test.ts index b6beacf8..769a895b 100644 --- a/test/webdriverio/test/flyout_test.ts +++ b/test/webdriverio/test/flyout_test.ts @@ -14,13 +14,17 @@ import { keyDown, tabNavigateBackward, tabNavigateToWorkspace, + sendKeyAndWait, keyRight, getCurrentFocusNodeId, getCurrentFocusedBlockId, } from './test_setup.js'; suite('Toolbox and flyout test', function () { - // Clear the workspace and load start blocks + // Disable timeouts when non-zero PAUSE_TIME is used to watch tests run. + if (PAUSE_TIME) this.timeout(0); + + // Clear the workspace and load start blocks. setup(async function () { this.browser = await testSetup(testFileLocations.BASE); await this.browser.pause(PAUSE_TIME); @@ -164,7 +168,7 @@ suite('Toolbox and flyout test', function () { test('Tabbing to the workspace after selecting flyout block via workspace toolbox shortcut should close the flyout', async function () { await tabNavigateToWorkspace(this.browser); - await this.browser.keys('t'); + await sendKeyAndWait(this.browser, 't'); await keyRight(this.browser); await tabNavigateForward(this.browser); diff --git a/test/webdriverio/test/insert_test.ts b/test/webdriverio/test/insert_test.ts index 7563f65e..e2305f97 100644 --- a/test/webdriverio/test/insert_test.ts +++ b/test/webdriverio/test/insert_test.ts @@ -14,6 +14,7 @@ import { tabNavigateToWorkspace, testFileLocations, testSetup, + sendKeyAndWait, keyRight, getCurrentFocusedBlockId, blockIsPresent, @@ -22,10 +23,10 @@ import { } from './test_setup.js'; suite('Insert test', function () { - // Setting timeout to unlimited as these tests take longer time to run - this.timeout(0); + // Disable timeouts when non-zero PAUSE_TIME is used to watch tests run. + if (PAUSE_TIME) this.timeout(0); - // Clear the workspace and load start blocks + // Clear the workspace and load start blocks. setup(async function () { this.browser = await testSetup(testFileLocations.BASE); await this.browser.pause(PAUSE_TIME); @@ -36,15 +37,15 @@ suite('Insert test', function () { await tabNavigateToWorkspace(this.browser); await focusOnBlock(this.browser, 'draw_circle_1'); // Insert 'if' block - await this.browser.keys('t'); + await sendKeyAndWait(this.browser, 't'); await keyRight(this.browser); - await this.browser.keys(Key.Enter); + await sendKeyAndWait(this.browser, Key.Enter); chai.assert.equal('controls_if', await getFocusedBlockType(this.browser)); const ifId = await getCurrentFocusedBlockId(this.browser); chai.assert.ok(ifId); // Cancel - await this.browser.keys(Key.Escape); + await sendKeyAndWait(this.browser, Key.Escape); chai.assert.isFalse(await blockIsPresent(this.browser, ifId)); }); @@ -52,17 +53,17 @@ suite('Insert test', function () { test('Insert and cancel with workspace selection', async function () { // Navigate to workspace. await tabNavigateToWorkspace(this.browser); - await this.browser.keys('w'); + await sendKeyAndWait(this.browser, 'w'); // Insert 'if' block - await this.browser.keys('t'); + await sendKeyAndWait(this.browser, 't'); await keyRight(this.browser); - await this.browser.keys(Key.Enter); + await sendKeyAndWait(this.browser, Key.Enter); chai.assert.equal('controls_if', await getFocusedBlockType(this.browser)); const ifId = await getCurrentFocusedBlockId(this.browser); chai.assert.ok(ifId); // Cancel - await this.browser.keys(Key.Escape); + await sendKeyAndWait(this.browser, Key.Escape); chai.assert.isFalse(await blockIsPresent(this.browser, ifId)); }); @@ -76,9 +77,9 @@ suite('Insert test', function () { // Move to flyout. await keyRight(this.browser); // Select Function block. - await this.browser.keys(Key.Enter); + await sendKeyAndWait(this.browser, Key.Enter); // Confirm move. - await this.browser.keys(Key.Enter); + await sendKeyAndWait(this.browser, Key.Enter); chai.assert.equal( 'procedures_defnoreturn', @@ -92,9 +93,9 @@ suite('Insert test', function () { // Insert 'if' block await keyRight(this.browser); // Choose. - await this.browser.keys(Key.Enter); + await sendKeyAndWait(this.browser, Key.Enter); // Confirm position. - await this.browser.keys(Key.Enter); + await sendKeyAndWait(this.browser, Key.Enter); // Assert inserted inside first block p5_setup not at top-level. chai.assert.equal('controls_if', await getFocusedBlockType(this.browser)); diff --git a/test/webdriverio/test/keyboard_mode_test.ts b/test/webdriverio/test/keyboard_mode_test.ts index d022c69a..8a362182 100644 --- a/test/webdriverio/test/keyboard_mode_test.ts +++ b/test/webdriverio/test/keyboard_mode_test.ts @@ -14,6 +14,7 @@ import { getBlockElementById, tabNavigateToWorkspace, clickBlock, + sendKeyAndWait, } from './test_setup.js'; import {Key} from 'webdriverio'; @@ -26,8 +27,8 @@ const isKeyboardNavigating = function (browser: WebdriverIO.Browser) { suite( 'Keyboard navigation mode set on mouse or keyboard interaction', function () { - // Setting timeout to unlimited as these tests take a longer time to run than most mocha tests - this.timeout(0); + // Disable timeouts when non-zero PAUSE_TIME is used to watch tests run. + if (PAUSE_TIME) this.timeout(0); setup(async function () { // Reload the page between tests @@ -46,8 +47,7 @@ suite( test('T to open toolbox enables keyboard mode', async function () { await this.browser.pause(PAUSE_TIME); - await this.browser.keys('t'); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, 't'); chai.assert.isTrue(await isKeyboardNavigating(this.browser)); }); @@ -55,15 +55,14 @@ suite( test('M for move mode enables keyboard mode', async function () { await focusOnBlock(this.browser, 'controls_if_2'); await this.browser.pause(PAUSE_TIME); - await this.browser.keys('m'); + await sendKeyAndWait(this.browser, 'm'); chai.assert.isTrue(await isKeyboardNavigating(this.browser)); }); test('W for workspace cursor enables keyboard mode', async function () { await this.browser.pause(PAUSE_TIME); - await this.browser.keys('w'); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, 'w'); chai.assert.isTrue(await isKeyboardNavigating(this.browser)); }); @@ -71,8 +70,7 @@ suite( test('X to disconnect enables keyboard mode', async function () { await focusOnBlock(this.browser, 'controls_if_2'); await this.browser.pause(PAUSE_TIME); - await this.browser.keys('x'); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, 'x'); chai.assert.isTrue(await isKeyboardNavigating(this.browser)); }); @@ -81,8 +79,7 @@ suite( // Make sure we're on a copyable block so that copy occurs await focusOnBlock(this.browser, 'controls_if_2'); await this.browser.pause(PAUSE_TIME); - await this.browser.keys([Key.Ctrl, 'c']); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, [Key.Ctrl, 'c']); chai.assert.isFalse(await isKeyboardNavigating(this.browser)); @@ -91,8 +88,7 @@ suite( }); await this.browser.pause(PAUSE_TIME); - await this.browser.keys([Key.Ctrl, 'c']); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, [Key.Ctrl, 'c']); chai.assert.isTrue(await isKeyboardNavigating(this.browser)); }); @@ -101,8 +97,7 @@ suite( // Make sure we're on a deletable block so that delete occurs await focusOnBlock(this.browser, 'controls_if_2'); await this.browser.pause(PAUSE_TIME); - await this.browser.keys(Key.Backspace); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, Key.Backspace); chai.assert.isFalse(await isKeyboardNavigating(this.browser)); @@ -113,8 +108,7 @@ suite( // Focus a different deletable block await focusOnBlock(this.browser, 'controls_if_1'); await this.browser.pause(PAUSE_TIME); - await this.browser.keys(Key.Backspace); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, Key.Backspace); chai.assert.isTrue(await isKeyboardNavigating(this.browser)); }); diff --git a/test/webdriverio/test/move_test.ts b/test/webdriverio/test/move_test.ts index 3a603e26..9de0ee19 100644 --- a/test/webdriverio/test/move_test.ts +++ b/test/webdriverio/test/move_test.ts @@ -18,10 +18,11 @@ import { } from './test_setup.js'; suite('Move tests', function () { - // Setting timeout to unlimited as these tests take longer time to run - this.timeout(0); + // Increase timeout to 10s for this longer test (but disable + // timeouts if when non-zero PAUSE_TIME is used to watch tests) run. + this.timeout(PAUSE_TIME ? 0 : 10000); - // Clear the workspace and load start blocks + // Clear the workspace and load start blocks. setup(async function () { this.browser = await testSetup(testFileLocations.MOVE_TEST_BLOCKS); await this.browser.pause(PAUSE_TIME); @@ -50,7 +51,7 @@ suite('Move tests', function () { chai.assert(info.nextId, 'selected block has no next block'); // Start move. - await this.browser.keys('m'); + await sendKeyAndWait(this.browser, 'm'); // Check that the moving block has nothing connected it its // next/previous connections, and same thing connected to value @@ -85,7 +86,7 @@ suite('Move tests', function () { ); // Abort move. - await this.browser.keys(Key.Escape); + await sendKeyAndWait(this.browser, Key.Escape); } }); @@ -110,7 +111,7 @@ suite('Move tests', function () { chai.assert(info.valueId, 'selected block has no child value block'); // Start move. - await this.browser.keys('m'); + await sendKeyAndWait(this.browser, 'm'); // Check that the moving block has nothing connected it its // next/previous connections, and same thing connected to value @@ -144,7 +145,7 @@ suite('Move tests', function () { ); // Abort move. - await this.browser.keys(Key.Escape); + await sendKeyAndWait(this.browser, Key.Escape); } }); @@ -168,7 +169,7 @@ suite('Move tests', function () { await tabNavigateToWorkspace(this.browser); await focusOnBlock(this.browser, BLOCK); const startCoordinate = await getCoordinate(this.browser, BLOCK); - await this.browser.keys('m'); + await sendKeyAndWait(this.browser, 'm'); // Check constrained moves have no effect. await keyDown(this.browser, 5); @@ -201,7 +202,7 @@ suite('Move tests', function () { } // Abort move. - await this.browser.keys(Key.Escape); + await sendKeyAndWait(this.browser, Key.Escape); }); }); diff --git a/test/webdriverio/test/mutator_test.ts b/test/webdriverio/test/mutator_test.ts index 16db298a..3e03c576 100644 --- a/test/webdriverio/test/mutator_test.ts +++ b/test/webdriverio/test/mutator_test.ts @@ -15,16 +15,17 @@ import { testFileLocations, PAUSE_TIME, tabNavigateToWorkspace, + sendKeyAndWait, keyRight, keyDown, } from './test_setup.js'; import {Key} from 'webdriverio'; suite('Mutator navigation', function () { - // Setting timeout to unlimited as these tests take a longer time to run than most mocha test - this.timeout(0); + // Disable timeouts when non-zero PAUSE_TIME is used to watch tests run. + if (PAUSE_TIME) this.timeout(0); - // Setup Selenium for all of the tests + // Clear the workspace and load start blocks. setup(async function () { this.browser = await testSetup(testFileLocations.NAVIGATION_TEST_BLOCKS); this.openMutator = async () => { @@ -35,8 +36,7 @@ suite('Mutator navigation', function () { // Navigate to the mutator icon await keyRight(this.browser); // Activate the icon - await this.browser.keys(Key.Enter); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, Key.Enter); }; }); @@ -54,8 +54,7 @@ suite('Mutator navigation', function () { test('Escape dismisses mutator', async function () { await this.openMutator(); - await this.browser.keys(Key.Escape); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, Key.Escape); // Main workspace should be the focused tree (since mutator workspace is gone) const mainWorkspaceFocused = await focusedTreeIsMainWorkspace(this.browser); @@ -75,11 +74,9 @@ suite('Mutator navigation', function () { test('Escape in the mutator flyout focuses the mutator workspace', async function () { await this.openMutator(); // Focus the flyout - await this.browser.keys('t'); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, 't'); // Hit escape to return focus to the mutator workspace - await this.browser.keys(Key.Escape); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, Key.Escape); // The "if" placeholder block in the mutator should be focused const focusedBlockType = await getFocusedBlockType(this.browser); chai.assert.equal(focusedBlockType, 'controls_if_if'); @@ -87,8 +84,7 @@ suite('Mutator navigation', function () { test('T focuses the mutator flyout', async function () { await this.openMutator(); - await this.browser.keys('t'); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, 't'); // The "else if" block in the mutator flyout should be focused const focusedBlockType = await getFocusedBlockType(this.browser); @@ -97,16 +93,14 @@ suite('Mutator navigation', function () { test('Blocks can be inserted from the mutator flyout', async function () { await this.openMutator(); - await this.browser.keys('t'); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, 't'); // Navigate down to the second block in the flyout await keyDown(this.browser); await this.browser.pause(PAUSE_TIME); // Hit enter to enter insert mode - await this.browser.keys(Key.Enter); - await this.browser.pause(PAUSE_TIME); + await sendKeyAndWait(this.browser, Key.Enter); // Hit enter again to lock it into place on the connection - await this.browser.keys(Key.Enter); + await sendKeyAndWait(this.browser, Key.Enter); const topBlocks = await this.browser.execute(() => { const focusedTree = Blockly.getFocusManager().getFocusedTree(); diff --git a/test/webdriverio/test/scroll_test.ts b/test/webdriverio/test/scroll_test.ts index 305ca31c..ac64b6d8 100644 --- a/test/webdriverio/test/scroll_test.ts +++ b/test/webdriverio/test/scroll_test.ts @@ -8,6 +8,7 @@ import * as Blockly from 'blockly'; import * as chai from 'chai'; import {Key} from 'webdriverio'; import { + sendKeyAndWait, keyDown, keyRight, PAUSE_TIME, @@ -17,25 +18,43 @@ import { } from './test_setup.js'; suite('Scrolling into view', function () { - // Setting timeout to unlimited as these tests take longer time to run - this.timeout(0); + // Disable timeouts when non-zero PAUSE_TIME is used to watch tests run. + if (PAUSE_TIME) this.timeout(0); - // Clear the workspace and load start blocks - setup(async function () { + // Resize browser to provide predictable small window size for scrolling. + // + // N.B. that this is called only one per suite, not once per test. + suiteSetup(async function () { this.browser = await testSetup(testFileLocations.BASE); - // Predictable small window size for scrolling. + this.windowSize = await this.browser.getWindowSize(); await this.browser.setWindowSize(800, 600); await this.browser.pause(PAUSE_TIME); }); + // Restore original browser window size. + suiteTeardown(async function () { + await this.browser.setWindowSize( + this.windowSize.width, + this.windowSize.height, + ); + }); + + // Clear the workspace and load start blocks. + setup(async function () { + await testSetup(testFileLocations.BASE); + }); + test('Insert scrolls new block into view', async function () { + // Increase timeout to 10s for this longer test. + this.timeout(PAUSE_TIME ? 0 : 10000); + await tabNavigateToWorkspace(this.browser); // Separate the two top-level blocks by moving p5_draw_1 further down. await keyDown(this.browser, 3); - await this.browser.keys('m'); - await this.browser.keys([Key.Alt, ...new Array(25).fill(Key.ArrowDown)]); - await this.browser.keys(Key.Enter); + await sendKeyAndWait(this.browser, 'm'); + await sendKeyAndWait(this.browser, [Key.Alt, Key.ArrowDown], 25); + await sendKeyAndWait(this.browser, Key.Enter); // Scroll back up, leaving cursor on the draw block out of the viewport. await this.browser.execute(() => { const workspace = Blockly.getMainWorkspace() as Blockly.WorkspaceSvg; @@ -47,10 +66,9 @@ suite('Scrolling into view', function () { }); // Insert and confirm the test block which should be scrolled into view. - await this.browser.keys('t'); + await sendKeyAndWait(this.browser, 't'); await keyRight(this.browser); - await this.browser.keys(Key.Enter); - await this.browser.keys(Key.Enter); + await sendKeyAndWait(this.browser, Key.Enter, 2); // Assert new block has been scrolled into the viewport. await this.browser.pause(PAUSE_TIME); diff --git a/test/webdriverio/test/stack_navigation.ts b/test/webdriverio/test/stack_navigation.ts index 08c9942f..2699e45d 100644 --- a/test/webdriverio/test/stack_navigation.ts +++ b/test/webdriverio/test/stack_navigation.ts @@ -12,13 +12,11 @@ import { tabNavigateToWorkspace, testFileLocations, testSetup, + sendKeyAndWait, } from './test_setup.js'; suite('Stack navigation', function () { - // Setting timeout to unlimited as these tests take longer time to run - this.timeout(0); - - // Clear the workspace and load start blocks + // Clear the workspace and load start blocks. setup(async function () { this.browser = await testSetup(testFileLocations.COMMENTS); await this.browser.pause(PAUSE_TIME); @@ -30,17 +28,17 @@ suite('Stack navigation', function () { 'p5_setup_1', await getCurrentFocusedBlockId(this.browser), ); - await this.browser.keys('n'); + await sendKeyAndWait(this.browser, 'n'); chai.assert.equal( 'p5_draw_1', await getCurrentFocusedBlockId(this.browser), ); - await this.browser.keys('n'); + await sendKeyAndWait(this.browser, 'n'); chai.assert.equal( 'workspace_comment_1', await getCurrentFocusNodeId(this.browser), ); - await this.browser.keys('n'); + await sendKeyAndWait(this.browser, 'n'); // Looped around. chai.assert.equal( 'p5_setup_1', @@ -54,18 +52,18 @@ suite('Stack navigation', function () { 'p5_setup_1', await getCurrentFocusedBlockId(this.browser), ); - await this.browser.keys('b'); + await sendKeyAndWait(this.browser, 'b'); // Looped to bottom. chai.assert.equal( 'workspace_comment_1', await getCurrentFocusNodeId(this.browser), ); - await this.browser.keys('b'); + await sendKeyAndWait(this.browser, 'b'); chai.assert.equal( 'p5_draw_1', await getCurrentFocusedBlockId(this.browser), ); - await this.browser.keys('b'); + await sendKeyAndWait(this.browser, 'b'); chai.assert.equal( 'p5_setup_1', await getCurrentFocusedBlockId(this.browser), diff --git a/test/webdriverio/test/styling_test.ts b/test/webdriverio/test/styling_test.ts index efa2514b..c2153f27 100644 --- a/test/webdriverio/test/styling_test.ts +++ b/test/webdriverio/test/styling_test.ts @@ -18,10 +18,10 @@ import { import * as chai from 'chai'; suite('Styling test', function () { - // Setting timeout to unlimited as these tests take longer time to run - this.timeout(0); + // Disable timeouts when non-zero PAUSE_TIME is used to watch tests run. + if (PAUSE_TIME) this.timeout(0); - // Clear the workspace and load start blocks + // Clear the workspace and load start blocks. setup(async function () { this.browser = await testSetup(testFileLocations.BASE); await this.browser.pause(PAUSE_TIME); diff --git a/test/webdriverio/test/test_setup.ts b/test/webdriverio/test/test_setup.ts index e5d47aa7..ccba37c1 100644 --- a/test/webdriverio/test/test_setup.ts +++ b/test/webdriverio/test/test_setup.ts @@ -11,9 +11,9 @@ * This file is to be used in the suiteSetup for any automated fuctional test. * * Note: In this file many functions return browser elements that can - * be clicked or otherwise interacted with through Selenium WebDriver. These + * be clicked or otherwise interacted with through WebdriverIO. These * elements are not the raw HTML and SVG elements on the page; they are - * identifiers that Selenium can use to find those elements. + * identifiers that WebdriverIO can use to find those elements. */ import * as Blockly from 'blockly'; @@ -27,16 +27,30 @@ import {fileURLToPath} from 'url'; let driver: webdriverio.Browser | null = null; /** - * The default amount of time to wait during a test. Increase this to make - * tests easier to watch; decrease it to make tests run faster. + * The default amount of time to wait during a test, in ms. Increase + * this to make tests easier to watch; decrease it to make tests run + * faster. + * + * The _test.js files in this directory are set up to disable timeouts + * automatically when PAUSE_TIME is set to a nonzero value via + * + * if (PAUSE_TIME) this.timeout(0); + * + * at the top of each suite. + * + * Tests should pass reliably even with this set to zero; use one of + * the browser.wait* functions if you need your test to wait for + * something to happen after sending input. */ -export const PAUSE_TIME = 50; +export const PAUSE_TIME = 0; /** - * Start up the test page. This should only be done once, to avoid - * constantly popping browser windows open and closed. + * Start up WebdriverIO and load the test page. This should only be + * done once, to avoid constantly popping browser windows open and + * closed. * - * @returns A Promise that resolves to a webdriverIO browser that tests can manipulate. + * @returns A Promise that resolves to a WebdriverIO browser that + * tests can manipulate. */ export async function driverSetup(): Promise { const options = { @@ -68,14 +82,14 @@ export async function driverSetup(): Promise { // https://github.com/google/blockly/issues/5345 for details. options.capabilities['goog:chromeOptions'].args.push('--disable-gpu'); } - // Use Selenium to bring up the page + // Use webdriver to bring up the page console.log('Starting webdriverio...'); driver = await webdriverio.remote(options); return driver; } /** - * End the webdriverIO session. + * End the WebdriverIO session. * * @return A Promise that resolves after the actions have been completed. */ @@ -90,7 +104,8 @@ export async function driverTeardown() { * * @param playgroundUrl The URL to open for the test, which should be * a Blockly playground with a workspace. - * @returns A Promise that resolves to a webdriverIO browser that tests can manipulate. + * @returns A Promise that resolves to a WebdriverIO browser that + * tests can manipulate. */ export async function testSetup( playgroundUrl: string, @@ -440,8 +455,14 @@ export async function tabNavigateToWorkspace( hasToolbox = true, hasFlyout = true, ) { - // Navigate past the initial pre-injection focusable div element. - await tabNavigateForward(browser); + // Move focus to initial pre-injection focusable div element. + // + // Ideally we'd just reset focus state to the state it is in when + // the document initially loads (and then send one tab), but alas + // there's no straightforward way to do that; see + // https://stackoverflow.com/q/51518855/4969945 + await browser.execute(() => document.getElementById('focusableDiv')?.focus()); + // Navigate to workspace. if (hasToolbox) await tabNavigateForward(browser); if (hasFlyout) await tabNavigateForward(browser); await tabNavigateForward(browser); // Tab to the workspace itself. @@ -679,7 +700,7 @@ export async function clickBlock( findableId, ); - // In the test context, get the Webdriverio Element that we've identified. + // In the test context, get the WebdriverIO Element that we've identified. const elem = await browser.$(`#${findableId}`); await elem.click(clickOptions); diff --git a/test/webdriverio/test/toast_test.ts b/test/webdriverio/test/toast_test.ts index cd4721d5..bf774491 100644 --- a/test/webdriverio/test/toast_test.ts +++ b/test/webdriverio/test/toast_test.ts @@ -9,6 +9,10 @@ import * as Blockly from 'blockly/core'; import {PAUSE_TIME, testFileLocations, testSetup} from './test_setup.js'; suite('HTML toasts', function () { + // Disable timeouts when non-zero PAUSE_TIME is used to watch tests run. + if (PAUSE_TIME) this.timeout(0); + + // Clear the workspace and load start blocks. setup(async function () { this.browser = await testSetup(testFileLocations.BASE); await this.browser.pause(PAUSE_TIME); diff --git a/test/webdriverio/test/workspace_comment_test.ts b/test/webdriverio/test/workspace_comment_test.ts index eb882e27..f97ddcde 100644 --- a/test/webdriverio/test/workspace_comment_test.ts +++ b/test/webdriverio/test/workspace_comment_test.ts @@ -19,14 +19,15 @@ import { keyDown, keyUp, contextMenuItems, + PAUSE_TIME, } from './test_setup.js'; import {Key} from 'webdriverio'; suite('Workspace comment navigation', function () { - // Setting timeout to unlimited as these tests take a longer time to run than most mocha test - this.timeout(0); + // Disable timeouts when non-zero PAUSE_TIME is used to watch tests run. + if (PAUSE_TIME) this.timeout(0); - // Setup Selenium for all of the tests + // Clear the workspace and load start blocks. setup(async function () { this.browser = await testSetup(testFileLocations.NAVIGATION_TEST_BLOCKS); [this.commentId1, this.commentId2] = await this.browser.execute(() => { From e438ba702535d8bbedbbea0f71859168ce0a30c5 Mon Sep 17 00:00:00 2001 From: John Nesky Date: Mon, 25 Aug 2025 12:01:46 -0700 Subject: [PATCH 06/19] chore: Added context menu item tests. (#575) * chore: Added context menu item tests. * Maybe clicking a different location fixes the github test failure. * Assigned the expected actions to constants at top of context menu test. * Fix shadow block action test in github action. * Fix shadow block action test in github action attempt 2. --- test/loadTestBlocks.js | 4 +- test/webdriverio/test/actions_test.ts | 207 ++++++++++++++++++-------- test/webdriverio/test/test_setup.ts | 20 ++- 3 files changed, 161 insertions(+), 70 deletions(-) diff --git a/test/loadTestBlocks.js b/test/loadTestBlocks.js index 14b1269e..3f5c56be 100644 --- a/test/loadTestBlocks.js +++ b/test/loadTestBlocks.js @@ -317,12 +317,12 @@ const moreBlocks = { 'next': { 'block': { 'type': 'text_print', - 'id': 'J`*)bq?#`_Vq^X(DQF2t', + 'id': 'text_print_1', 'inputs': { 'TEXT': { 'shadow': { 'type': 'text', - 'id': '6fW_sIt1t|63j}nPE1ge', + 'id': 'text_print_shadow_text_1', 'fields': { 'TEXT': 'abc', }, diff --git a/test/webdriverio/test/actions_test.ts b/test/webdriverio/test/actions_test.ts index 6b0a86c6..61d47401 100644 --- a/test/webdriverio/test/actions_test.ts +++ b/test/webdriverio/test/actions_test.ts @@ -7,10 +7,13 @@ import * as chai from 'chai'; import {Key} from 'webdriverio'; import { + clickBlock, contextMenuExists, moveToToolboxCategory, PAUSE_TIME, focusOnBlock, + focusWorkspace, + rightClickOnFlyoutBlockType, tabNavigateToWorkspace, testFileLocations, testSetup, @@ -19,6 +22,70 @@ import { contextMenuItems, } from './test_setup.js'; +const isDarwin = process.platform === 'darwin'; + +const blockActionsViaKeyboard = [ + {'text': 'Duplicate D'}, + {'text': 'Add Comment'}, + {'text': 'External Inputs'}, + {'text': 'Collapse Block'}, + {'text': 'Disable Block'}, + {'text': 'Delete 2 Blocks Delete'}, + {'text': 'Move Block M'}, + {'text': 'Edit Block contents Right'}, + {'text': isDarwin ? 'Cut ⌘ X' : 'Cut Ctrl + X'}, + {'text': isDarwin ? 'Copy ⌘ C' : 'Copy Ctrl + C'}, + {'disabled': true, 'text': isDarwin ? 'Paste ⌘ V' : 'Paste Ctrl + V'}, +]; + +const blockActionsViaMouse = [ + {'text': 'Duplicate D'}, + {'text': 'Add Comment'}, + {'text': 'External Inputs'}, + {'text': 'Collapse Block'}, + {'text': 'Disable Block'}, + {'text': 'Delete 2 Blocks Delete'}, + {'text': isDarwin ? 'Cut ⌘ X' : 'Cut Ctrl + X'}, + {'text': isDarwin ? 'Copy ⌘ C' : 'Copy Ctrl + C'}, + {'disabled': true, 'text': isDarwin ? 'Paste ⌘ V' : 'Paste Ctrl + V'}, +]; + +const shadowBlockActionsViaKeyboard = [ + {'text': 'Add Comment'}, + {'text': 'Collapse Block'}, + {'text': 'Disable Block'}, + {'text': 'Help'}, + {'text': 'Move Block M'}, + {'text': 'Edit Block contents Right'}, + {'disabled': true, 'text': isDarwin ? 'Cut ⌘ X' : 'Cut Ctrl + X'}, + {'text': isDarwin ? 'Copy ⌘ C' : 'Copy Ctrl + C'}, + {'disabled': true, 'text': isDarwin ? 'Paste ⌘ V' : 'Paste Ctrl + V'}, +]; + +const toolboxBlockActionsViaKeyboard = [ + {'text': 'Help'}, + {'disabled': true, 'text': 'Move Block M'}, + {'disabled': true, 'text': isDarwin ? 'Cut ⌘ X' : 'Cut Ctrl + X'}, + {'text': isDarwin ? 'Copy ⌘ C' : 'Copy Ctrl + C'}, +]; + +const flyoutBlockActionsViaMouse = [ + {'text': 'Help'}, + {'disabled': true, 'text': isDarwin ? 'Cut ⌘ X' : 'Cut Ctrl + X'}, + {'text': isDarwin ? 'Copy ⌘ C' : 'Copy Ctrl + C'}, +]; + +const workspaceActionsViaKeyboard = [ + {'disabled': true, 'text': 'Undo'}, + {'disabled': true, 'text': 'Redo'}, + {'text': 'Clean up Blocks'}, + {'text': 'Collapse Blocks'}, + {'disabled': true, 'text': 'Expand Blocks'}, + {'text': 'Delete 14 Blocks'}, + {'text': 'Add Comment'}, + {'disabled': true, 'text': isDarwin ? 'Paste ⌘ V' : 'Paste Ctrl + V'}, +]; + suite('Menus test', function () { // Disable timeouts when non-zero PAUSE_TIME is used to watch tests run. if (PAUSE_TIME) this.timeout(0); @@ -30,51 +97,47 @@ suite('Menus test', function () { // seconds. Allow 30s just in case. this.timeout(30000); - this.browser = await testSetup(testFileLocations.BASE); + this.browser = await testSetup(testFileLocations.MORE_BLOCKS); await this.browser.pause(PAUSE_TIME); }); - test('Menu on block', async function () { + test('Menu action via keyboard on block opens menu', async function () { // Navigate to draw_circle_1. await focusOnBlock(this.browser, 'draw_circle_1'); await this.browser.pause(PAUSE_TIME); await sendKeyAndWait(this.browser, [Key.Ctrl, Key.Return]); chai.assert.deepEqual( - process.platform === 'darwin' - ? [ - {'text': 'Duplicate D'}, - {'text': 'Add Comment'}, - {'text': 'External Inputs'}, - {'text': 'Collapse Block'}, - {'text': 'Disable Block'}, - {'text': 'Delete 2 Blocks Delete'}, - {'text': 'Move Block M'}, - {'text': 'Edit Block contents Right'}, - {'text': 'Cut ⌘ X'}, - {'text': 'Copy ⌘ C'}, - {'disabled': true, 'text': 'Paste ⌘ V'}, - ] - : [ - {'text': 'Duplicate D'}, - {'text': 'Add Comment'}, - {'text': 'External Inputs'}, - {'text': 'Collapse Block'}, - {'text': 'Disable Block'}, - {'text': 'Delete 2 Blocks Delete'}, - {'text': 'Move Block M'}, - {'text': 'Edit Block contents Right'}, - {'text': 'Cut Ctrl + X'}, - {'text': 'Copy Ctrl + C'}, - {'disabled': true, 'text': 'Paste Ctrl + V'}, - ], await contextMenuItems(this.browser), + blockActionsViaKeyboard, ); }); - test('Menu on block in the toolbox', async function () { - // Navigate to draw_circle_1. - await focusOnBlock(this.browser, 'draw_circle_1'); + test('Block menu via mouse displays expected items', async function () { + await tabNavigateToWorkspace(this.browser); + await clickBlock(this.browser, 'draw_circle_1', {button: 'right'}); + + chai.assert.deepEqual( + await contextMenuItems(this.browser), + blockActionsViaMouse, + ); + }); + + test('Shadow block menu via keyboard displays expected items', async function () { + await tabNavigateToWorkspace(this.browser); + await focusOnBlock(this.browser, 'text_print_1'); + await this.browser.keys(Key.ArrowRight); + await this.browser.keys([Key.Ctrl, Key.Return]); + await this.browser.pause(PAUSE_TIME); + + chai.assert.deepEqual( + await contextMenuItems(this.browser), + shadowBlockActionsViaKeyboard, + ); + }); + + test('Menu action on block in the toolbox', async function () { + await tabNavigateToWorkspace(this.browser); // Navigate to a toolbox category await moveToToolboxCategory(this.browser, 'Functions'); // Move to flyout. @@ -82,20 +145,24 @@ suite('Menus test', function () { await sendKeyAndWait(this.browser, [Key.Ctrl, Key.Return]); chai.assert.deepEqual( - process.platform === 'darwin' - ? [ - {'text': 'Help'}, - {'disabled': true, 'text': 'Move Block M'}, - {'disabled': true, 'text': 'Cut ⌘ X'}, - {'text': 'Copy ⌘ C'}, - ] - : [ - {'text': 'Help'}, - {'disabled': true, 'text': 'Move Block M'}, - {'disabled': true, 'text': 'Cut Ctrl + X'}, - {'text': 'Copy Ctrl + C'}, - ], await contextMenuItems(this.browser), + toolboxBlockActionsViaKeyboard, + ); + }); + + test('Flyout block menu via mouse displays expected items', async function () { + await tabNavigateToWorkspace(this.browser); + // Navigate to a toolbox category + await moveToToolboxCategory(this.browser, 'Math'); + // Move to flyout. + await keyRight(this.browser); + await this.browser.pause(PAUSE_TIME); + await rightClickOnFlyoutBlockType(this.browser, 'math_number'); + await this.browser.pause(PAUSE_TIME); + + chai.assert.deepEqual( + await contextMenuItems(this.browser), + flyoutBlockActionsViaMouse, ); }); @@ -106,28 +173,8 @@ suite('Menus test', function () { await sendKeyAndWait(this.browser, [Key.Ctrl, Key.Return]); chai.assert.deepEqual( - process.platform === 'darwin' - ? [ - {'disabled': true, 'text': 'Undo'}, - {'disabled': true, 'text': 'Redo'}, - {'text': 'Clean up Blocks'}, - {'text': 'Collapse Blocks'}, - {'disabled': true, 'text': 'Expand Blocks'}, - {'text': 'Delete 4 Blocks'}, - {'text': 'Add Comment'}, - {'disabled': true, 'text': 'Paste ⌘ V'}, - ] - : [ - {'disabled': true, 'text': 'Undo'}, - {'disabled': true, 'text': 'Redo'}, - {'text': 'Clean up Blocks'}, - {'text': 'Collapse Blocks'}, - {'disabled': true, 'text': 'Expand Blocks'}, - {'text': 'Delete 4 Blocks'}, - {'text': 'Add Comment'}, - {'disabled': true, 'text': 'Paste Ctrl + V'}, - ], await contextMenuItems(this.browser), + workspaceActionsViaKeyboard, ); }); @@ -143,4 +190,32 @@ suite('Menus test', function () { 'The menu should not be openable during a move', ); }); + + test('Escape key dismisses menu', async function () { + await tabNavigateToWorkspace(this.browser); + await focusOnBlock(this.browser, 'draw_circle_1'); + await this.browser.pause(PAUSE_TIME); + await this.browser.keys([Key.Ctrl, Key.Return]); + await this.browser.pause(PAUSE_TIME); + await this.browser.keys(Key.Escape); + await this.browser.pause(PAUSE_TIME); + + chai.assert.isTrue( + await contextMenuExists(this.browser, 'Duplicate', /* reverse= */ true), + 'The menu should be closed', + ); + }); + + test('Clicking workspace dismisses menu', async function () { + await tabNavigateToWorkspace(this.browser); + await clickBlock(this.browser, 'draw_circle_1', {button: 'right'}); + await this.browser.pause(PAUSE_TIME); + await focusWorkspace(this.browser); + await this.browser.pause(PAUSE_TIME); + + chai.assert.isTrue( + await contextMenuExists(this.browser, 'Duplicate', /* reverse= */ true), + 'The menu should be closed', + ); + }); }); diff --git a/test/webdriverio/test/test_setup.ts b/test/webdriverio/test/test_setup.ts index ccba37c1..4d77fbcb 100644 --- a/test/webdriverio/test/test_setup.ts +++ b/test/webdriverio/test/test_setup.ts @@ -153,6 +153,8 @@ export const testFileLocations = { new URLSearchParams({scenario: 'navigationTestBlocks'}), ), // eslint-disable-next-line @typescript-eslint/naming-convention + MORE_BLOCKS: createTestUrl(new URLSearchParams({scenario: 'moreBlocks'})), + // eslint-disable-next-line @typescript-eslint/naming-convention MOVE_TEST_BLOCKS: createTestUrl( new URLSearchParams({scenario: 'moveTestBlocks'}), ), @@ -188,7 +190,7 @@ export async function focusWorkspace(browser: WebdriverIO.Browser) { const workspaceElement = await browser.$( '#blocklyDiv > div > svg.blocklySvg > g', ); - await workspaceElement.click(); + await workspaceElement.click({x: 100}); } /** @@ -573,7 +575,7 @@ export async function isDragging( } /** - * Returns the result of the specificied action precondition. + * Returns the result of the specified action precondition. * * @param browser The active WebdriverIO Browser object. * @param action The action to check the precondition for. @@ -710,3 +712,17 @@ export async function clickBlock( document.getElementById(elemId)?.removeAttribute('id'); }, findableId); } + +/** + * Right-clicks on a block with the provided type in the flyout. + * + * @param browser The active WebdriverIO Browser object. + * @param blockType The name of the type block to right click on. + */ +export async function rightClickOnFlyoutBlockType( + browser: WebdriverIO.Browser, + blockType: string, +) { + const elem = await browser.$(`.blocklyFlyout .${blockType}`); + await elem.click({button: 'right'}); +} From ec703af06d871d8912ed90d62c79b539d231dc26 Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Tue, 26 Aug 2025 14:10:31 +0100 Subject: [PATCH 07/19] refactor(tests): Remove unneeded tabNavigateToWorkspace calls (#701) The focusOnBlock function moves focus to the workspace regardless of where it was previously, so there's no need to call tabNavigateToWorkspace first. --- test/webdriverio/test/duplicate_test.ts | 1 - test/webdriverio/test/insert_test.ts | 2 -- test/webdriverio/test/move_test.ts | 4 ---- test/webdriverio/test/mutator_test.ts | 3 --- test/webdriverio/test/styling_test.ts | 3 --- 5 files changed, 13 deletions(-) diff --git a/test/webdriverio/test/duplicate_test.ts b/test/webdriverio/test/duplicate_test.ts index 93e33a4c..8498d32b 100644 --- a/test/webdriverio/test/duplicate_test.ts +++ b/test/webdriverio/test/duplicate_test.ts @@ -29,7 +29,6 @@ suite('Duplicate test', function () { test('Duplicate block', async function () { // Navigate to draw_circle_1. - await tabNavigateToWorkspace(this.browser); await focusOnBlock(this.browser, 'draw_circle_1'); // Duplicate diff --git a/test/webdriverio/test/insert_test.ts b/test/webdriverio/test/insert_test.ts index e2305f97..bf22f8f9 100644 --- a/test/webdriverio/test/insert_test.ts +++ b/test/webdriverio/test/insert_test.ts @@ -34,7 +34,6 @@ suite('Insert test', function () { test('Insert and cancel with block selection', async function () { // Navigate to draw_circle_1. - await tabNavigateToWorkspace(this.browser); await focusOnBlock(this.browser, 'draw_circle_1'); // Insert 'if' block await sendKeyAndWait(this.browser, 't'); @@ -70,7 +69,6 @@ suite('Insert test', function () { test('Insert C-shaped block with statement block selected', async function () { // Navigate to draw_circle_1. - await tabNavigateToWorkspace(this.browser); await focusOnBlock(this.browser, 'draw_circle_1'); await moveToToolboxCategory(this.browser, 'Functions'); diff --git a/test/webdriverio/test/move_test.ts b/test/webdriverio/test/move_test.ts index 9de0ee19..a32e34e7 100644 --- a/test/webdriverio/test/move_test.ts +++ b/test/webdriverio/test/move_test.ts @@ -10,7 +10,6 @@ import {Browser, Key} from 'webdriverio'; import { PAUSE_TIME, focusOnBlock, - tabNavigateToWorkspace, testFileLocations, testSetup, sendKeyAndWait, @@ -36,7 +35,6 @@ suite('Move tests', function () { test('Start moving statement blocks', async function () { for (let i = 1; i < 7; i++) { // Navigate to statement_. - await tabNavigateToWorkspace(this.browser); await focusOnBlock(this.browser, `statement_${i}`); // Get information about parent connection of selected block, @@ -96,7 +94,6 @@ suite('Move tests', function () { test('Start moving value blocks', async function () { for (let i = 1; i < 7; i++) { // Navigate to statement_. - await tabNavigateToWorkspace(this.browser); await focusOnBlock(this.browser, `value_${i}`); // Get information about parent connection of selected block, @@ -166,7 +163,6 @@ suite('Move tests', function () { }); // Navigate to unconnectable block, get initial coords and start move. - await tabNavigateToWorkspace(this.browser); await focusOnBlock(this.browser, BLOCK); const startCoordinate = await getCoordinate(this.browser, BLOCK); await sendKeyAndWait(this.browser, 'm'); diff --git a/test/webdriverio/test/mutator_test.ts b/test/webdriverio/test/mutator_test.ts index 3e03c576..0d106e16 100644 --- a/test/webdriverio/test/mutator_test.ts +++ b/test/webdriverio/test/mutator_test.ts @@ -14,7 +14,6 @@ import { testSetup, testFileLocations, PAUSE_TIME, - tabNavigateToWorkspace, sendKeyAndWait, keyRight, keyDown, @@ -29,8 +28,6 @@ suite('Mutator navigation', function () { setup(async function () { this.browser = await testSetup(testFileLocations.NAVIGATION_TEST_BLOCKS); this.openMutator = async () => { - await tabNavigateToWorkspace(this.browser); - await this.browser.pause(PAUSE_TIME); await focusOnBlock(this.browser, 'controls_if_1'); await this.browser.pause(PAUSE_TIME); // Navigate to the mutator icon diff --git a/test/webdriverio/test/styling_test.ts b/test/webdriverio/test/styling_test.ts index c2153f27..f226a5a5 100644 --- a/test/webdriverio/test/styling_test.ts +++ b/test/webdriverio/test/styling_test.ts @@ -81,7 +81,6 @@ suite('Styling test', function () { }); test('Workspace has only active tree style when move is in progress', async function () { - await tabNavigateToWorkspace(this.browser); await focusOnBlock(this.browser, 'set_background_color_1'); // Moves block to drag layer which requires different selectors. await sendKeyAndWait(this.browser, 'm'); @@ -91,7 +90,6 @@ suite('Styling test', function () { }); test('Workspace has only active tree style when widget has focus', async function () { - await tabNavigateToWorkspace(this.browser); await focusOnBlock(this.browser, 'create_canvas_1'); // Move to field. await keyRight(this.browser); @@ -103,7 +101,6 @@ suite('Styling test', function () { }); test('Workspace has only active tree style when dropdown has focus', async function () { - await tabNavigateToWorkspace(this.browser); await focusOnBlock(this.browser, 'set_background_color_1'); // Move to color block. await keyRight(this.browser); From 7df913db6328e115202e0158d02612be41b903a6 Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Tue, 26 Aug 2025 14:10:59 +0100 Subject: [PATCH 08/19] test(MoveActions): Test starting a move using the context menu (#700) * test(MoveActions): Test starting move using keyboard shortcuts Fixes #695. * refactor(tests): Make sendKeyAndWait faster if PAUSE_TIME === 0 Making a single webdriverio call is much faster when sending repeating keypresses; do that if we don't need to pause. --- test/webdriverio/test/move_test.ts | 17 +++++++++++++++-- test/webdriverio/test/test_setup.ts | 10 ++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/test/webdriverio/test/move_test.ts b/test/webdriverio/test/move_test.ts index a32e34e7..a8a34b39 100644 --- a/test/webdriverio/test/move_test.ts +++ b/test/webdriverio/test/move_test.ts @@ -14,6 +14,7 @@ import { testSetup, sendKeyAndWait, keyDown, + contextMenuItems, } from './test_setup.js'; suite('Move tests', function () { @@ -32,6 +33,8 @@ suite('Move tests', function () { // moved, with subsequent statement blocks below it in the stack // reattached to where the moving block was - i.e., that a stack // heal will occur. + // + // Also tests initating a move using the shortcut key. test('Start moving statement blocks', async function () { for (let i = 1; i < 7; i++) { // Navigate to statement_. @@ -48,7 +51,7 @@ suite('Move tests', function () { ); chai.assert(info.nextId, 'selected block has no next block'); - // Start move. + // Start move using keyboard shortcut. await sendKeyAndWait(this.browser, 'm'); // Check that the moving block has nothing connected it its @@ -91,6 +94,8 @@ suite('Move tests', function () { // When a move of a value block begins, it is expected that block // and all blocks connected to its inputs will be moved - i.e., that // a stack heal (really: unary operator chain heal) will NOT occur. + // + // Also tests initiating a move via the context menu. test('Start moving value blocks', async function () { for (let i = 1; i < 7; i++) { // Navigate to statement_. @@ -107,8 +112,16 @@ suite('Move tests', function () { ); chai.assert(info.valueId, 'selected block has no child value block'); - // Start move. + // Start move using context menu (using keyboard nav). + await sendKeyAndWait(this.browser, [Key.Ctrl, Key.Return]); await sendKeyAndWait(this.browser, 'm'); + await keyDown( + this.browser, + (await contextMenuItems(this.browser)).findIndex(({text}) => + text.includes('Move'), + ), + ); + await sendKeyAndWait(this.browser, Key.Return); // Check that the moving block has nothing connected it its // next/previous connections, and same thing connected to value diff --git a/test/webdriverio/test/test_setup.ts b/test/webdriverio/test/test_setup.ts index 4d77fbcb..93bec99a 100644 --- a/test/webdriverio/test/test_setup.ts +++ b/test/webdriverio/test/test_setup.ts @@ -554,9 +554,15 @@ export async function sendKeyAndWait( keys: string | string[], times = 1, ) { - for (let i = 0; i < times; i++) { + if (PAUSE_TIME === 0) { + // Send all keys in one call if no pauses needed. + keys = Array(times).fill(keys).flat(); await browser.keys(keys); - await browser.pause(PAUSE_TIME); + } else { + for (let i = 0; i < times; i++) { + await browser.keys(keys); + await browser.pause(PAUSE_TIME); + } } } From 2af65b837e65311af8f3867c4f7b489be4e2847a Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Wed, 27 Aug 2025 18:01:55 +0100 Subject: [PATCH 09/19] test(`Mover`): Add test for moving statement blocks (#704) * refactor(tests): Rename move test blocks to move start test blocks Also remove an unnecessary round trip through JSON.stringify + JSON.parse. * test(Mover): Introduce test blocks and suite for statement move tests Introduce moveStatementTestBlocks and suite 'Statement move tests', for testing movement of statement blocks. Move the existing test for constrained movement following unconstrained movement to the new suite. * test(Mover): Test constrained move of simple stack block left/right * refactor(tests): Refactor simple stack block move left/right Introduce a new helper function, testMove, that encapsulates the work needed to test moving a block through a constrained move, checking candidate connections and final position. Use this helper to reimplement the move left/right test as two separate tests. * test(Mover): Test constrained move of simple stack block up/down * docs(tests): Improve documentation of getFocusedNeighbourInfo * test(Mover): Also check index of candidateConnection.local Modify getConnectionCandidate to also return the index of candidateConnection.local in the list of connections returned from block.getConnections_(true), so we can check wich connection on the moving block is being used to connect. For the simple mover case this is not very interesting (it's normally index 0, for the previous connection, occasionally index 1 if the moving block will be the new top block in a stack) but this will be more important for tests involving moving more complexly-shaped blocks. * chore(tests): Add block with statment inputs to statement test blocks Add a new, E-shaped block with ID complex_mover to moveStatementTestBlocks, and update expected test results for statement move tests. * test(Mover): Test constrained move of stack block with statement inputs Note that this test verifies current behaviour which, due to bug #702, does not conform to the desired behaviour. There are TODOs to update tests to desired behaviour when bug is fixed. * docs(tests): Correct copied comment --- test/index.html | 7 +- test/loadTestBlocks.js | 135 ++++++++++++++- test/webdriverio/test/move_test.ts | 244 +++++++++++++++++++++++++++- test/webdriverio/test/test_setup.ts | 8 +- 4 files changed, 373 insertions(+), 21 deletions(-) diff --git a/test/index.html b/test/index.html index 212d0354..d64f6a64 100644 --- a/test/index.html +++ b/test/index.html @@ -108,7 +108,12 @@ - + + diff --git a/test/loadTestBlocks.js b/test/loadTestBlocks.js index 3f5c56be..da64cb05 100644 --- a/test/loadTestBlocks.js +++ b/test/loadTestBlocks.js @@ -570,7 +570,15 @@ const navigationTestBlocks = { }, }; -const moveTestBlocks = { +// The draw block contains a stack of statement blocks, each of which +// has a value input to which is connected a value expression block +// which itself has one or two inputs which have (non-shadow) simple +// value blocks connected. Each statement block will be selected in +// turn and then a move initiated (and then aborted). This is then +// repeated with the first level value blocks (those that are attached +// to the statement blocks). The second level value blocks are +// present to verify correct (lack of) heal behaviour. +const moveStartTestBlocks = { 'blocks': { 'languageVersion': 0, 'blocks': [ @@ -862,6 +870,112 @@ const moveTestBlocks = { }, }; +// A bunch of statement blocks. The blocks with IDs simple_mover and +// complex_mover will be (constrained-)moved up, down, left and right +// to verify that they visit all the expected candidate connections. +const moveStatementTestBlocks = { + 'blocks': { + 'languageVersion': 0, + 'blocks': [ + { + 'type': 'p5_setup', + 'id': 'p5_setup', + 'x': 75, + 'y': 75, + 'deletable': false, + 'inputs': { + 'STATEMENTS': { + 'block': { + 'type': 'p5_canvas', + 'id': 'p5_canvas', + 'deletable': false, + 'movable': false, + 'fields': { + 'WIDTH': 400, + 'HEIGHT': 400, + }, + 'next': { + 'block': { + 'type': 'draw_emoji', + 'id': 'simple_mover', + 'fields': { + 'emoji': '✨' + }, + 'next': { + 'block': { + 'type': 'controls_if', + 'id': 'complex_mover', + 'extraState': { + 'hasElse': true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + 'type': 'text_print', + 'id': 'text_print', + "disabledReasons": [ + "MANUALLY_DISABLED" + ], + 'x': 75, + 'y': 400, + 'inputs': { + 'TEXT': { + 'shadow': { + 'type': 'text', + 'id': 'shadow_text', + 'fields': { + 'TEXT': 'abc', + }, + }, + }, + }, + 'next': { + 'block': { + 'type': 'controls_if', + 'id': 'controls_if', + 'extraState': { + 'elseIfCount': 1, + 'hasElse': true, + }, + 'inputs': { + 'DO0': { + 'block': { + 'type': 'controls_repeat_ext', + 'id': 'controls_repeat_ext', + 'inputs': { + 'TIMES': { + 'shadow': { + 'type': 'math_number', + 'id': 'shadow_math_number', + 'fields': { + 'NUM': 10, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + 'type': 'p5_draw', + 'id': 'p5_draw', + 'x': 75, + 'y': 950, + 'deletable': false, + }, + ], + }, +}; + const comments = { 'workspaceComments': [ { @@ -985,17 +1099,20 @@ const comments = { export const load = function (workspace, scenarioString) { const scenarioMap = { 'blank': blankCanvas, - 'comments': comments, - 'moreBlocks': moreBlocks, - 'moveTestBlocks': moveTestBlocks, - 'navigationTestBlocks': navigationTestBlocks, - 'simpleCircle': simpleCircle, + comments, + moreBlocks, + moveStartTestBlocks, + moveStatementTestBlocks, + navigationTestBlocks, + simpleCircle, 'sun': sunnyDay, }; - - const data = JSON.stringify(scenarioMap[scenarioString]); // Don't emit events during loading. Blockly.Events.disable(); - Blockly.serialization.workspaces.load(JSON.parse(data), workspace, false); + Blockly.serialization.workspaces.load( + scenarioMap[scenarioString], + workspace, + false, + ); Blockly.Events.enable(); }; diff --git a/test/webdriverio/test/move_test.ts b/test/webdriverio/test/move_test.ts index a8a34b39..781abdd3 100644 --- a/test/webdriverio/test/move_test.ts +++ b/test/webdriverio/test/move_test.ts @@ -17,14 +17,14 @@ import { contextMenuItems, } from './test_setup.js'; -suite('Move tests', function () { +suite('Move start tests', function () { // Increase timeout to 10s for this longer test (but disable // timeouts if when non-zero PAUSE_TIME is used to watch tests) run. this.timeout(PAUSE_TIME ? 0 : 10000); // Clear the workspace and load start blocks. setup(async function () { - this.browser = await testSetup(testFileLocations.MOVE_TEST_BLOCKS); + this.browser = await testSetup(testFileLocations.MOVE_START_TEST_BLOCKS); await this.browser.pause(PAUSE_TIME); }); @@ -158,6 +158,149 @@ suite('Move tests', function () { await sendKeyAndWait(this.browser, Key.Escape); } }); +}); + +suite('Statement move tests', function () { + // Increase timeout to 10s for this longer test (but disable + // timeouts if when non-zero PAUSE_TIME is used to watch tests) run. + this.timeout(PAUSE_TIME ? 0 : 10000); + + // Clear the workspace and load start blocks. + setup(async function () { + this.browser = await testSetup( + testFileLocations.MOVE_STATEMENT_TEST_BLOCKS, + ); + await this.browser.pause(PAUSE_TIME); + }); + + /** ID of a statement block with no inputs. */ + const BLOCK_SIMPLE = 'simple_mover'; + + /** + * Expected connection candidates when moving BLOCK_SIMPLE, after + * pressing right or down arrow n times. + */ + const EXPECTED_SIMPLE = [ + {id: 'p5_canvas', index: 1, ownIndex: 0}, // Next; starting location. + {id: 'complex_mover', index: 3, ownIndex: 0}, // "If" statement input. + {id: 'complex_mover', index: 4, ownIndex: 0}, // "Else" statement input. + {id: 'complex_mover', index: 1, ownIndex: 0}, // Next. + {id: 'text_print', index: 0, ownIndex: 1}, // Previous. + {id: 'text_print', index: 1, ownIndex: 0}, // Next. + {id: 'controls_if', index: 3, ownIndex: 0}, // "If" statement input. + {id: 'controls_repeat_ext', index: 3, ownIndex: 0}, // Statement input. + {id: 'controls_repeat_ext', index: 1, ownIndex: 0}, // Next. + {id: 'controls_if', index: 5, ownIndex: 0}, // "Else if" statement input. + {id: 'controls_if', index: 6, ownIndex: 0}, // "Else" statement input. + {id: 'controls_if', index: 1, ownIndex: 0}, // Next. + {id: 'p5_draw', index: 0, ownIndex: 0}, // Statement input. + {id: 'p5_canvas', index: 1, ownIndex: 0}, // Next; starting location again. + ]; + const EXPECTED_SIMPLE_REVERSED = EXPECTED_SIMPLE.slice().reverse(); + + test( + 'Constrained move of simple stack block right', + moveTest(BLOCK_SIMPLE, Key.ArrowRight, EXPECTED_SIMPLE, { + parentId: 'complex_mover', + parentIndex: 3, + nextId: null, + valueId: null, + }), + ); + test( + 'Constrained move of simple stack block left', + moveTest(BLOCK_SIMPLE, Key.ArrowLeft, EXPECTED_SIMPLE_REVERSED, { + parentId: 'p5_draw', + parentIndex: 0, + nextId: null, + valueId: null, + }), + ); + test( + 'Constrained move of simple stack block down', + moveTest(BLOCK_SIMPLE, Key.ArrowDown, EXPECTED_SIMPLE, { + parentId: 'complex_mover', + parentIndex: 3, + nextId: null, + valueId: null, + }), + ); + test( + 'Constrained move of simple stack block up', + moveTest(BLOCK_SIMPLE, Key.ArrowUp, EXPECTED_SIMPLE_REVERSED, { + parentId: 'p5_draw', + parentIndex: 0, + nextId: null, + valueId: null, + }), + ); + + /** ID of a statement block with multiple statement inputs. */ + const BLOCK_COMPLEX = 'complex_mover'; + + /** + * Expected connection candidates when moving BLOCK_COMPLEX, after + * pressing right or down arrow n times. + */ + const EXPECTED_COMPLEX = [ + // TODO(#702): Due to a bug in KeyboardDragStrategy, certain + // connection candidates that can be found using the mouse are not + // visited when doing a keyboard drag. They appear in the list + // below, but commented out for now. + // is fixed. + {id: 'simple_mover', index: 1, ownIndex: 0}, // Next; starting location. + // {id: 'text_print', index: 0, ownIndex: 1}, // Previous to own next. + {id: 'text_print', index: 0, ownIndex: 4}, // Previous to own else input. + // {id: 'text_print', index: 0, ownIndex: 3}, // Previous to own if input. + {id: 'text_print', index: 1, ownIndex: 0}, // Next. + {id: 'controls_if', index: 3, ownIndex: 0}, // "If" statement input. + {id: 'controls_repeat_ext', index: 3, ownIndex: 0}, // Statement input. + {id: 'controls_repeat_ext', index: 1, ownIndex: 0}, // Next. + {id: 'controls_if', index: 5, ownIndex: 0}, // "Else if" statement input. + {id: 'controls_if', index: 6, ownIndex: 0}, // "Else" statement input. + {id: 'controls_if', index: 1, ownIndex: 0}, // Next. + {id: 'p5_draw', index: 0, ownIndex: 0}, // Statement input. + {id: 'p5_canvas', index: 1, ownIndex: 0}, // Next; starting location again. + {id: 'simple_mover', index: 1, ownIndex: 0}, // Next; starting location. + ]; + const EXPECTED_COMPLEX_REVERSED = EXPECTED_COMPLEX.slice().reverse(); + + test( + 'Constrained move of complex stack block right', + moveTest(BLOCK_COMPLEX, Key.ArrowRight, EXPECTED_COMPLEX,{ + parentId: null, + parentIndex: null, + nextId: null, // TODO(#702): Should be 'text_print', + valueId: null, + }), + ); + test( + 'Constrained move of complex stack block left', + moveTest(BLOCK_COMPLEX, Key.ArrowLeft, EXPECTED_COMPLEX_REVERSED, { + parentId: 'p5_canvas', + parentIndex: 1, + nextId: 'simple_mover', + valueId: null, + }), + ); + test( + 'Constrained move of complex stack block down', + moveTest(BLOCK_COMPLEX, Key.ArrowDown, EXPECTED_COMPLEX, { + parentId: null, + parentIndex: null, + nextId: null, // TODO(#702): Should be 'text_print', + valueId: null, + }), + ); + test( + 'Constrained move of complex stack block up', + moveTest(BLOCK_COMPLEX, Key.ArrowUp, EXPECTED_COMPLEX_REVERSED, { + parentId: 'p5_canvas', + parentIndex: 1, + nextId: 'simple_mover', + valueId: null, + }), + ); // When a top-level block with no previous, next or output // connections is subject to a constrained move, it should not move. @@ -168,7 +311,7 @@ suite('Move tests', function () { // block unexpectedly moving (unless workspace scale was === 1). test('Constrained move of unattachable top-level block', async function () { // Block ID of an unconnectable block. - const BLOCK = 'p5_setup_1'; + const BLOCK = 'p5_setup'; // Scale workspace. await this.browser.execute(() => { @@ -216,15 +359,58 @@ suite('Move tests', function () { }); /** - * Get information about the currently-selected block's parent and + * Create a mocha test function moving a specified block in a + * particular direction, checking that it has the the expected + * connection candidate after each step, and that once the move + * finishes it is connected as expected. + * + * @param mover Block ID of the block to be moved. + * @param key Key to send to move one step. + * @param candidates Array of expected connection candidates. + * @param finalInfo Expected final connections when move finished, + * as returne d by getFocusedNeighbourInfo. + * @returns function to pass as second argument to mocha's test function. + */ +function moveTest( + mover: string, + key: string | string[], + candidates: Array<{id: string; index: number}>, + finalInfo: Awaited>, +) { + return async function (this: Mocha.Context) { + // Navigate to block to be moved and intiate move. + await focusOnBlock(this.browser, mover); + await sendKeyAndWait(this.browser, 'm'); + // Move to right multiple times, checking connection candidates. + for (let i = 0; i < candidates.length; i++) { + const candidate = await getConnectionCandidate(this.browser); + chai.assert.deepEqual(candidate, candidates[i]); + await sendKeyAndWait(this.browser, key); + } + + // Finish move and check final location of moved block. + await sendKeyAndWait(this.browser, Key.Enter); + const info = await getFocusedNeighbourInfo(this.browser); + chai.assert.deepEqual(info, finalInfo); + }; +} + +/** + * Get information about the currently-focused block's parent and * child blocks. * * @param browser The webdriverio browser session. - * @returns A promise setting to {parentId, parentIndex, nextId, - * valueId}, being respectively the parent block ID, index of parent - * connection, next block ID, and ID of the block connected to the - * zeroth value value input, or null if the given item does not - * exist. + * @returns A promise setting to + * + * {parentId, parentIndex, nextId, valueId} + * + * where parentId, parentIndex are the ID of the parent block and + * the index of the connection on that block to which the + * currently-focused block is connected, nextId is the ID of block + * connected to the focused block's next connection, and valueID + * is the ID of a block connected to the zeroth input of the + * focused block (or, in each case, null if there is no such + * block). */ function getFocusedNeighbourInfo(browser: Browser) { return browser.execute(() => { @@ -305,3 +491,43 @@ function getCoordinate( return block.getRelativeToSurfaceXY(); }, id); } + +/** + * Get information about the connection candidate for the + * currently-moving block (if any). + * + * @param browser The webdriverio browser session. + * @returns A promise setting to either null if there is no connection + * candidate, or otherwise if there is one to + * + * {id, index, ownIndex} + * + * where id is the block ID of the neighbour, index is the index + * of the candidate connection on the neighbour, and ownIndex is + * the index of the candidate connection on the moving block. + */ +function getConnectionCandidate( + browser: Browser, +): Promise<{id: string; index: number} | null> { + return browser.execute(() => { + const focused = Blockly.getFocusManager().getFocusedNode(); + if (!focused) throw new Error('nothing focused'); + if (!(focused instanceof Blockly.BlockSvg)) { + throw new TypeError('focused node is not a BlockSvg'); + } + const block = focused; // Inferred as BlockSvg. + const dragStrategy = + block.getDragStrategy() as Blockly.dragging.BlockDragStrategy; + if (!dragStrategy) throw new Error('no drag strategy'); + // @ts-expect-error connectionCandidate is private. + const candidate = dragStrategy.connectionCandidate; + if (!candidate) return null; + const neighbourBlock = candidate.neighbour.getSourceBlock(); + if (!neighbourBlock) throw new TypeError('connection has no source block'); + const neighbourConnections = neighbourBlock.getConnections_(true); + const index = neighbourConnections.indexOf(candidate.neighbour); + const ownConnections = block.getConnections_(true); + const ownIndex = ownConnections.indexOf(candidate.local); + return {id: neighbourBlock.id, index, ownIndex}; + }); +} diff --git a/test/webdriverio/test/test_setup.ts b/test/webdriverio/test/test_setup.ts index 93bec99a..783270ad 100644 --- a/test/webdriverio/test/test_setup.ts +++ b/test/webdriverio/test/test_setup.ts @@ -155,8 +155,12 @@ export const testFileLocations = { // eslint-disable-next-line @typescript-eslint/naming-convention MORE_BLOCKS: createTestUrl(new URLSearchParams({scenario: 'moreBlocks'})), // eslint-disable-next-line @typescript-eslint/naming-convention - MOVE_TEST_BLOCKS: createTestUrl( - new URLSearchParams({scenario: 'moveTestBlocks'}), + MOVE_START_TEST_BLOCKS: createTestUrl( + new URLSearchParams({scenario: 'moveStartTestBlocks'}), + ), + // eslint-disable-next-line @typescript-eslint/naming-convention + MOVE_STATEMENT_TEST_BLOCKS: createTestUrl( + new URLSearchParams({scenario: 'moveStatementTestBlocks'}), ), COMMENTS: createTestUrl(new URLSearchParams({scenario: 'comments'})), // eslint-disable-next-line @typescript-eslint/naming-convention From 83b8b1b458b4629394eb61c338efda2c655fe612 Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Wed, 27 Aug 2025 18:21:45 +0100 Subject: [PATCH 10/19] chore: Format (#705) --- test/loadTestBlocks.js | 6 ++---- test/webdriverio/test/move_test.ts | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/test/loadTestBlocks.js b/test/loadTestBlocks.js index da64cb05..78a5c7cb 100644 --- a/test/loadTestBlocks.js +++ b/test/loadTestBlocks.js @@ -899,7 +899,7 @@ const moveStatementTestBlocks = { 'type': 'draw_emoji', 'id': 'simple_mover', 'fields': { - 'emoji': '✨' + 'emoji': '✨', }, 'next': { 'block': { @@ -919,9 +919,7 @@ const moveStatementTestBlocks = { { 'type': 'text_print', 'id': 'text_print', - "disabledReasons": [ - "MANUALLY_DISABLED" - ], + 'disabledReasons': ['MANUALLY_DISABLED'], 'x': 75, 'y': 400, 'inputs': { diff --git a/test/webdriverio/test/move_test.ts b/test/webdriverio/test/move_test.ts index 781abdd3..94db43ae 100644 --- a/test/webdriverio/test/move_test.ts +++ b/test/webdriverio/test/move_test.ts @@ -267,7 +267,7 @@ suite('Statement move tests', function () { test( 'Constrained move of complex stack block right', - moveTest(BLOCK_COMPLEX, Key.ArrowRight, EXPECTED_COMPLEX,{ + moveTest(BLOCK_COMPLEX, Key.ArrowRight, EXPECTED_COMPLEX, { parentId: null, parentIndex: null, nextId: null, // TODO(#702): Should be 'text_print', From 8a59b99ad6bc585f0cbb49aa2c99c754a6a4a292 Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Sun, 31 Aug 2025 10:18:20 +0100 Subject: [PATCH 11/19] test(Mover): Add test for moving value blocks right/left (#710) * fix(tests): Suppress tsc unintentional comparison error When PAUSE_TIME is set to a value other than 0 the comparison in sendKeyAndWait generates a TS unintentionl comparison error. * refactor(tests): Simplify moveTest Simplify moveTest (and the tests that use it) by assuming that the move always finishes in the same location it started in. We will leave testing various move end scenarios (e.g. healing) for a separate suite of move-finishing tests, akin to existing move start tests. * fix(tests): eslint-disable to ignore @ts-igonre * test(Mover): Introduce test blocks and suite for value move tests Introduce moveValueTestBlocks and a suite 'Value expression move tests' for each of two renderers (geras, zelos) to test constrained movement of value blocks. * test(Mover): Add value expression right/left move tests Moving value blocks left/right behaves as expected when using geras, but in zelos there is a bug where external inputs are not visited in the expected order (not that external inputs _look_ external when using geras, but still it is a possible configuration and should behave as expected). Note that two of these tests fail due to a bug; this will be dealt with in the next commit. * refactor(tests): Use text_join, nested suites Refactor the simple value expression move tests as follows: - Use text_join rather than text_count, as the former is a more plausible example of an value block with multiple external inputs. - Use common outer suite with nested suites for each of the different renderers, moving common constants to outer suite. * feat(tests): Add blocks for complex value block move tests * refactor(tests): Further organise with nested suites * test(Mover): Add value row (no free inputs) right/left tests * test(Mover): Add unary expression right/left tests * chore(tests): Skip failing zelos value block move tests due to #707. * chore(tests): Skip failing unary move right tests due to #709 * fix(tests): Remove debugging console.log * fix(tests): Ignore unhelpful lint error --- test/index.html | 3 + test/loadTestBlocks.js | 166 ++++++++++++++++ test/webdriverio/test/move_test.ts | 285 +++++++++++++++++++--------- test/webdriverio/test/test_setup.ts | 8 +- 4 files changed, 371 insertions(+), 91 deletions(-) diff --git a/test/index.html b/test/index.html index d64f6a64..53dbcf3e 100644 --- a/test/index.html +++ b/test/index.html @@ -114,6 +114,9 @@ + diff --git a/test/loadTestBlocks.js b/test/loadTestBlocks.js index 78a5c7cb..f7de5113 100644 --- a/test/loadTestBlocks.js +++ b/test/loadTestBlocks.js @@ -974,6 +974,171 @@ const moveStatementTestBlocks = { }, }; +const moveValueTestBlocks = { + 'blocks': { + 'languageVersion': 0, + 'blocks': [ + { + 'type': 'p5_setup', + 'id': 'p5_setup', + 'x': 75, + 'y': 75, + 'deletable': false, + 'inputs': { + 'STATEMENTS': { + 'block': { + 'type': 'p5_canvas', + 'id': 'p5_canvas', + 'deletable': false, + 'movable': false, + 'fields': { + 'WIDTH': 400, + 'HEIGHT': 400, + }, + }, + }, + }, + }, + { + 'type': 'text', + 'id': 'unattached', + 'x': 75, + 'y': 200, + 'fields': { + 'TEXT': 'unattached value', + }, + }, + { + 'type': 'p5_draw', + 'id': 'p5_draw', + 'x': 75, + 'y': 260, + 'deletable': false, + 'inputs': { + 'STATEMENTS': { + 'block': { + 'type': 'text_print', + 'id': 'print0', + 'inputs': { + 'TEXT': { + 'block': { + 'type': 'text_changeCase', + 'id': 'complex_mover', + 'fields': { + 'CASE': 'TITLECASE', + }, + 'inputs': { + 'TEXT': { + 'block': { + 'type': 'text', + 'id': 'simple_mover', + 'fields': { + 'TEXT': 'simple mover', + }, + }, + }, + }, + }, + }, + }, + 'next': { + 'block': { + 'type': 'text_print', + 'id': 'print1', + 'next': { + 'block': { + 'type': 'text_print', + 'id': 'print2', + 'inputs': { + 'TEXT': { + 'shadow': { + 'type': 'text', + 'id': 'shadow_print2', + 'fields': { + 'TEXT': 'shadow', + }, + }, + }, + }, + 'next': { + 'block': { + 'type': 'draw_emoji', + 'id': 'draw_emoji', + 'fields': { + 'emoji': '🐻', + }, + 'next': { + 'block': { + 'type': 'text_print', + 'id': 'print3', + 'inputs': { + 'TEXT': { + 'block': { + 'type': 'text_join', + 'id': 'text_join1', + 'inline': true, + 'inputs': { + 'ADD0': { + 'shadow': { + 'type': 'text', + 'id': 'shadow_join', + 'fields': { + 'TEXT': 'inline', + }, + }, + }, + }, + }, + }, + }, + 'next': { + 'block': { + 'type': 'controls_repeat_ext', + 'id': 'controls_repeat_ext', + 'inputs': { + 'TIMES': { + 'shadow': { + 'type': 'math_number', + 'id': 'shadow_repeat', + 'fields': { + 'NUM': 1, + }, + }, + }, + 'DO': { + 'block': { + 'type': 'text_print', + 'id': 'print4', + 'inputs': { + 'TEXT': { + 'block': { + 'type': 'text_join', + 'id': 'text_join2', + 'inline': false, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + ], + }, +}; + const comments = { 'workspaceComments': [ { @@ -1101,6 +1266,7 @@ export const load = function (workspace, scenarioString) { moreBlocks, moveStartTestBlocks, moveStatementTestBlocks, + moveValueTestBlocks, navigationTestBlocks, simpleCircle, 'sun': sunnyDay, diff --git a/test/webdriverio/test/move_test.ts b/test/webdriverio/test/move_test.ts index 94db43ae..646282a2 100644 --- a/test/webdriverio/test/move_test.ts +++ b/test/webdriverio/test/move_test.ts @@ -10,6 +10,7 @@ import {Browser, Key} from 'webdriverio'; import { PAUSE_TIME, focusOnBlock, + createTestUrl, testFileLocations, testSetup, sendKeyAndWait, @@ -178,9 +179,9 @@ suite('Statement move tests', function () { /** * Expected connection candidates when moving BLOCK_SIMPLE, after - * pressing right or down arrow n times. + * pressing right (or down) arrow n times. */ - const EXPECTED_SIMPLE = [ + const EXPECTED_SIMPLE_RIGHT = [ {id: 'p5_canvas', index: 1, ownIndex: 0}, // Next; starting location. {id: 'complex_mover', index: 3, ownIndex: 0}, // "If" statement input. {id: 'complex_mover', index: 4, ownIndex: 0}, // "Else" statement input. @@ -194,55 +195,42 @@ suite('Statement move tests', function () { {id: 'controls_if', index: 6, ownIndex: 0}, // "Else" statement input. {id: 'controls_if', index: 1, ownIndex: 0}, // Next. {id: 'p5_draw', index: 0, ownIndex: 0}, // Statement input. - {id: 'p5_canvas', index: 1, ownIndex: 0}, // Next; starting location again. ]; - const EXPECTED_SIMPLE_REVERSED = EXPECTED_SIMPLE.slice().reverse(); - - test( - 'Constrained move of simple stack block right', - moveTest(BLOCK_SIMPLE, Key.ArrowRight, EXPECTED_SIMPLE, { - parentId: 'complex_mover', - parentIndex: 3, - nextId: null, - valueId: null, - }), - ); - test( - 'Constrained move of simple stack block left', - moveTest(BLOCK_SIMPLE, Key.ArrowLeft, EXPECTED_SIMPLE_REVERSED, { - parentId: 'p5_draw', - parentIndex: 0, - nextId: null, - valueId: null, - }), - ); - test( - 'Constrained move of simple stack block down', - moveTest(BLOCK_SIMPLE, Key.ArrowDown, EXPECTED_SIMPLE, { - parentId: 'complex_mover', - parentIndex: 3, - nextId: null, - valueId: null, - }), - ); - test( - 'Constrained move of simple stack block up', - moveTest(BLOCK_SIMPLE, Key.ArrowUp, EXPECTED_SIMPLE_REVERSED, { - parentId: 'p5_draw', - parentIndex: 0, - nextId: null, - valueId: null, - }), + /** + * Expected connection candidates when moving BLOCK_SIMPLE after + * pressing left (or up) arrow n times. + */ + const EXPECTED_SIMPLE_LEFT = EXPECTED_SIMPLE_RIGHT.slice(0, 1).concat( + EXPECTED_SIMPLE_RIGHT.slice(1).reverse(), ); + suite('Constrained moves of simple stack block', function () { + test( + 'moving right', + moveTest(BLOCK_SIMPLE, Key.ArrowRight, EXPECTED_SIMPLE_RIGHT), + ); + test( + 'moving left', + moveTest(BLOCK_SIMPLE, Key.ArrowLeft, EXPECTED_SIMPLE_LEFT), + ); + test( + 'moving down', + moveTest(BLOCK_SIMPLE, Key.ArrowDown, EXPECTED_SIMPLE_RIGHT), + ); + test( + 'moving up', + moveTest(BLOCK_SIMPLE, Key.ArrowUp, EXPECTED_SIMPLE_LEFT), + ); + }); + /** ID of a statement block with multiple statement inputs. */ const BLOCK_COMPLEX = 'complex_mover'; /** * Expected connection candidates when moving BLOCK_COMPLEX, after - * pressing right or down arrow n times. + * pressing right (or down) arrow n times. */ - const EXPECTED_COMPLEX = [ + const EXPECTED_COMPLEX_RIGHT = [ // TODO(#702): Due to a bug in KeyboardDragStrategy, certain // connection candidates that can be found using the mouse are not // visited when doing a keyboard drag. They appear in the list @@ -261,47 +249,34 @@ suite('Statement move tests', function () { {id: 'controls_if', index: 1, ownIndex: 0}, // Next. {id: 'p5_draw', index: 0, ownIndex: 0}, // Statement input. {id: 'p5_canvas', index: 1, ownIndex: 0}, // Next; starting location again. - {id: 'simple_mover', index: 1, ownIndex: 0}, // Next; starting location. ]; - const EXPECTED_COMPLEX_REVERSED = EXPECTED_COMPLEX.slice().reverse(); - - test( - 'Constrained move of complex stack block right', - moveTest(BLOCK_COMPLEX, Key.ArrowRight, EXPECTED_COMPLEX, { - parentId: null, - parentIndex: null, - nextId: null, // TODO(#702): Should be 'text_print', - valueId: null, - }), - ); - test( - 'Constrained move of complex stack block left', - moveTest(BLOCK_COMPLEX, Key.ArrowLeft, EXPECTED_COMPLEX_REVERSED, { - parentId: 'p5_canvas', - parentIndex: 1, - nextId: 'simple_mover', - valueId: null, - }), - ); - test( - 'Constrained move of complex stack block down', - moveTest(BLOCK_COMPLEX, Key.ArrowDown, EXPECTED_COMPLEX, { - parentId: null, - parentIndex: null, - nextId: null, // TODO(#702): Should be 'text_print', - valueId: null, - }), - ); - test( - 'Constrained move of complex stack block up', - moveTest(BLOCK_COMPLEX, Key.ArrowUp, EXPECTED_COMPLEX_REVERSED, { - parentId: 'p5_canvas', - parentIndex: 1, - nextId: 'simple_mover', - valueId: null, - }), + /** + * Expected connection candidates when moving BLOCK_COMPLEX after + * pressing left or up arrow n times. + */ + const EXPECTED_COMPLEX_LEFT = EXPECTED_COMPLEX_RIGHT.slice(0, 1).concat( + EXPECTED_COMPLEX_RIGHT.slice(1).reverse(), ); + suite('Constrained moves of stack block with statement inputs', function () { + test( + 'moving right', + moveTest(BLOCK_COMPLEX, Key.ArrowRight, EXPECTED_COMPLEX_RIGHT), + ); + test( + 'moving left', + moveTest(BLOCK_COMPLEX, Key.ArrowLeft, EXPECTED_COMPLEX_LEFT), + ); + test( + 'moving down', + moveTest(BLOCK_COMPLEX, Key.ArrowDown, EXPECTED_COMPLEX_RIGHT), + ); + test( + 'moving up', + moveTest(BLOCK_COMPLEX, Key.ArrowUp, EXPECTED_COMPLEX_LEFT), + ); + }); + // When a top-level block with no previous, next or output // connections is subject to a constrained move, it should not move. // @@ -358,30 +333,166 @@ suite('Statement move tests', function () { }); }); +suite(`Value expression move tests`, function () { + // Increase timeout to 10s for this longer test (but disable + // timeouts if when non-zero PAUSE_TIME is used to watch tests) run. + this.timeout(PAUSE_TIME ? 0 : 10000); + + /** ID of a simple reporter (a value block with no inputs). */ + const BLOCK_SIMPLE = 'simple_mover'; + + /** + * Expected connection candidates when moving BLOCK_SIMPLE, after + * pressing ArrowRight n times. + */ + const EXPECTED_SIMPLE_RIGHT = [ + {id: 'complex_mover', index: 1, ownIndex: 0}, // Starting location. + {id: 'print1', index: 2, ownIndex: 0}, // Print block with no shadow. + {id: 'print2', index: 2, ownIndex: 0}, // Print block with shadow. + // Skip draw_emoji block as it has no value inputs. + {id: 'print3', index: 2, ownIndex: 0}, // Replacing join expression. + {id: 'text_join1', index: 1, ownIndex: 0}, // Join block ADD0 input. + {id: 'text_join1', index: 2, ownIndex: 0}, // Join block ADD1 input. + // Skip controls_repeat_ext block's TIMES input as it is incompatible. + {id: 'print4', index: 2, ownIndex: 0}, // Replacing join expression. + {id: 'text_join2', index: 1, ownIndex: 0}, // Join block ADD0 input. + {id: 'text_join2', index: 2, ownIndex: 0}, // Join block ADD1 input. + // Skip unconnected text block as it has no inputs. + {id: 'print0', index: 2, ownIndex: 0}, // Print block having complex_mover. + ]; + /** + * Expected connection candidates when moving BLOCK_SIMPLE, after + * pressing ArrowLeft n times. + */ + const EXPECTED_SIMPLE_LEFT = EXPECTED_SIMPLE_RIGHT.slice(0, 1).concat( + EXPECTED_SIMPLE_RIGHT.slice(1).reverse(), + ); + + /** ID of a unary expression block (block with one value input + output) */ + const BLOCK_COMPLEX = 'complex_mover'; + + /** + * Expected connection candidates when moving row consisting of + * BLOCK_COMPLEX, with a block (in this case BLOCK_SIMPLE) attached + * to its input, after pressing ArrowRight n times. + */ + const EXPECTED_ROW_RIGHT = EXPECTED_SIMPLE_RIGHT.slice(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + EXPECTED_ROW_RIGHT[0] = EXPECTED_ROW_RIGHT.pop()!; + /** + * Expected connection candidates when moving row consisting of + * BLOCK_COMPLEX, with a block (in this case BLOCK_SIMPLE) attached + * to its input, after pressing ArrowLeft n times. + */ + const EXPECTED_ROW_LEFT = EXPECTED_ROW_RIGHT.slice(0, 1).concat( + EXPECTED_ROW_RIGHT.slice(1).reverse(), + ); + + /** + * Expected connection candidates when moving row consisting of + * BLOCK_COMPLEX on its own after pressing ArrowRight n times. + */ + const EXPECTED_UNARY_RIGHT = [ + {id: 'print0', index: 2, ownIndex: 0}, // Starting location. + {id: 'print1', index: 2, ownIndex: 0}, // Print block with no shadow. + {id: 'print2', index: 2, ownIndex: 0}, // Print block with shadow. + // Skip draw_emoji block as it has no value inputs. + {id: 'print3', index: 2, ownIndex: 0}, // Replacing join expression. + {id: 'text_join1', index: 1, ownIndex: 0}, // Join block ADD0 input. + {id: 'text_join1', index: 2, ownIndex: 0}, // Join block ADD1 input. + // Skip controls_repeat_ext block's TIMES input as it is incompatible. + {id: 'print4', index: 2, ownIndex: 0}, // Replacing join expression. + {id: 'text_join2', index: 1, ownIndex: 0}, // Join block ADD0 input. + {id: 'text_join2', index: 2, ownIndex: 0}, // Join block ADD1 input. + {id: 'unattached', index: 0, ownIndex: 1}, // Unattached text to own input. + ]; + /** + * Expected connection candidates when moving row consisting of + * BLOCK_UNARY on its own after pressing ArrowLEFT n times. + */ + const EXPECTED_UNARY_LEFT = EXPECTED_UNARY_RIGHT.slice(0, 1).concat( + EXPECTED_UNARY_RIGHT.slice(1).reverse(), + ); + + for (const renderer of ['geras', 'thrasos', 'zelos']) { + // TODO(#707): These tests fail when run using zelos, so for now + // we skip entire suite. Stop skipping suite when bug is fixed. + const suiteOrSkip = renderer === 'zelos' ? suite.skip : suite; + suiteOrSkip(`using ${renderer}`, function () { + // Clear the workspace and load start blocks. + setup(async function () { + this.browser = await testSetup( + createTestUrl( + new URLSearchParams({renderer, scenario: 'moveValueTestBlocks'}), + ), + ); + await this.browser.pause(PAUSE_TIME); + }); + + suite('Constrained moves of a simple reporter block', function () { + test( + 'moving right', + moveTest(BLOCK_SIMPLE, Key.ArrowRight, EXPECTED_SIMPLE_RIGHT), + ); + test( + 'moving left', + moveTest(BLOCK_SIMPLE, Key.ArrowLeft, EXPECTED_SIMPLE_LEFT), + ); + }); + suite('Constrained moves of two blocks with no free inputs', function () { + test( + 'moving right', + moveTest(BLOCK_COMPLEX, Key.ArrowRight, EXPECTED_ROW_RIGHT), + ); + test( + 'moving left', + moveTest(BLOCK_COMPLEX, Key.ArrowLeft, EXPECTED_ROW_LEFT), + ); + }); + suite('Constrained moves of unary expression block', function () { + setup(async function () { + // Delete block connected to complex_mover's input. + await focusOnBlock(this.browser, BLOCK_SIMPLE); + await sendKeyAndWait(this.browser, Key.Delete); + }); + + // TODO(#709): Reenable test once crash bug is fixed. + test.skip( + 'moving right', + moveTest(BLOCK_COMPLEX, Key.ArrowRight, EXPECTED_UNARY_RIGHT), + ); + test( + 'moving left', + moveTest(BLOCK_COMPLEX, Key.ArrowLeft, EXPECTED_UNARY_LEFT), + ); + }); + }); + } +}); + /** * Create a mocha test function moving a specified block in a * particular direction, checking that it has the the expected * connection candidate after each step, and that once the move - * finishes it is connected as expected. + * finishes that the moving block is reconnected to its initial + * location. * * @param mover Block ID of the block to be moved. * @param key Key to send to move one step. * @param candidates Array of expected connection candidates. - * @param finalInfo Expected final connections when move finished, - * as returne d by getFocusedNeighbourInfo. * @returns function to pass as second argument to mocha's test function. */ function moveTest( mover: string, key: string | string[], candidates: Array<{id: string; index: number}>, - finalInfo: Awaited>, ) { return async function (this: Mocha.Context) { // Navigate to block to be moved and intiate move. await focusOnBlock(this.browser, mover); + const initialInfo = await getFocusedNeighbourInfo(this.browser); await sendKeyAndWait(this.browser, 'm'); - // Move to right multiple times, checking connection candidates. + // Press specified key multiple times, checking connection candidates. for (let i = 0; i < candidates.length; i++) { const candidate = await getConnectionCandidate(this.browser); chai.assert.deepEqual(candidate, candidates[i]); @@ -390,8 +501,8 @@ function moveTest( // Finish move and check final location of moved block. await sendKeyAndWait(this.browser, Key.Enter); - const info = await getFocusedNeighbourInfo(this.browser); - chai.assert.deepEqual(info, finalInfo); + const finalInfo = await getFocusedNeighbourInfo(this.browser); + chai.assert.deepEqual(initialInfo, finalInfo); }; } diff --git a/test/webdriverio/test/test_setup.ts b/test/webdriverio/test/test_setup.ts index 783270ad..ae93f073 100644 --- a/test/webdriverio/test/test_setup.ts +++ b/test/webdriverio/test/test_setup.ts @@ -131,13 +131,11 @@ export async function testSetup( * @returns posix path */ function posixPath(target: string): string { - const result = target.split(path.sep).join(path.posix.sep); - console.log(result); - return result; + return target.split(path.sep).join(path.posix.sep); } // Relative to dist folder for TS build -const createTestUrl = (options?: URLSearchParams) => { +export const createTestUrl = (options?: URLSearchParams) => { const dirname = path.dirname(fileURLToPath(import.meta.url)); const base = new URL( `file://${posixPath(path.join(dirname, '..', '..', 'build', 'index.html'))}`, @@ -558,6 +556,8 @@ export async function sendKeyAndWait( keys: string | string[], times = 1, ) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: Unintentional comparison error if (PAUSE_TIME === 0) { // Send all keys in one call if no pauses needed. keys = Array(times).fill(keys).flat(); From 1ff601272841e00cc13aab328be553421cf10048 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 2 Sep 2025 15:39:32 -0700 Subject: [PATCH 12/19] release: release version 3.0. (#708) * chore: Update Blockly dependency to 12.3.0. * release: bump version to 3.0.0 * chore: Update Blockly peer dependency to 12.3.0. * release: bump version to 3.0.1 --- package-lock.json | 15 ++++++++------- package.json | 6 +++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2adddc54..0d2c846f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@blockly/keyboard-navigation", - "version": "2.0.0", + "version": "3.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@blockly/keyboard-navigation", - "version": "2.0.0", + "version": "3.0.1", "license": "Apache-2.0", "devDependencies": { "@blockly/dev-scripts": "^4.0.8", @@ -18,7 +18,7 @@ "@types/p5": "^1.7.6", "@typescript-eslint/eslint-plugin": "^6.7.2", "@typescript-eslint/parser": "^6.7.2", - "blockly": "^12.2.0", + "blockly": "12.3.0", "chai": "^5.2.0", "eslint": "^8.49.0", "eslint-config-google": "^0.14.0", @@ -35,7 +35,7 @@ "webdriverio": "^9.12.1" }, "peerDependencies": { - "blockly": "^12.2.0" + "blockly": "^12.3.0" } }, "node_modules/@asamuzakjp/css-color": { @@ -3063,10 +3063,11 @@ } }, "node_modules/blockly": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/blockly/-/blockly-12.2.0.tgz", - "integrity": "sha512-s4QL9ogEMzc4Pxfe8Oi3Kmu6SQ0ts2thzmRYjdnMSEIVZFpBZ4OUuNKvpFICqujO0yfAo99zON8KzxAFw8hA1w==", + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/blockly/-/blockly-12.3.0.tgz", + "integrity": "sha512-dtxM6Dk8cm0QW4vMJTXmn7xi7a4GnQdXu28Esuuofx7DsYfq73456O5tm3ShUMDcXaFg8w3GVfgoH8I9v6gSVA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "jsdom": "26.1.0" }, diff --git a/package.json b/package.json index 26ba4d39..697cd4d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@blockly/keyboard-navigation", - "version": "2.0.0", + "version": "3.0.1", "description": "A plugin for keyboard navigation.", "scripts": { "audit:fix": "blockly-scripts auditFix", @@ -59,7 +59,7 @@ "@types/p5": "^1.7.6", "@typescript-eslint/eslint-plugin": "^6.7.2", "@typescript-eslint/parser": "^6.7.2", - "blockly": "^12.2.0", + "blockly": "12.3.0", "chai": "^5.2.0", "eslint": "^8.49.0", "eslint-config-google": "^0.14.0", @@ -76,7 +76,7 @@ "webdriverio": "^9.12.1" }, "peerDependencies": { - "blockly": "^12.2.0" + "blockly": "^12.3.0" }, "publishConfig": { "access": "public", From aa8ea7f46bdfe65c1a8f9862753c0d91c4d6ccf8 Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Thu, 4 Sep 2025 22:24:17 +0100 Subject: [PATCH 13/19] test(Mover): Add test for moving complex expression blocks right/left move tests (#711) * feat(tests): Introduce appendBlock test helper Creaet a new test helper appendBlock, that will create a new block from JSON and connect it as requested. * refactor(tests): Use appendBlock in statement move tests This means the test blocks don't need to include the block(s) to be moved. * refactor(tests): Use appendBlock in value move tests This means the test blocks don't need to include the block(s) to be moved, which considerably simplifies adding several more test cases. * chore(tests): Improve value move tests checking vs. unconnected block Use an unconnected (top-level) block with two empty value inputs, to allow us to better compare order of visiting inputs vs. outputs of top-level blocks. * test(Mover): Add complex expression right/left move tests --- test/loadTestBlocks.js | 172 +++++++----------- test/webdriverio/test/move_test.ts | 273 ++++++++++++++++++++--------- 2 files changed, 253 insertions(+), 192 deletions(-) diff --git a/test/loadTestBlocks.js b/test/loadTestBlocks.js index f7de5113..0a0b762d 100644 --- a/test/loadTestBlocks.js +++ b/test/loadTestBlocks.js @@ -870,9 +870,10 @@ const moveStartTestBlocks = { }, }; -// A bunch of statement blocks. The blocks with IDs simple_mover and -// complex_mover will be (constrained-)moved up, down, left and right -// to verify that they visit all the expected candidate connections. +// A bunch of statement blocks. It is intended that statement blocks +// to be moved can be attached to the next connection of p5_canvas, +// and then be (constrained-)moved up, down, left and right to verify +// that they visit all the expected candidate connections. const moveStatementTestBlocks = { 'blocks': { 'languageVersion': 0, @@ -894,24 +895,6 @@ const moveStatementTestBlocks = { 'WIDTH': 400, 'HEIGHT': 400, }, - 'next': { - 'block': { - 'type': 'draw_emoji', - 'id': 'simple_mover', - 'fields': { - 'emoji': '✨', - }, - 'next': { - 'block': { - 'type': 'controls_if', - 'id': 'complex_mover', - 'extraState': { - 'hasElse': true, - }, - }, - }, - }, - }, }, }, }, @@ -1000,123 +983,92 @@ const moveValueTestBlocks = { }, }, { - 'type': 'text', - 'id': 'unattached', + 'type': 'text_join', + 'id': 'join0', 'x': 75, 'y': 200, - 'fields': { - 'TEXT': 'unattached value', - }, }, { 'type': 'p5_draw', 'id': 'p5_draw', 'x': 75, - 'y': 260, + 'y': 300, 'deletable': false, 'inputs': { 'STATEMENTS': { 'block': { 'type': 'text_print', - 'id': 'print0', - 'inputs': { - 'TEXT': { - 'block': { - 'type': 'text_changeCase', - 'id': 'complex_mover', - 'fields': { - 'CASE': 'TITLECASE', - }, - 'inputs': { - 'TEXT': { - 'block': { - 'type': 'text', - 'id': 'simple_mover', - 'fields': { - 'TEXT': 'simple mover', - }, + 'id': 'print1', + 'next': { + 'block': { + 'type': 'text_print', + 'id': 'print2', + 'inputs': { + 'TEXT': { + 'shadow': { + 'type': 'text', + 'id': 'shadow_print2', + 'fields': { + 'TEXT': 'shadow', }, }, }, }, - }, - }, - 'next': { - 'block': { - 'type': 'text_print', - 'id': 'print1', 'next': { 'block': { - 'type': 'text_print', - 'id': 'print2', - 'inputs': { - 'TEXT': { - 'shadow': { - 'type': 'text', - 'id': 'shadow_print2', - 'fields': { - 'TEXT': 'shadow', - }, - }, - }, + 'type': 'draw_emoji', + 'id': 'draw_emoji', + 'fields': { + 'emoji': '🐻', }, 'next': { 'block': { - 'type': 'draw_emoji', - 'id': 'draw_emoji', - 'fields': { - 'emoji': '🐻', + 'type': 'text_print', + 'id': 'print3', + 'inputs': { + 'TEXT': { + 'block': { + 'type': 'text_join', + 'id': 'join1', + 'inline': true, + 'inputs': { + 'ADD0': { + 'shadow': { + 'type': 'text', + 'id': 'shadow_join', + 'fields': { + 'TEXT': 'inline', + }, + }, + }, + }, + }, + }, }, 'next': { 'block': { - 'type': 'text_print', - 'id': 'print3', + 'type': 'controls_repeat_ext', + 'id': 'controls_repeat_ext', 'inputs': { - 'TEXT': { - 'block': { - 'type': 'text_join', - 'id': 'text_join1', - 'inline': true, - 'inputs': { - 'ADD0': { - 'shadow': { - 'type': 'text', - 'id': 'shadow_join', - 'fields': { - 'TEXT': 'inline', - }, - }, - }, + 'TIMES': { + 'shadow': { + 'type': 'math_number', + 'id': 'shadow_repeat', + 'fields': { + 'NUM': 1, }, }, }, - }, - 'next': { - 'block': { - 'type': 'controls_repeat_ext', - 'id': 'controls_repeat_ext', - 'inputs': { - 'TIMES': { - 'shadow': { - 'type': 'math_number', - 'id': 'shadow_repeat', - 'fields': { - 'NUM': 1, - }, - }, - }, - 'DO': { - 'block': { - 'type': 'text_print', - 'id': 'print4', - 'inputs': { - 'TEXT': { - 'block': { - 'type': 'text_join', - 'id': 'text_join2', - 'inline': false, - }, - }, + 'DO': { + 'block': { + 'type': 'text_print', + 'id': 'print4', + 'inputs': { + 'TEXT': { + 'block': { + 'type': 'text_join', + 'id': 'join2', + 'inline': false, }, }, }, diff --git a/test/webdriverio/test/move_test.ts b/test/webdriverio/test/move_test.ts index 646282a2..aa441910 100644 --- a/test/webdriverio/test/move_test.ts +++ b/test/webdriverio/test/move_test.ts @@ -174,18 +174,18 @@ suite('Statement move tests', function () { await this.browser.pause(PAUSE_TIME); }); - /** ID of a statement block with no inputs. */ - const BLOCK_SIMPLE = 'simple_mover'; - + /** Serialized simple statement block with no statement inputs. */ + const STATEMENT_SIMPLE = { + type: 'draw_emoji', + id: 'simple_mover', + fields: {emoji: '✨'}, + }; /** - * Expected connection candidates when moving BLOCK_SIMPLE, after - * pressing right (or down) arrow n times. + * Expected connection candidates when moving a block with no + * inputs, after pressing right (or down) arrow n times. */ const EXPECTED_SIMPLE_RIGHT = [ {id: 'p5_canvas', index: 1, ownIndex: 0}, // Next; starting location. - {id: 'complex_mover', index: 3, ownIndex: 0}, // "If" statement input. - {id: 'complex_mover', index: 4, ownIndex: 0}, // "Else" statement input. - {id: 'complex_mover', index: 1, ownIndex: 0}, // Next. {id: 'text_print', index: 0, ownIndex: 1}, // Previous. {id: 'text_print', index: 1, ownIndex: 0}, // Next. {id: 'controls_if', index: 3, ownIndex: 0}, // "If" statement input. @@ -197,46 +197,52 @@ suite('Statement move tests', function () { {id: 'p5_draw', index: 0, ownIndex: 0}, // Statement input. ]; /** - * Expected connection candidates when moving BLOCK_SIMPLE after + * Expected connection candidates when moving STATEMENT_SIMPLE after * pressing left (or up) arrow n times. */ const EXPECTED_SIMPLE_LEFT = EXPECTED_SIMPLE_RIGHT.slice(0, 1).concat( EXPECTED_SIMPLE_RIGHT.slice(1).reverse(), ); - suite('Constrained moves of simple stack block', function () { + suite('Constrained moves of simple statement block', function () { + setup(async function () { + await appendBlock(this.browser, STATEMENT_SIMPLE, 'p5_canvas'); + }); test( 'moving right', - moveTest(BLOCK_SIMPLE, Key.ArrowRight, EXPECTED_SIMPLE_RIGHT), + moveTest(STATEMENT_SIMPLE.id, Key.ArrowRight, EXPECTED_SIMPLE_RIGHT), ); test( 'moving left', - moveTest(BLOCK_SIMPLE, Key.ArrowLeft, EXPECTED_SIMPLE_LEFT), + moveTest(STATEMENT_SIMPLE.id, Key.ArrowLeft, EXPECTED_SIMPLE_LEFT), ); test( 'moving down', - moveTest(BLOCK_SIMPLE, Key.ArrowDown, EXPECTED_SIMPLE_RIGHT), + moveTest(STATEMENT_SIMPLE.id, Key.ArrowDown, EXPECTED_SIMPLE_RIGHT), ); test( 'moving up', - moveTest(BLOCK_SIMPLE, Key.ArrowUp, EXPECTED_SIMPLE_LEFT), + moveTest(STATEMENT_SIMPLE.id, Key.ArrowUp, EXPECTED_SIMPLE_LEFT), ); }); - /** ID of a statement block with multiple statement inputs. */ - const BLOCK_COMPLEX = 'complex_mover'; - + /** Serialized statement block with multiple statement inputs. */ + const STATEMENT_COMPLEX = { + type: 'controls_if', + id: 'complex_mover', + extraState: {hasElse: true}, + }; /** - * Expected connection candidates when moving BLOCK_COMPLEX, after + * Expected connection candidates when moving STATEMENT_COMPLEX, after * pressing right (or down) arrow n times. */ const EXPECTED_COMPLEX_RIGHT = [ // TODO(#702): Due to a bug in KeyboardDragStrategy, certain // connection candidates that can be found using the mouse are not - // visited when doing a keyboard drag. They appear in the list - // below, but commented out for now. - // is fixed. - {id: 'simple_mover', index: 1, ownIndex: 0}, // Next; starting location. + // visited when doing a keyboard move. They appear in the list + // below, but commented out for now. They should be uncommented + // when bug is fixed. + {id: 'p5_canvas', index: 1, ownIndex: 0}, // Next; starting location again. // {id: 'text_print', index: 0, ownIndex: 1}, // Previous to own next. {id: 'text_print', index: 0, ownIndex: 4}, // Previous to own else input. // {id: 'text_print', index: 0, ownIndex: 3}, // Previous to own if input. @@ -248,10 +254,9 @@ suite('Statement move tests', function () { {id: 'controls_if', index: 6, ownIndex: 0}, // "Else" statement input. {id: 'controls_if', index: 1, ownIndex: 0}, // Next. {id: 'p5_draw', index: 0, ownIndex: 0}, // Statement input. - {id: 'p5_canvas', index: 1, ownIndex: 0}, // Next; starting location again. ]; /** - * Expected connection candidates when moving BLOCK_COMPLEX after + * Expected connection candidates when moving STATEMENT_COMPLEX after * pressing left or up arrow n times. */ const EXPECTED_COMPLEX_LEFT = EXPECTED_COMPLEX_RIGHT.slice(0, 1).concat( @@ -259,21 +264,24 @@ suite('Statement move tests', function () { ); suite('Constrained moves of stack block with statement inputs', function () { + setup(async function () { + await appendBlock(this.browser, STATEMENT_COMPLEX, 'p5_canvas'); + }); test( 'moving right', - moveTest(BLOCK_COMPLEX, Key.ArrowRight, EXPECTED_COMPLEX_RIGHT), + moveTest(STATEMENT_COMPLEX.id, Key.ArrowRight, EXPECTED_COMPLEX_RIGHT), ); test( 'moving left', - moveTest(BLOCK_COMPLEX, Key.ArrowLeft, EXPECTED_COMPLEX_LEFT), + moveTest(STATEMENT_COMPLEX.id, Key.ArrowLeft, EXPECTED_COMPLEX_LEFT), ); test( 'moving down', - moveTest(BLOCK_COMPLEX, Key.ArrowDown, EXPECTED_COMPLEX_RIGHT), + moveTest(STATEMENT_COMPLEX.id, Key.ArrowDown, EXPECTED_COMPLEX_RIGHT), ); test( 'moving up', - moveTest(BLOCK_COMPLEX, Key.ArrowUp, EXPECTED_COMPLEX_LEFT), + moveTest(STATEMENT_COMPLEX.id, Key.ArrowUp, EXPECTED_COMPLEX_LEFT), ); }); @@ -338,27 +346,30 @@ suite(`Value expression move tests`, function () { // timeouts if when non-zero PAUSE_TIME is used to watch tests) run. this.timeout(PAUSE_TIME ? 0 : 10000); - /** ID of a simple reporter (a value block with no inputs). */ - const BLOCK_SIMPLE = 'simple_mover'; - + /** Serialized simple reporter value block with no inputs. */ + const VALUE_SIMPLE = { + type: 'text', + id: 'simple_mover', + fields: {TEXT: 'simple mover'}, + }; /** - * Expected connection candidates when moving BLOCK_SIMPLE, after + * Expected connection candidates when moving VALUE_SIMPLE, after * pressing ArrowRight n times. */ const EXPECTED_SIMPLE_RIGHT = [ - {id: 'complex_mover', index: 1, ownIndex: 0}, // Starting location. + {id: 'join0', index: 1, ownIndex: 0}, // Join block ADD0 input. + {id: 'join0', index: 2, ownIndex: 0}, // Join block ADD1 input. {id: 'print1', index: 2, ownIndex: 0}, // Print block with no shadow. {id: 'print2', index: 2, ownIndex: 0}, // Print block with shadow. // Skip draw_emoji block as it has no value inputs. {id: 'print3', index: 2, ownIndex: 0}, // Replacing join expression. - {id: 'text_join1', index: 1, ownIndex: 0}, // Join block ADD0 input. - {id: 'text_join1', index: 2, ownIndex: 0}, // Join block ADD1 input. + {id: 'join1', index: 1, ownIndex: 0}, // Join block ADD0 input. + {id: 'join1', index: 2, ownIndex: 0}, // Join block ADD1 input. // Skip controls_repeat_ext block's TIMES input as it is incompatible. {id: 'print4', index: 2, ownIndex: 0}, // Replacing join expression. - {id: 'text_join2', index: 1, ownIndex: 0}, // Join block ADD0 input. - {id: 'text_join2', index: 2, ownIndex: 0}, // Join block ADD1 input. - // Skip unconnected text block as it has no inputs. - {id: 'print0', index: 2, ownIndex: 0}, // Print block having complex_mover. + {id: 'join2', index: 1, ownIndex: 0}, // Join block ADD0 input. + {id: 'join2', index: 2, ownIndex: 0}, // Join block ADD1 input. + // Skip input of unattached join block. ]; /** * Expected connection candidates when moving BLOCK_SIMPLE, after @@ -368,50 +379,66 @@ suite(`Value expression move tests`, function () { EXPECTED_SIMPLE_RIGHT.slice(1).reverse(), ); - /** ID of a unary expression block (block with one value input + output) */ - const BLOCK_COMPLEX = 'complex_mover'; - /** - * Expected connection candidates when moving row consisting of - * BLOCK_COMPLEX, with a block (in this case BLOCK_SIMPLE) attached - * to its input, after pressing ArrowRight n times. + * Serialized row of value blocks with no free inputs; should behave + * as VALUE_SIMPLE does. + */ + const VALUE_ROW = { + type: 'text_changeCase', + id: 'row_mover', + fields: {CASE: 'TITLECASE'}, + inputs: { + TEXT: {block: VALUE_SIMPLE}, + }, + }; + // EXPECTED_ROW_RIGHT will be same as EXPECTED_SIMPLE_RIGHT (and + // same for ..._LEFT). + + /** Serialized value block with a single free (external) input. */ + const VALUE_UNARY = { + type: 'text_changeCase', + id: 'unary_mover', + fields: {CASE: 'TITLECASE'}, + }; + /** + * Expected connection candidates when moving VALUE_UNARY after + * pressing ArrowRight n times. */ - const EXPECTED_ROW_RIGHT = EXPECTED_SIMPLE_RIGHT.slice(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - EXPECTED_ROW_RIGHT[0] = EXPECTED_ROW_RIGHT.pop()!; + const EXPECTED_UNARY_RIGHT = EXPECTED_SIMPLE_RIGHT.concat([ + {id: 'join0', index: 0, ownIndex: 1}, // Unattached block to own input. + ]); /** * Expected connection candidates when moving row consisting of - * BLOCK_COMPLEX, with a block (in this case BLOCK_SIMPLE) attached - * to its input, after pressing ArrowLeft n times. + * BLOCK_UNARY on its own after pressing ArrowLEFT n times. */ - const EXPECTED_ROW_LEFT = EXPECTED_ROW_RIGHT.slice(0, 1).concat( - EXPECTED_ROW_RIGHT.slice(1).reverse(), + const EXPECTED_UNARY_LEFT = EXPECTED_UNARY_RIGHT.slice(0, 1).concat( + EXPECTED_UNARY_RIGHT.slice(1).reverse(), ); + /** Serialized value block with a single free (external) input. */ + const VALUE_COMPLEX = { + type: 'text_join', + id: 'complex_mover', + }; /** - * Expected connection candidates when moving row consisting of - * BLOCK_COMPLEX on its own after pressing ArrowRight n times. + * Expected connection candidates when moving VALUE_COMPLEX after + * pressing ArrowRight n times. */ - const EXPECTED_UNARY_RIGHT = [ - {id: 'print0', index: 2, ownIndex: 0}, // Starting location. - {id: 'print1', index: 2, ownIndex: 0}, // Print block with no shadow. - {id: 'print2', index: 2, ownIndex: 0}, // Print block with shadow. - // Skip draw_emoji block as it has no value inputs. - {id: 'print3', index: 2, ownIndex: 0}, // Replacing join expression. - {id: 'text_join1', index: 1, ownIndex: 0}, // Join block ADD0 input. - {id: 'text_join1', index: 2, ownIndex: 0}, // Join block ADD1 input. - // Skip controls_repeat_ext block's TIMES input as it is incompatible. - {id: 'print4', index: 2, ownIndex: 0}, // Replacing join expression. - {id: 'text_join2', index: 1, ownIndex: 0}, // Join block ADD0 input. - {id: 'text_join2', index: 2, ownIndex: 0}, // Join block ADD1 input. - {id: 'unattached', index: 0, ownIndex: 1}, // Unattached text to own input. - ]; + const EXPECTED_COMPLEX_RIGHT = EXPECTED_SIMPLE_RIGHT.concat([ + // TODO(#702): Due to a bug in KeyboardDragStrategy, certain + // connection candidates that can be found using the mouse are not + // visited when doing a keyboard move. They appear in the list + // below, but commented out for now. They should be uncommented + // when bug is fixed. + {id: 'join0', index: 0, ownIndex: 2}, // Unattached block to own input. + // {id: 'join0', index: 0, ownIndex: 1}, // Unattached block to own input. + ]); /** * Expected connection candidates when moving row consisting of - * BLOCK_UNARY on its own after pressing ArrowLEFT n times. + * BLOCK_COMPLEX on its own after pressing ArrowLEFT n times. */ - const EXPECTED_UNARY_LEFT = EXPECTED_UNARY_RIGHT.slice(0, 1).concat( - EXPECTED_UNARY_RIGHT.slice(1).reverse(), + const EXPECTED_COMPLEX_LEFT = EXPECTED_COMPLEX_RIGHT.slice(0, 1).concat( + EXPECTED_COMPLEX_RIGHT.slice(1).reverse(), ); for (const renderer of ['geras', 'thrasos', 'zelos']) { @@ -430,40 +457,59 @@ suite(`Value expression move tests`, function () { }); suite('Constrained moves of a simple reporter block', function () { + setup(async function () { + await appendBlock(this.browser, VALUE_SIMPLE, 'join0', 'ADD0'); + }); test( 'moving right', - moveTest(BLOCK_SIMPLE, Key.ArrowRight, EXPECTED_SIMPLE_RIGHT), + moveTest(VALUE_SIMPLE.id, Key.ArrowRight, EXPECTED_SIMPLE_RIGHT), ); test( 'moving left', - moveTest(BLOCK_SIMPLE, Key.ArrowLeft, EXPECTED_SIMPLE_LEFT), + moveTest(VALUE_SIMPLE.id, Key.ArrowLeft, EXPECTED_SIMPLE_LEFT), ); }); - suite('Constrained moves of two blocks with no free inputs', function () { + + suite('Constrained moves of row of value blocks', function () { + setup(async function () { + await appendBlock(this.browser, VALUE_ROW, 'join0', 'ADD0'); + }); test( 'moving right', - moveTest(BLOCK_COMPLEX, Key.ArrowRight, EXPECTED_ROW_RIGHT), + moveTest(VALUE_ROW.id, Key.ArrowRight, EXPECTED_SIMPLE_RIGHT), ); test( 'moving left', - moveTest(BLOCK_COMPLEX, Key.ArrowLeft, EXPECTED_ROW_LEFT), + moveTest(VALUE_ROW.id, Key.ArrowLeft, EXPECTED_SIMPLE_LEFT), ); }); + suite('Constrained moves of unary expression block', function () { setup(async function () { - // Delete block connected to complex_mover's input. - await focusOnBlock(this.browser, BLOCK_SIMPLE); - await sendKeyAndWait(this.browser, Key.Delete); + await appendBlock(this.browser, VALUE_UNARY, 'join0', 'ADD0'); }); - // TODO(#709): Reenable test once crash bug is fixed. test.skip( 'moving right', - moveTest(BLOCK_COMPLEX, Key.ArrowRight, EXPECTED_UNARY_RIGHT), + moveTest(VALUE_UNARY.id, Key.ArrowRight, EXPECTED_UNARY_RIGHT), ); test( 'moving left', - moveTest(BLOCK_COMPLEX, Key.ArrowLeft, EXPECTED_UNARY_LEFT), + moveTest(VALUE_UNARY.id, Key.ArrowLeft, EXPECTED_UNARY_LEFT), + ); + }); + + suite('Constrained moves of a complex expression block', function () { + setup(async function () { + await appendBlock(this.browser, VALUE_COMPLEX, 'join0', 'ADD0'); + }); + test( + 'moving right', + moveTest(VALUE_COMPLEX.id, Key.ArrowRight, EXPECTED_COMPLEX_RIGHT), + ); + test( + 'moving left', + moveTest(VALUE_COMPLEX.id, Key.ArrowLeft, EXPECTED_COMPLEX_LEFT), ); }); }); @@ -642,3 +688,66 @@ function getConnectionCandidate( return {id: neighbourBlock.id, index, ownIndex}; }); } + +/** + * Create a new block from serialised state (parsed JSON) and + * optionally attach it to an existing block on the workspace. + * + * @param browser The WebdriverIO browser object. + * @param state The JSON definition of the new block. + * @param parentId The ID of the block to attach to. If undefined, the + * new block is not attached. + * @param inputName The name of the input on the parent block to + * attach to. If undefined, the new block is attached to the + * parent's next connection. + * @returns A promise that resolves with the new block's ID. + */ +async function appendBlock( + browser: Browser, + state: Blockly.serialization.blocks.State, + parentId?: string, + inputName?: string, +): Promise { + return await browser.execute( + (state, parentId, inputName) => { + const workspace = Blockly.getMainWorkspace(); + if (!workspace) throw new Error('workspace not found'); + + const block = Blockly.serialization.blocks.append(state, workspace); + if (!block) throw new Error('failed to create block from state'); + if (!parentId) return block.id; + + try { + const parent = workspace.getBlockById(parentId); + if (!parent) throw new Error(`parent block not found: ${parentId}`); + + let parentConnection; + let childConnection; + + if (inputName) { + parentConnection = parent.getInput(inputName)?.connection; + if (!parentConnection) { + throw new Error(`input ${inputName} not found on parent`); + } + childConnection = block.outputConnection ?? block.previousConnection; + } else { + parentConnection = parent.nextConnection; + if (!parentConnection) { + throw new Error('parent has no next connection'); + } + childConnection = block.previousConnection; + } + if (!childConnection) throw new Error('new block not compatible'); + parentConnection.connect(childConnection); + return block.id; + } catch (e) { + // If anything goes wrong during attachment, clean up the new block. + block.dispose(); + throw e; + } + }, + state, + parentId, + inputName, + ); +} From 57bca4840c7c64afae37f4a8ac5ab44674f12e6d Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Fri, 5 Sep 2025 13:31:12 -0700 Subject: [PATCH 14/19] chore: add empty workspace scenario (#716) --- test/index.html | 1 + test/loadTestBlocks.js | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/test/index.html b/test/index.html index 53dbcf3e..9eb2a639 100644 --- a/test/index.html +++ b/test/index.html @@ -118,6 +118,7 @@ move value test blocks +
diff --git a/test/loadTestBlocks.js b/test/loadTestBlocks.js index 0a0b762d..16f0ab81 100644 --- a/test/loadTestBlocks.js +++ b/test/loadTestBlocks.js @@ -1206,6 +1206,12 @@ const comments = { }, }; +const emptyWorkspace = { + 'blocks': { + 'blocks': [], + }, +}; + /** * Loads saved state from local storage into the given workspace. * @param {Blockly.Workspace} workspace Blockly workspace to load into. @@ -1222,6 +1228,7 @@ export const load = function (workspace, scenarioString) { navigationTestBlocks, simpleCircle, 'sun': sunnyDay, + emptyWorkspace, }; // Don't emit events during loading. Blockly.Events.disable(); From 344c1bf97a896762b2cb5334f0b5fd34bd115346 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Fri, 5 Sep 2025 15:48:41 -0700 Subject: [PATCH 15/19] fix: add instructions on how to register toolbox (#715) --- README.md | 22 ++++++++++++++++++++-- src/index.ts | 11 +++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8a549eaa..54dc1aae 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,11 @@ import {KeyboardNavigation} from '@blockly/keyboard-navigation'; // Must be done before calling Blockly.inject. KeyboardNavigation.registerKeyboardNavigationStyles(); +// Register the default toolbox. Only do this once per page-load. +// Must be done before calling Blockly.inject. +// See instructions below if you don't use the default toolbox. +KeyboardNavigation.registerNavigationDeferringToolbox(); + // Inject Blockly. const workspace = Blockly.inject('blocklyDiv', { toolbox: toolboxCategories, @@ -81,7 +86,7 @@ const workspace = Blockly.inject('blocklyDiv', { const keyboardNav = new KeyboardNavigation(workspace); ``` -## Add shortcuts to page +### Add shortcuts to page In order to see the keyboard help popup when the user presses /, you need to add an empty div element to the hosting page that has the Blockly div element with the id "shortcuts". The plugin will take care of layout and formatting. @@ -93,7 +98,20 @@ In order to see the keyboard help popup when the user presses /, you need to add ... ``` -### Usage with cross-tab-copy-paste plugin +### Use with custom Toolbox implementation + +If you supply your own subclass of `Toolbox`, you need to override the `onKeyDown_` method to make it a no-op. The base class has its own keyboard navigation built-in that you need to disable. + +```js +class YourCustomToolbox extends Blockly.Toolbox { + protected override onKeyDown_(e: KeyboardEvent) { + // No-op, prevent keyboard handling by superclass in order to defer to + // global keyboard navigation. + } +} +``` + +### Use with cross-tab-copy-paste plugin This plugin adds context menu items for copying & pasting. It also adds feedback to copying & pasting as toasts that are shown to the user upon successful copy or cut. It is compatible with the `@blockly/plugin-cross-tab-copy-paste` by following these steps: diff --git a/src/index.ts b/src/index.ts index 320d97ab..164bc3ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -139,6 +139,17 @@ export class KeyboardNavigation { this.navigationController.shortcutDialog.toggle(this.workspace); } + /** + * Registers a default toolbox implementation that doesn't handle + * keydown events, since we now handle them in this plugin. If you + * use the default toolbox, call this function before calling + * `Blockly.inject`. If you use a custom toolbox, override the + * `onKeyDown_` method in your toolbox implementation to make it a no-op. + */ + static registerNavigationDeferringToolbox() { + this.registerNavigationDeferringToolbox(); + } + /** * Register CSS used by the plugin. * This is broken up into sections by purpose, with some notes about From 3ef37bc63b48f0b4eb9347c2285d8947cdb1f42a Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 8 Sep 2025 09:58:07 -0700 Subject: [PATCH 16/19] chore: Add the advanced playground to the demo/test page. (#718) * chore: Make the default test page include advanced playground features. * chore: Remove unused import. * fix: Fix registration exceptions on reinjection. --- package-lock.json | 333 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + test/index.ts | 38 ++++-- 3 files changed, 360 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0d2c846f..4456a9fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "devDependencies": { "@blockly/dev-scripts": "^4.0.8", + "@blockly/dev-tools": "^9.0.2", "@blockly/field-colour": "^6.0.2", "@eslint/eslintrc": "^2.1.2", "@eslint/js": "^8.49.0", @@ -323,6 +324,19 @@ "node": ">=6.9.0" } }, + "node_modules/@blockly/block-test": { + "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" + }, + "peerDependencies": { + "blockly": "^12.0.0" + } + }, "node_modules/@blockly/dev-scripts": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/@blockly/dev-scripts/-/dev-scripts-4.0.8.tgz", @@ -761,6 +775,117 @@ "node": ">=10" } }, + "node_modules/@blockly/dev-tools": { + "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.2", + "@blockly/theme-dark": "^8.0.1", + "@blockly/theme-deuteranopia": "^7.0.1", + "@blockly/theme-highcontrast": "^7.0.1", + "@blockly/theme-tritanopia": "^7.0.1", + "chai": "^4.2.0", + "dat.gui": "^0.7.7", + "lodash.assign": "^4.2.0", + "lodash.merge": "^4.6.2", + "monaco-editor": "^0.20.0", + "sinon": "^9.0.2" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "blockly": "^12.0.0" + } + }, + "node_modules/@blockly/dev-tools/node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@blockly/dev-tools/node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@blockly/dev-tools/node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@blockly/dev-tools/node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@blockly/dev-tools/node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/@blockly/dev-tools/node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@blockly/dev-tools/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/@blockly/eslint-config": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@blockly/eslint-config/-/eslint-config-4.0.1.tgz", @@ -1004,6 +1129,58 @@ "blockly": "^12.0.0" } }, + "node_modules/@blockly/theme-dark": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@blockly/theme-dark/-/theme-dark-8.0.1.tgz", + "integrity": "sha512-0Di3WIUwCVQw7jK9myUf/J+4oHLADWc8YxeF40KQgGsyulVrVnYipwtBolj+wxq2xjxIkqgvctAN3BdvM4mynA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.17.0" + }, + "peerDependencies": { + "blockly": "^12.0.0" + } + }, + "node_modules/@blockly/theme-deuteranopia": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@blockly/theme-deuteranopia/-/theme-deuteranopia-7.0.1.tgz", + "integrity": "sha512-V05Hk2hzQZict47LfzDdSTP+J5HlYiF7de/8LR/bsRQB/ft7UUTraqDLIivYc9gL2alsVtKzq/yFs9wi7FMAqQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.17.0" + }, + "peerDependencies": { + "blockly": "^12.0.0" + } + }, + "node_modules/@blockly/theme-highcontrast": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@blockly/theme-highcontrast/-/theme-highcontrast-7.0.1.tgz", + "integrity": "sha512-dMhysbXf8QtHxuhI1EY5GdZErlfEhjpCogwfzglDKSu8MF2C+5qzOQBxKmqfnEYJl6G9B2HNGw+mEaUo8oel6Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.17.0" + }, + "peerDependencies": { + "blockly": "^12.0.0" + } + }, + "node_modules/@blockly/theme-tritanopia": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@blockly/theme-tritanopia/-/theme-tritanopia-7.0.1.tgz", + "integrity": "sha512-eLqPCmW6xvSYvyTFFE5uz0Bw806LxOmaQrCOzbUywkT41s2ITP06OP1BVQrHdkZSt5whipZYpB1RMGxYxS/Bpw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.17.0" + }, + "peerDependencies": { + "blockly": "^12.0.0" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", @@ -1530,6 +1707,45 @@ "node": ">=18" } }, + "node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.3.1.tgz", + "integrity": "sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true, + "license": "(Unlicense OR Apache-2.0)" + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -3996,6 +4212,13 @@ "node": ">=18" } }, + "node_modules/dat.gui": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.9.tgz", + "integrity": "sha512-sCNc1OHobc+Erc1HqiswYgHdVNpSJUlk/Hz8vzOCsER7rl+oF/4+v8GXFUyCgtXpoCX6+bnmg07DedLvBLwYKQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -5659,6 +5882,16 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -6813,6 +7046,13 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true, + "license": "MIT" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6997,12 +7237,27 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -7353,6 +7608,13 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/monaco-editor": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.20.0.tgz", + "integrity": "sha512-hkvf4EtPJRMQlPC3UbMoRs0vTAFAYdzFQ+gpMb8A+9znae1c43q8Mab9iVsgTcg/4PNiLGGn3SlDIa8uvK1FIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7414,6 +7676,37 @@ "node": ">= 0.4.0" } }, + "node_modules/nise": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz", + "integrity": "sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -9018,6 +9311,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sinon": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", + "integrity": "sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==", + "deprecated": "16.1.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^1.8.1", + "@sinonjs/fake-timers": "^6.0.1", + "@sinonjs/samsam": "^5.3.1", + "diff": "^4.0.2", + "nise": "^4.0.4", + "supports-color": "^7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -9814,6 +10137,16 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", diff --git a/package.json b/package.json index 697cd4d2..6d805ced 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ ], "devDependencies": { "@blockly/dev-scripts": "^4.0.8", + "@blockly/dev-tools": "^9.0.2", "@blockly/field-colour": "^6.0.2", "@eslint/eslintrc": "^2.1.2", "@eslint/js": "^8.49.0", diff --git a/test/index.ts b/test/index.ts index 4aa282a8..e785cef1 100644 --- a/test/index.ts +++ b/test/index.ts @@ -24,6 +24,7 @@ import {javascriptGenerator} from 'blockly/javascript'; // @ts-expect-error No types in js file import {load} from './loadTestBlocks'; import {runCode, registerRunCodeShortcut} from './runCode'; +import {createPlayground} from '@blockly/dev-tools'; (window as unknown as {Blockly: typeof Blockly}).Blockly = Blockly; @@ -80,7 +81,7 @@ function getOptions() { * * @returns The created workspace. */ -function createWorkspace(): Blockly.WorkspaceSvg { +async function createWorkspace(): Promise { const {scenario, renderer, toolbox} = getOptions(); const injectOptions = { @@ -96,17 +97,30 @@ function createWorkspace(): Blockly.WorkspaceSvg { KeyboardNavigation.registerKeyboardNavigationStyles(); registerFlyoutCursor(); registerNavigationDeferringToolbox(); - const workspace = Blockly.inject(blocklyDiv, injectOptions); - - Blockly.ContextMenuItems.registerCommentOptions(); - new KeyboardNavigation(workspace); registerRunCodeShortcut(); + Blockly.ContextMenuItems.registerCommentOptions(); - // Disable blocks that aren't inside the setup or draw loops. - workspace.addChangeListener(Blockly.Events.disableOrphans); - - load(workspace, scenario); - runCode(); + let navigation: KeyboardNavigation | null = null; + const workspace = ( + await createPlayground( + blocklyDiv, + (blocklyDiv, options) => { + if (navigation) { + navigation.dispose(); + } + const ws = Blockly.inject(blocklyDiv, options); + navigation = new KeyboardNavigation(ws); + + // Disable blocks that aren't inside the setup or draw loops. + ws.addChangeListener(Blockly.Events.disableOrphans); + + load(ws, scenario); + runCode(); + return ws; + }, + injectOptions, + ) + ).getWorkspace(); return workspace; } @@ -124,9 +138,9 @@ function addP5() { javascriptGenerator.addReservedWords('sketch'); } -document.addEventListener('DOMContentLoaded', () => { +document.addEventListener('DOMContentLoaded', async () => { addP5(); - createWorkspace(); + await createWorkspace(); document.getElementById('run')?.addEventListener('click', runCode); // Add Blockly to the global scope so that test code can access it to // verify state after keypresses. From 4b0b2cc5538a870e667ff76a38bd0362dcb8ab97 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Mon, 8 Sep 2025 09:59:55 -0700 Subject: [PATCH 17/19] fix: dont start a new move when pressing m repeatedly (#717) --- src/actions/mover.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/actions/mover.ts b/src/actions/mover.ts index ebc9b1e4..8c94bbcc 100644 --- a/src/actions/mover.ts +++ b/src/actions/mover.ts @@ -179,6 +179,7 @@ export class Mover { utils.KeyCodes.DOWN, utils.KeyCodes.ENTER, utils.KeyCodes.ESC, + utils.KeyCodes.M, ].includes( typeof keyCode === 'number' ? keyCode From b38e7588998a02a6b82dbf3c8b51b7d0cbfb3420 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Tue, 9 Sep 2025 08:41:49 -0700 Subject: [PATCH 18/19] chore: fix a bug in flaky move test (#720) * chore: fix a bug in flaky move test * chore: add assert to make sure context menu is selected * chore: try using keyboard to start move * chore: lint * Revert "chore: lint" This reverts commit c906f12d7ba99affd029129615eb007d03c75b3c. * Revert "chore: try using keyboard to start move" This reverts commit 6a474f740a15e360eb53a0d5a2309a58156d71d3. * fix: add class to move indicator bubble * chore: minor refactor --- src/move_indicator.ts | 1 + test/webdriverio/test/move_test.ts | 56 +++++++++++++++++++----------- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/src/move_indicator.ts b/src/move_indicator.ts index e6f8e92b..caa5b09e 100644 --- a/src/move_indicator.ts +++ b/src/move_indicator.ts @@ -38,6 +38,7 @@ export class MoveIndicatorBubble {}, workspace.getBubbleCanvas(), ); + this.svgRoot.classList.add('blocklyMoveIndicatorBubble'); const rtl = workspace.RTL; Blockly.utils.dom.createSvgElement( Blockly.utils.Svg.CIRCLE, diff --git a/test/webdriverio/test/move_test.ts b/test/webdriverio/test/move_test.ts index aa441910..34c38313 100644 --- a/test/webdriverio/test/move_test.ts +++ b/test/webdriverio/test/move_test.ts @@ -45,12 +45,15 @@ suite('Move start tests', function () { // and block connected to selected block's next connection. const info = await getFocusedNeighbourInfo(this.browser); - chai.assert(info.parentId, 'selected block has no parent block'); + chai.assert.exists( + info.parentId, + 'selected block should have parent block', + ); chai.assert( typeof info.parentIndex === 'number', - 'parent connection index not found', + 'parent connection index should exist and be a number', ); - chai.assert(info.nextId, 'selected block has no next block'); + chai.assert.exists(info.nextId, 'selected block should have next block'); // Start move using keyboard shortcut. await sendKeyAndWait(this.browser, 'm'); @@ -59,12 +62,12 @@ suite('Move start tests', function () { // next/previous connections, and same thing connected to value // input. const newInfo = await getFocusedNeighbourInfo(this.browser); - chai.assert( - newInfo.parentId === null, + chai.assert.isNull( + newInfo.parentId, 'moving block should have no parent block', ); - chai.assert( - newInfo.nextId === null, + chai.assert.isNull( + newInfo.nextId, 'moving block should have no next block', ); chai.assert.strictEqual( @@ -106,34 +109,47 @@ suite('Move start tests', function () { // and block connected to selected block's value input. const info = await getFocusedNeighbourInfo(this.browser); - chai.assert(info.parentId, 'selected block has no parent block'); + chai.assert.exists( + info.parentId, + 'selected block should have parent block', + ); chai.assert( typeof info.parentIndex === 'number', - 'parent connection index not found', + 'parent connection index should exist and be a number', + ); + chai.assert.exists( + info.valueId, + 'selected block should have child value block', ); - chai.assert(info.valueId, 'selected block has no child value block'); // Start move using context menu (using keyboard nav). await sendKeyAndWait(this.browser, [Key.Ctrl, Key.Return]); - await sendKeyAndWait(this.browser, 'm'); - await keyDown( - this.browser, - (await contextMenuItems(this.browser)).findIndex(({text}) => - text.includes('Move'), - ), + + // Find how many times to press the down arrow + const index = (await contextMenuItems(this.browser)).findIndex(({text}) => + text.includes('Move'), + ); + chai.assert.isAbove( + index, + -1, + 'expected Move to appear in context menu items', ); + await keyDown(this.browser, index); await sendKeyAndWait(this.browser, Key.Return); + // Wait for the move icon to appear so we know we're in move mode. + await this.browser.$('.blocklyMoveIndicatorBubble').waitForExist(); + // Check that the moving block has nothing connected it its // next/previous connections, and same thing connected to value // input. const newInfo = await getFocusedNeighbourInfo(this.browser); - chai.assert( - newInfo.parentId === null, + chai.assert.isNull( + newInfo.parentId, 'moving block should have no parent block', ); - chai.assert( - newInfo.nextId === null, + chai.assert.isNull( + newInfo.nextId, 'moving block should have no next block', ); chai.assert.strictEqual( From e4ad0d96307df00863a0786d22e000951d5081e4 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 15 Sep 2025 14:57:13 -0700 Subject: [PATCH 19/19] fix: Don't try to move blocks onto shadow block connections. (#729) --- src/keyboard_drag_strategy.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/keyboard_drag_strategy.ts b/src/keyboard_drag_strategy.ts index d5729ad7..5a5ef209 100644 --- a/src/keyboard_drag_strategy.ts +++ b/src/keyboard_drag_strategy.ts @@ -54,6 +54,7 @@ export class KeyboardDragStrategy extends dragging.BlockDragStrategy { this.allConnections.push( ...topBlock .getDescendants(true) + .filter((block: BlockSvg) => !block.isShadow()) .flatMap((block: BlockSvg) => block.getConnections_(false)) .sort((a: RenderedConnection, b: RenderedConnection) => { let delta = a.y - b.y;