diff --git a/.gitignore b/.gitignore index f23a8695794..d5332699e27 100644 --- a/.gitignore +++ b/.gitignore @@ -21,9 +21,10 @@ packages/*/test/**/*.d.ts.map # Core packages/blockly/tests/compile/main_compressed.js packages/blockly/tests/compile/main_compressed.js.map +packages/blockly/tests/mocha/test-modules.generated.mjs packages/blockly/temp/ -# Docs: +# Docs: # Autogenerated reference docs, do not check in packages/docs/docs/reference/ packages/docs/.docusaurus diff --git a/package-lock.json b/package-lock.json index fd1402fa05b..49648d83df0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9284,6 +9284,14 @@ "node": ">=4" } }, + "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==", + "deprecated": "Deprecated: no longer maintained and no longer used by Sinon packages. See\n https://github.com/sinonjs/nise/issues/243 for replacement details.", + "dev": true, + "license": "(Unlicense OR Apache-2.0)" + }, "node_modules/@slorber/remark-comment": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@slorber/remark-comment/-/remark-comment-1.0.0.tgz", @@ -19910,6 +19918,13 @@ "dev": true, "license": "MIT" }, + "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", @@ -20459,6 +20474,14 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "license": "MIT" }, + "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.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -23469,6 +23492,40 @@ "dev": true, "license": "ISC" }, + "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/@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/nise/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/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -32350,6 +32407,7 @@ "puppeteer-core": "^25.3.0", "readline-sync": "^1.4.10", "rimraf": "^6.1.3", + "sinon": "^9.2.4", "typescript": "^6.0.3", "typescript-eslint": "^8.62.1", "webdriverio": "^9.30.0", @@ -32362,6 +32420,38 @@ "jsdom": ">=27.4.0 <30.0.0" } }, + "packages/blockly/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" + } + }, + "packages/blockly/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" + } + }, + "packages/blockly/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" + } + }, "packages/blockly/node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -32385,6 +32475,16 @@ "node": "20 || >=22" } }, + "packages/blockly/node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "packages/blockly/node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -32446,6 +32546,26 @@ "url": "https://github.com/sponsors/isaacs" } }, + "packages/blockly/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" + } + }, "packages/docs": { "name": "blockly-docs", "version": "0.0.0", diff --git a/packages/blockly/core/comments/rendered_workspace_comment.ts b/packages/blockly/core/comments/rendered_workspace_comment.ts index f60e5bae865..b6eb4541bdd 100644 --- a/packages/blockly/core/comments/rendered_workspace_comment.ts +++ b/packages/blockly/core/comments/rendered_workspace_comment.ts @@ -201,7 +201,11 @@ export class RenderedWorkspaceComment this.disposing = true; const focusManager = getFocusManager(); if (focusManager.getFocusedNode() === this) { - setTimeout(() => focusManager.focusTree(this.workspace), 0); + setTimeout(() => { + if (focusManager.isRegistered(this.workspace)) { + focusManager.focusTree(this.workspace); + } + }, 0); } if (!this.view.isDeadOrDying()) this.view.dispose(); diff --git a/packages/blockly/package.json b/packages/blockly/package.json index 7018ad0bd21..e2243ffb897 100644 --- a/packages/blockly/package.json +++ b/packages/blockly/package.json @@ -19,6 +19,10 @@ "browser": { "jsdom": false }, + "imports": { + "#core/*": "./build/src/core/*", + "#generators/*": "./build/src/generators/*" + }, "scripts": { "build": "gulp build", "build-debug": "gulp build --verbose --debug", @@ -42,6 +46,7 @@ "tsc": "gulp tsc", "test": "gulp test", "test:browser": "npx mocha --config tests/browser/.mocharc.js", + "test:mocha:node": "npx mocha --config tests/mocha/.mocharc.node.cjs \"tests/mocha/**/*_test.js\"", "test:generators": "gulp testGenerators", "test:mocha:interactive": "npm run build && concurrently -n tsc,server \"tsc --watch --preserveWatchOutput --outDir \"build/src\" --declarationDir \"build/declarations\"\" \"gulp interactiveMocha\"", "test:compile:advanced": "gulp buildAdvancedCompilationTest --debug", @@ -130,6 +135,7 @@ "puppeteer-core": "^25.3.0", "readline-sync": "^1.4.10", "rimraf": "^6.1.3", + "sinon": "^9.2.4", "typescript": "^6.0.3", "typescript-eslint": "^8.62.1", "webdriverio": "^9.30.0", diff --git a/packages/blockly/scripts/gulpfiles/test_tasks.mjs b/packages/blockly/scripts/gulpfiles/test_tasks.mjs index d32769bb89a..53af411477a 100644 --- a/packages/blockly/scripts/gulpfiles/test_tasks.mjs +++ b/packages/blockly/scripts/gulpfiles/test_tasks.mjs @@ -10,11 +10,12 @@ /* eslint-env node */ import asyncDone from 'async-done'; +import {spawnSync} from 'child_process'; +import * as fs from 'fs'; +import {globSync} from 'glob'; import * as gulp from 'gulp'; import gzip from 'gulp-gzip'; -import * as fs from 'fs'; import * as path from 'path'; -import {spawnSync} from 'child_process'; import {rimraf} from 'rimraf'; import {RELEASE_DIR, TEST_TSC_OUTPUT_DIR} from './config.mjs'; @@ -263,6 +264,22 @@ async function metadata() { }); } +/** + * Generates tests/mocha/test-modules.generated.mjs, + * the list of tests imported by the browser harness (tests/mocha/index.html). + * Keeping it generated from a glob means new *_test.js files are picked up + * automatically and the browser and Node runners stay in sync. + * @return {Promise} Asynchronous result. + */ +function generateMochaIndex() { + return runTestTask('generateMochaIndex', async () => { + const files = globSync('**/*_test.js', {cwd: 'tests/mocha'}).sort(); + + const body = files.map((f) => `import './${f}';`).join('\n') + '\n'; + fs.writeFileSync('tests/mocha/test-modules.generated.mjs', body); + }); +} + /** * Run Mocha tests inside a browser. * @return {Promise} Asynchronous result. @@ -277,16 +294,19 @@ function mocha() { * Run Mocha tests inside a browser and keep the browser open upon completion. * @return {Promise} Asynchronous result. */ -export function interactiveMocha() { - return runTestTask('interactiveMocha', () => { - return runMochaTestsInBrowser(false).then((result) => { - if (result) { - throw new Error('Mocha tests failed'); - } - console.log('Mocha tests passed'); +export const interactiveMocha = gulp.series( + generateMochaIndex, + function interactiveMochaRun() { + return runTestTask('interactiveMocha', () => { + return runMochaTestsInBrowser(false).then((result) => { + if (result) { + throw new Error('Mocha tests failed'); + } + console.log('Mocha tests passed'); + }); }); - }); -} + }, +); /** * Helper method for comparison file. @@ -373,14 +393,6 @@ export async function generators() { }); } -/** - * Run Node tests. - * @return {Promise} Asynchronous result. - */ -function node() { - return runTestCommand('node', 'mocha tests/node --config tests/node/.mocharc.js'); -} - /** * Attempt advanced compilation of a Blockly app. * @returns {Promise} Async result. @@ -417,9 +429,9 @@ const tasks = [ // Build must run before the remaining tasks build, renamings, + generateMochaIndex, mocha, generators, - node, typeDefinitions, // Make sure these two are in series with each other advancedCompile, diff --git a/packages/blockly/tests/browser/test/dragger_test.mjs b/packages/blockly/tests/browser/test/dragger_test.mjs new file mode 100644 index 00000000000..9fe0b4ff103 --- /dev/null +++ b/packages/blockly/tests/browser/test/dragger_test.mjs @@ -0,0 +1,373 @@ +/** + * @license + * Copyright 2026 Raspberry Pi Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as chai from 'chai'; +import {testFileLocations, testSetup} from './test_setup.mjs'; + +suite('Dragging into a delete area', function () { + suiteSetup(async function () { + this.browser = await testSetup(testFileLocations.PLAYGROUND); + + await this.browser.execute(() => { + /** + * @param {!Blockly.BlockSvg} block The block to measure. + * @returns {{x: number, y: number}} Viewport coordinates at the block center. + */ + window.blockCenterClient = function (block) { + const boundingRect = block.getSvgRoot().getBoundingClientRect(); + return { + x: (boundingRect.left + boundingRect.right) / 2, + y: (boundingRect.top + boundingRect.bottom) / 2, + }; + }; + + /** + * @param {!Blockly.BlockSvg} block The block to measure. + * @returns {{x: number, y: number}} Viewport coordinates at the block origin. + */ + window.blockOriginClient = function (block) { + const ws = block.workspace; + let point = block.getRelativeToSurfaceXY(); + if (ws.isMutator) { + point = point.scale(ws.options.parentWorkspace.scale); + } + const screenCoords = Blockly.utils.svgMath.wsToScreenCoordinates( + ws, + point, + ); + return {x: screenCoords.x, y: screenCoords.y}; + }; + + /** + * Simulates pressing on the block center and dragging to a viewport point. + * + * @param {!Blockly.BlockSvg} block The block to drag. + * @param {{x: number, y: number}} pointerEnd The viewport point to drag to. + * @returns {{dragger: !Blockly.dragging.Dragger, dragEvent: !PointerEvent, block: !Blockly.BlockSvg}} + * The dragger, final pointer event, and block being dragged. + */ + window.dragBlock = function (block, pointerEnd) { + const start = blockCenterClient(block); + const totalDelta = new Blockly.utils.Coordinate( + pointerEnd.x - start.x, + pointerEnd.y - start.y, + ); + + const dragger = new Blockly.dragging.Dragger(block); + const dragStartEvent = pointerAt(start.x, start.y, 'pointerdown'); + const dragEvent = pointerAt(pointerEnd.x, pointerEnd.y); + + dragger.onDragStart(dragStartEvent); + dragger.onDrag(dragEvent, totalDelta); + + return {dragger, dragEvent, block: dragger.draggable}; + }; + + window.hasDeleteStyle = function (block) { + return block.getSvgRoot().classList.contains('blocklyDraggingDelete'); + }; + + /** + * @param {number} clientX The viewport x coordinate. + * @param {number} clientY The viewport y coordinate. + * @param {string=} type The pointer event type. + * @returns {!PointerEvent} A synthetic pointer event at the given location. + */ + window.pointerAt = function (clientX, clientY, type = 'pointermove') { + return new PointerEvent(type, {clientX, clientY}); + }; + + /** + * Opens a mutator on a controls_if block and returns the mutator workspace. + * + * @param {!Blockly.WorkspaceSvg} workspace The main workspace. + * @returns {!Promise} The mutator workspace. + */ + window.openMutator = async function (workspace) { + const block = Blockly.serialization.blocks.append( + { + 'type': 'controls_if', + 'extraState': { + 'elseIfCount': 0, + }, + }, + workspace, + ); + block.initSvg(); + block.render(); + const icon = block.getIcon(Blockly.icons.MutatorIcon.TYPE); + await icon.setBubbleVisible(true); + return icon.getWorkspace(); + }; + + /** + * @param {!Blockly.utils.Rect} rect The rectangle to measure. + * @returns {{x: number, y: number}} Viewport coordinates at the rect center. + */ + window.rectCenterClient = function (rect) { + return { + x: (rect.left + rect.right) / 2, + y: (rect.top + rect.bottom) / 2, + }; + }; + + /** + * @param {!Blockly.WorkspaceSvg} workspace The workspace with a trashcan. + * @returns {boolean} Whether the trashcan lid open style is applied. + */ + window.hasTrashLidOpen = function (workspace) { + return workspace.trashcan?.svgGroup.classList.contains( + 'blocklyTrashOpen', + ); + }; + + /** + * @param {!Blockly.WorkspaceSvg} workspace The workspace to zoom. + * @param {number} scale The target zoom factor. + */ + window.setWorkspaceScale = function (workspace, scale) { + workspace.setScale(scale); + }; + + window.getAssertionState = function (block, dragDelta, deleteAreaRect) { + const { + dragger, + dragEvent, + block: draggedBlock, + } = dragBlock(block, dragDelta); + + const originAfter = blockOriginClient(draggedBlock); + const deleteAreaContainsBlockOrigin = deleteAreaRect.contains( + originAfter.x, + originAfter.y, + ); + const deleteAreaContainsCursor = deleteAreaRect.contains( + dragEvent.clientX, + dragEvent.clientY, + ); + const blockHasDeleteStyle = hasDeleteStyle(draggedBlock); + + const trashLidOpen = hasTrashLidOpen(draggedBlock.workspace); + + dragger.onDragEnd(dragEvent); + + const blockIsDeadOrDying = draggedBlock.isDeadOrDying(); + + return { + deleteAreaContainsBlockOrigin, + deleteAreaContainsCursor, + blockHasDeleteStyle, + blockIsDeadOrDying, + trashLidOpen, + }; + }; + }); + }); + + suiteTeardown(async function () { + await this.browser.execute(() => { + delete window.blockCenterClient; + delete window.blockOriginClient; + delete window.dragBlock; + delete window.hasDeleteStyle; + delete window.pointerAt; + delete window.openMutator; + delete window.rectCenterClient; + delete window.hasTrashLidOpen; + delete window.setWorkspaceScale; + delete window.getAssertionState; + }); + }); + + test('does not apply delete styling when only block origin overlaps delete area', async function () { + const { + deleteAreaContainsBlockOrigin, + deleteAreaContainsCursor, + blockHasDeleteStyle, + blockIsDeadOrDying, + } = await this.browser.execute(() => { + const block = Blockly.getMainWorkspace().newBlock('controls_if'); + block.initSvg(); + block.render(); + + const start = blockCenterClient(block); + const originBefore = blockOriginClient(block); + const deleteAreaRect = Blockly.getMainWorkspace().toolbox.getClientRect(); + const desiredOrigin = { + x: deleteAreaRect.right - 5, + y: originBefore.y, + }; + const dragDelta = { + x: start.x + desiredOrigin.x - originBefore.x, + y: start.y + desiredOrigin.y - originBefore.y, + }; + + return getAssertionState(block, dragDelta, deleteAreaRect); + }); + + chai.assert.isTrue( + deleteAreaContainsBlockOrigin, + 'Expected block origin to overlap delete area', + ); + chai.assert.isFalse( + deleteAreaContainsCursor, + 'Expected cursor to be outside delete area', + ); + chai.assert.isFalse(blockHasDeleteStyle); + chai.assert.isFalse(blockIsDeadOrDying); + }); + + test('does not apply delete styling when only block origin overlaps flyout delete area at zoomed scale', async function () { + const { + flyoutRect, + deleteAreaContainsBlockOrigin, + deleteAreaContainsCursor, + blockHasDeleteStyle, + blockIsDeadOrDying, + } = await this.browser.execute(async () => { + const workspace = Blockly.getMainWorkspace(); + + for (let i = 0; i < 3; i++) { + workspace.zoomCenter(1); + } + + const mutatorWorkspace = await openMutator(workspace); + mutatorWorkspace.recordDragTargets(); + + const flyout = mutatorWorkspace.getFlyout(); + const flyoutRect = flyout.getClientRect(); + + const workspaceBlock = mutatorWorkspace.newBlock('controls_if_elseif'); + workspaceBlock.initSvg(); + workspaceBlock.render(); + workspaceBlock.moveBy(200, 50); + + const start = blockCenterClient(workspaceBlock); + const originBefore = blockOriginClient(workspaceBlock); + const desiredOrigin = { + x: flyoutRect.right - 5, + y: originBefore.y, + }; + const dragDelta = { + x: start.x + desiredOrigin.x - originBefore.x, + y: start.y + desiredOrigin.y - originBefore.y, + }; + + return getAssertionState(workspaceBlock, dragDelta, flyoutRect); + }); + + // chai.assert.isNotNull(flyoutRect); + chai.assert.isTrue( + deleteAreaContainsBlockOrigin, + 'Expected block origin to overlap flyout delete area', + ); + chai.assert.isFalse( + deleteAreaContainsCursor, + 'Expected cursor to be outside flyout delete area', + ); + chai.assert.isFalse(blockHasDeleteStyle); + chai.assert.isFalse(blockIsDeadOrDying); + }); + + test('deletes flyout block when pointer is over flyout delete area at zoomed scale', async function () { + const { + flyoutRect, + deleteAreaContainsCursor, + blockHasDeleteStyle, + blockIsDeadOrDying, + } = await this.browser.execute(async () => { + const workspace = Blockly.getMainWorkspace(); + + for (let i = 0; i < 3; i++) { + workspace.zoomCenter(1); + } + + const mutatorWorkspace = await openMutator(workspace); + mutatorWorkspace.recordDragTargets(); + + const flyout = mutatorWorkspace.getFlyout(); + const flyoutRect = flyout.getClientRect(); + + const flyoutBlock = flyout + .getWorkspace() + .getBlocksByType('controls_if_elseif')[0]; + flyoutBlock.initSvg(); + flyoutBlock.render(); + + return getAssertionState( + flyoutBlock, + rectCenterClient(flyoutRect), + flyoutRect, + ); + }); + + chai.assert.isTrue( + deleteAreaContainsCursor, + 'Expected cursor to be inside flyout delete area', + ); + chai.assert.isTrue(blockHasDeleteStyle); + chai.assert.isTrue(blockIsDeadOrDying); + }); + + const zoomLevels = [ + {name: 'default scale', scale: null}, + {name: 'zoomed in', scale: 1.5}, + {name: 'zoomed out', scale: 0.7}, + ]; + + zoomLevels.forEach(({name: zoomName, scale}) => { + [ + {name: 'trashcan', rectKey: 'trashRect', checkLid: true}, + {name: 'toolbox', rectKey: 'toolboxRect', checkLid: false}, + ].forEach(({name, rectKey, checkLid}) => { + test(`applies delete styling and deletes when dragged to ${name} at ${zoomName}`, async function () { + const { + deleteAreaContainsCursor, + trashLidOpen, + blockHasDeleteStyle, + blockIsDeadOrDying, + } = await this.browser.execute( + (scale, rectKey) => { + const workspace = Blockly.getMainWorkspace(); + + const block = workspace.newBlock('controls_if'); + block.initSvg(); + block.render(); + + if (scale !== null) { + setWorkspaceScale(workspace, scale); + this.trashRect = workspace.trashcan.getClientRect(); + this.toolboxRect = workspace.toolbox.getClientRect(); + } + + const deleteRect = + rectKey === 'trashRect' + ? workspace.trashcan.getClientRect() + : workspace.toolbox.getClientRect(); + + return getAssertionState( + block, + rectCenterClient(deleteRect), + deleteRect, + ); + }, + scale, + rectKey, + ); + + chai.assert.isTrue( + deleteAreaContainsCursor, + `Expected cursor to be inside ${name} delete area`, + ); + chai.assert.isTrue(blockHasDeleteStyle); + if (checkLid) { + chai.assert.isTrue(trashLidOpen, 'Expected trashcan lid to be open'); + } + chai.assert.isTrue(blockIsDeadOrDying); + }); + }); + }); +}); diff --git a/packages/blockly/tests/browser/test/dropdowndiv_test.mjs b/packages/blockly/tests/browser/test/dropdowndiv_test.mjs new file mode 100644 index 00000000000..1842c58c26a --- /dev/null +++ b/packages/blockly/tests/browser/test/dropdowndiv_test.mjs @@ -0,0 +1,47 @@ +/** + * @license + * Copyright 2026 Raspberry Pi Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as chai from 'chai'; +import {testFileLocations, testSetup} from './test_setup.mjs'; + +suite('DropDownDiv', function () { + suiteSetup(async function () { + this.browser = await testSetup(testFileLocations.PLAYGROUND); + }); + + test('show() with bounds set positions and shows div near specified location', async function () { + const result = await this.browser.execute(() => { + const workspace = Blockly.getMainWorkspace(); + Blockly.DropDownDiv.setBoundsElement(document.body); + const block = Blockly.serialization.blocks.append( + {'type': 'text', 'fields': {'TEXT': ''}}, + workspace, + ); + const field = Array.from(block.getFields())[0]; + + Blockly.DropDownDiv.show(field, false, 50, 60, 70, 80, false); + + const dropDownDivElem = document.querySelector('.blocklyDropDownDiv'); + const bounds = dropDownDivElem.getBoundingClientRect(); + return { + opacity: dropDownDivElem.style.opacity, + left: bounds.left, + top: bounds.top, + width: bounds.width, + }; + }); + + chai.assert.strictEqual( + result.opacity, + '1', + 'Expected the div to be shown', + ); + // The div is centered horizontally on the requested x, and its top edge + // sits at the requested y. + chai.assert.strictEqual(result.left, 50 - result.width / 2); + chai.assert.strictEqual(result.top, 60); + }); +}); diff --git a/packages/blockly/tests/browser/test/focus_manager_test.mjs b/packages/blockly/tests/browser/test/focus_manager_test.mjs new file mode 100644 index 00000000000..6dd80918edc --- /dev/null +++ b/packages/blockly/tests/browser/test/focus_manager_test.mjs @@ -0,0 +1,295 @@ +/** + * @license + * Copyright 2026 Raspberry Pi Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview FocusManager tests that depend on the browser's own focus + * handling, and so cannot run in the headless JSDOM harness alongside the rest + * of tests/mocha/focus_manager_test.js. + * + * Two behaviors jsdom does not reproduce are covered here: + * + * - Detaching the focused element. Browsers reset document.activeElement to + * and fire the corresponding events; JSDOM leaves activeElement + * pointing at the removed element, so FocusManager still reports it as + * focused. + * - Moving DOM focus re-entrantly from within a focusin handler. When a tree's + * root receives focus, FocusManager responds by focusing the tree's previously + * focused node instead (see focusTree()). JSDOM does not carry that nested + * focus() through, so the node never becomes active. + */ + +import * as chai from 'chai'; +import {FIXTURE_HTML} from '../../mocha/test_helpers/dom_fixtures.js'; +import {focusableTreeImplFactory} from '../../mocha/test_helpers/focusable_tree.js'; +import {testFileLocations, testSetup} from './test_setup.mjs'; + +/** + * Installs the shared DOM fixtures, the shared FocusableTreeImpl, and the + * createFocusableTree/createFocusableNode helpers. + * + * @param {!WebdriverIO.Browser} browser The active WebdriverIO Browser object. + * @param {string} fixtureHtml The shared fixture markup to install. + * @return {!Promise} A Promise that resolves once the page is prepared. + */ +async function installFocusTestHelpers(browser, fixtureHtml) { + await browser.execute( + `window.FocusableTreeImpl = (${focusableTreeImplFactory.toString()})();`, + ); + + await browser.execute((fixtureHtml) => { + document.body.insertAdjacentHTML('beforeend', fixtureHtml); + + window.createFocusableTree = function (rootElementId, nestedTrees) { + return new window.FocusableTreeImpl( + document.getElementById(rootElementId), + nestedTrees || [], + ); + }; + window.createFocusableNode = function (tree, elementId) { + return tree.addNode(document.getElementById(elementId)); + }; + }, fixtureHtml); +} + +suite('FocusManager', function () { + suiteSetup(async function () { + this.browser = await testSetup(testFileLocations.PLAYGROUND); + await installFocusTestHelpers(this.browser, FIXTURE_HTML); + }); + + suite('focus*() switching in HTML tree', function () { + suite('getFocusedNode()', function () { + test('deletion after focusNode() returns null', async function () { + const result = await this.browser.execute(() => { + const focusManager = Blockly.getFocusManager(); + + const rootElem = document.createElement('div'); + const nodeElem = document.createElement('div'); + rootElem.setAttribute('id', 'focusRoot'); + rootElem.setAttribute('tabindex', '-1'); + nodeElem.setAttribute('id', 'focusNode'); + nodeElem.setAttribute('tabindex', '-1'); + nodeElem.textContent = 'Focusable node'; + rootElem.appendChild(nodeElem); + document.body.appendChild(rootElem); + const root = createFocusableTree('focusRoot'); + const node = createFocusableNode(root, 'focusNode'); + focusManager.registerTree(root); + focusManager.focusNode(node); + const focusedBeforeDeletion = focusManager.getFocusedNode() === node; + + node.getFocusableElement().remove(); + + const focusedAfterDeletion = focusManager.getFocusedNode() === node; + focusManager.unregisterTree(root); + rootElem.remove(); // Cleanup. + return {focusedBeforeDeletion, focusedAfterDeletion}; + }); + + chai.assert.isTrue( + result.focusedBeforeDeletion, + 'Expected focusNode() to have focused the node', + ); + chai.assert.isFalse( + result.focusedAfterDeletion, + 'Expected the deleted node to no longer be focused', + ); + }); + }); + }); + + suite('DOM focus() switching in HTML tree', function () { + suite('getFocusedNode()', function () { + test('deletion after focus() returns null', async function () { + const result = await this.browser.execute(() => { + const focusManager = Blockly.getFocusManager(); + + const rootElem = document.createElement('div'); + const nodeElem = document.createElement('div'); + rootElem.setAttribute('id', 'focusRoot'); + rootElem.setAttribute('tabindex', '-1'); + nodeElem.setAttribute('id', 'focusNode'); + nodeElem.setAttribute('tabindex', '-1'); + nodeElem.textContent = 'Focusable node'; + rootElem.appendChild(nodeElem); + document.body.appendChild(rootElem); + const root = createFocusableTree('focusRoot'); + const node = createFocusableNode(root, 'focusNode'); + focusManager.registerTree(root); + document.getElementById('focusNode').tabIndex = -1; + document.getElementById('focusNode').focus(); + const focusedBeforeDeletion = focusManager.getFocusedNode() === node; + + node.getFocusableElement().remove(); + + const focusedAfterDeletion = focusManager.getFocusedNode() === node; + focusManager.unregisterTree(root); + rootElem.remove(); // Cleanup. + return {focusedBeforeDeletion, focusedAfterDeletion}; + }); + + chai.assert.isTrue( + result.focusedBeforeDeletion, + 'Expected focus() to have focused the node', + ); + chai.assert.isFalse( + result.focusedAfterDeletion, + 'Expected the deleted node to no longer be focused', + ); + }); + }); + + suite('CSS classes', function () { + test('registered tree focus()ed other tree node passively focused tree node now has active property', async function () { + const focusClasses = await this.browser.execute(() => { + const focusManager = Blockly.getFocusManager(); + const testFocusableTree1 = createFocusableTree('testFocusableTree1'); + const testFocusableTree1Node1 = createFocusableNode( + testFocusableTree1, + 'testFocusableTree1.node1', + ); + const testFocusableTree2 = createFocusableTree('testFocusableTree2'); + createFocusableNode(testFocusableTree2, 'testFocusableTree2.node1'); + + focusManager.registerTree(testFocusableTree1); + focusManager.registerTree(testFocusableTree2); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableTree1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableTree1').focus(); + + // Directly refocusing a tree's root should have functional parity with focusTree(). That + // means the tree's previous node should now have active focus again and its root should + // have no focus indication. + const rootElem = testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + const nodeElem = testFocusableTree1Node1.getFocusableElement(); + const { + ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + } = Blockly.FocusManager; + const focusClasses = { + nodeActive: nodeElem.classList.contains( + ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ), + nodePassive: nodeElem.classList.contains( + PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ), + rootActive: rootElem.classList.contains( + ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ), + rootPassive: rootElem.classList.contains( + PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ), + }; + + focusManager.unregisterTree(testFocusableTree1); + focusManager.unregisterTree(testFocusableTree2); + return focusClasses; + }); + + chai.assert.isTrue( + focusClasses.nodeActive, + "Expected the tree's node to have active focus", + ); + chai.assert.isFalse( + focusClasses.nodePassive, + "Expected the tree's node to not have passive focus", + ); + chai.assert.isFalse( + focusClasses.rootActive, + "Expected the tree's root to not have active focus", + ); + chai.assert.isFalse( + focusClasses.rootPassive, + "Expected the tree's root to not have passive focus", + ); + }); + }); + }); + + suite('DOM focus() switching in SVG tree', function () { + suite('CSS classes', function () { + test('registered tree focus()ed other tree node passively focused tree node now has active property', async function () { + const focusClasses = await this.browser.execute(() => { + const focusManager = Blockly.getFocusManager(); + const testFocusableGroup1 = createFocusableTree( + 'testFocusableGroup1', + ); + const testFocusableGroup1Node1 = createFocusableNode( + testFocusableGroup1, + 'testFocusableGroup1.node1', + ); + const testFocusableGroup2 = createFocusableTree( + 'testFocusableGroup2', + ); + createFocusableNode(testFocusableGroup2, 'testFocusableGroup2.node1'); + + focusManager.registerTree(testFocusableGroup1); + focusManager.registerTree(testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableGroup1').focus(); + + // Directly refocusing a tree's root should have functional parity with focusTree(). That + // means the tree's previous node should now have active focus again and its root should + // have no focus indication. + const rootElem = testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + const nodeElem = testFocusableGroup1Node1.getFocusableElement(); + const { + ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + } = Blockly.FocusManager; + const focusClasses = { + nodeActive: nodeElem.classList.contains( + ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ), + nodePassive: nodeElem.classList.contains( + PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ), + rootActive: rootElem.classList.contains( + ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ), + rootPassive: rootElem.classList.contains( + PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ), + }; + + focusManager.unregisterTree(testFocusableGroup1); + focusManager.unregisterTree(testFocusableGroup2); + return focusClasses; + }); + + chai.assert.isTrue( + focusClasses.nodeActive, + "Expected the tree's node to have active focus", + ); + chai.assert.isFalse( + focusClasses.nodePassive, + "Expected the tree's node to not have passive focus", + ); + chai.assert.isFalse( + focusClasses.rootActive, + "Expected the tree's root to not have active focus", + ); + chai.assert.isFalse( + focusClasses.rootPassive, + "Expected the tree's root to not have passive focus", + ); + }); + }); + }); +}); diff --git a/packages/blockly/tests/mocha/.mocharc.node.cjs b/packages/blockly/tests/mocha/.mocharc.node.cjs new file mode 100644 index 00000000000..ee49831778b --- /dev/null +++ b/packages/blockly/tests/mocha/.mocharc.node.cjs @@ -0,0 +1,22 @@ +'use strict'; + +/** + * Mocha configuration for running the Blockly unit suite headless under Node + + * jsdom. See tests/mocha/node-setup.mjs for the DOM/global bootstrap. + * + * To run or debug a single file: + * + * npx mocha --config tests/mocha/.mocharc.node.cjs tests/mocha/foo_test.js + * + * Console output from the tests is suppressed; set BLOCKLY_TEST_CONSOLE=1 to + * see it. See tests/mocha/node-setup.mjs. + */ + +module.exports = { + ui: 'tdd', + reporter: 'spec', + parallel: true, + require: ['./tests/mocha/node-setup.mjs'], + timeout: 10000, + exit: true, +}; diff --git a/packages/blockly/tests/mocha/aria_test.js b/packages/blockly/tests/mocha/aria_test.js index 20bb35aa5c6..b68954a06da 100644 --- a/packages/blockly/tests/mocha/aria_test.js +++ b/packages/blockly/tests/mocha/aria_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {getInputLabelsSubset} from '../../build/src/core/block_aria_composer.js'; -import {assert} from '../../node_modules/chai/index.js'; +import {getInputLabelsSubset} from '#core/block_aria_composer.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/block_json_test.js b/packages/blockly/tests/mocha/block_json_test.js index 02ed05a012d..507738ecc9a 100644 --- a/packages/blockly/tests/mocha/block_json_test.js +++ b/packages/blockly/tests/mocha/block_json_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {Align} from '../../build/src/core/inputs/align.js'; -import {assert} from '../../node_modules/chai/index.js'; +import {Align} from '#core/inputs/align.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/block_test.js b/packages/blockly/tests/mocha/block_test.js index 9762f466ce5..d7c4dd99bda 100644 --- a/packages/blockly/tests/mocha/block_test.js +++ b/packages/blockly/tests/mocha/block_test.js @@ -4,14 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {ConnectionType} from '../../build/src/core/connection_type.js'; -import {EventType} from '../../build/src/core/events/type.js'; -import * as eventUtils from '../../build/src/core/events/utils.js'; -import {IconType} from '../../build/src/core/icons/icon_types.js'; -import {EndRowInput} from '../../build/src/core/inputs/end_row_input.js'; -import {isCommentIcon} from '../../build/src/core/interfaces/i_comment_icon.js'; -import {Size} from '../../build/src/core/utils/size.js'; -import {assert} from '../../node_modules/chai/index.js'; +import {ConnectionType} from '#core/connection_type.js'; +import {EventType} from '#core/events/type.js'; +import * as eventUtils from '#core/events/utils.js'; +import {IconType} from '#core/icons/icon_types.js'; +import {EndRowInput} from '#core/inputs/end_row_input.js'; +import {isCommentIcon} from '#core/interfaces/i_comment_icon.js'; +import {Size} from '#core/utils/size.js'; +import {assert} from 'chai'; import {createRenderedBlock} from './test_helpers/block_definitions.js'; import { createChangeListenerSpy, diff --git a/packages/blockly/tests/mocha/blocks/lists_test.js b/packages/blockly/tests/mocha/blocks/lists_test.js index e749fae90a7..9fafbb54788 100644 --- a/packages/blockly/tests/mocha/blocks/lists_test.js +++ b/packages/blockly/tests/mocha/blocks/lists_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {ConnectionType} from '../../../build/src/core/connection_type.js'; -import {assert} from '../../../node_modules/chai/index.js'; +import {ConnectionType} from '#core/connection_type.js'; +import {assert} from 'chai'; import {defineStatementBlock} from '../test_helpers/block_definitions.js'; import {runSerializationTestSuite} from '../test_helpers/serialization.js'; import { diff --git a/packages/blockly/tests/mocha/blocks/logic_ternary_test.js b/packages/blockly/tests/mocha/blocks/logic_ternary_test.js index 3d343a7caec..fd0998cafe8 100644 --- a/packages/blockly/tests/mocha/blocks/logic_ternary_test.js +++ b/packages/blockly/tests/mocha/blocks/logic_ternary_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as eventUtils from '../../../build/src/core/events/utils.js'; -import {assert} from '../../../node_modules/chai/index.js'; +import * as eventUtils from '#core/events/utils.js'; +import {assert} from 'chai'; import {runSerializationTestSuite} from '../test_helpers/serialization.js'; import { sharedTestSetup, diff --git a/packages/blockly/tests/mocha/blocks/loops_test.js b/packages/blockly/tests/mocha/blocks/loops_test.js index eb040c884a7..ebc0ad0bc6a 100644 --- a/packages/blockly/tests/mocha/blocks/loops_test.js +++ b/packages/blockly/tests/mocha/blocks/loops_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as Blockly from '../../../build/src/core/blockly.js'; -import {assert} from '../../../node_modules/chai/index.js'; +import * as Blockly from '#core/blockly.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/blocks/procedures_test.js b/packages/blockly/tests/mocha/blocks/procedures_test.js index 8e226186bbe..87ce1478134 100644 --- a/packages/blockly/tests/mocha/blocks/procedures_test.js +++ b/packages/blockly/tests/mocha/blocks/procedures_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as Blockly from '../../../build/src/core/blockly.js'; -import {assert} from '../../../node_modules/chai/index.js'; +import * as Blockly from '#core/blockly.js'; +import {assert} from 'chai'; import {defineRowBlock} from '../test_helpers/block_definitions.js'; import { assertCallBlockStructure, diff --git a/packages/blockly/tests/mocha/blocks/variables_test.js b/packages/blockly/tests/mocha/blocks/variables_test.js index 724e2e543d0..0b440d9a19f 100644 --- a/packages/blockly/tests/mocha/blocks/variables_test.js +++ b/packages/blockly/tests/mocha/blocks/variables_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {nameUsedWithConflictingParam} from '../../../build/src/core/variables.js'; -import {assert} from '../../../node_modules/chai/index.js'; +import {nameUsedWithConflictingParam} from '#core/variables.js'; +import {assert} from 'chai'; import { MockParameterModelWithVar, MockProcedureModel, diff --git a/packages/blockly/tests/mocha/clipboard_test.js b/packages/blockly/tests/mocha/clipboard_test.js index ff49c0e303c..cbba541fc19 100644 --- a/packages/blockly/tests/mocha/clipboard_test.js +++ b/packages/blockly/tests/mocha/clipboard_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { assertEventFired, createChangeListenerSpy, diff --git a/packages/blockly/tests/mocha/comment_deserialization_test.js b/packages/blockly/tests/mocha/comment_deserialization_test.js index 8299b8215d2..e4a1dc6dae9 100644 --- a/packages/blockly/tests/mocha/comment_deserialization_test.js +++ b/packages/blockly/tests/mocha/comment_deserialization_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/comment_test.js b/packages/blockly/tests/mocha/comment_test.js index eb0f6ab9d75..feab6bda14f 100644 --- a/packages/blockly/tests/mocha/comment_test.js +++ b/packages/blockly/tests/mocha/comment_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {EventType} from '../../build/src/core/events/type.js'; -import {assert} from '../../node_modules/chai/index.js'; +import {EventType} from '#core/events/type.js'; +import {assert} from 'chai'; import {assertEventFired} from './test_helpers/events.js'; import { sharedTestSetup, diff --git a/packages/blockly/tests/mocha/comment_view_test.js b/packages/blockly/tests/mocha/comment_view_test.js index a60a7a973ff..068649c77f1 100644 --- a/packages/blockly/tests/mocha/comment_view_test.js +++ b/packages/blockly/tests/mocha/comment_view_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/connection_checker_test.js b/packages/blockly/tests/mocha/connection_checker_test.js index bdbcb70a6ec..3c50dd03d76 100644 --- a/packages/blockly/tests/mocha/connection_checker_test.js +++ b/packages/blockly/tests/mocha/connection_checker_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {ConnectionType} from '../../build/src/core/connection_type.js'; -import {assert} from '../../node_modules/chai/index.js'; +import {ConnectionType} from '#core/connection_type.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/connection_db_test.js b/packages/blockly/tests/mocha/connection_db_test.js index 459c59e3ab4..7b8afd2118d 100644 --- a/packages/blockly/tests/mocha/connection_db_test.js +++ b/packages/blockly/tests/mocha/connection_db_test.js @@ -4,9 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {ConnectionType} from '../../build/src/core/connection_type.js'; -import * as idGenerator from '../../build/src/core/utils/idgenerator.js'; -import {assert} from '../../node_modules/chai/index.js'; +import {ConnectionType} from '#core/connection_type.js'; +import * as idGenerator from '#core/utils/idgenerator.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/connection_test.js b/packages/blockly/tests/mocha/connection_test.js index b36f358eac3..5931d87ef42 100644 --- a/packages/blockly/tests/mocha/connection_test.js +++ b/packages/blockly/tests/mocha/connection_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { defineRowBlock, defineStackBlock, diff --git a/packages/blockly/tests/mocha/contextmenu_items_test.js b/packages/blockly/tests/mocha/contextmenu_items_test.js index 3316d626b0a..8021252f9a5 100644 --- a/packages/blockly/tests/mocha/contextmenu_items_test.js +++ b/packages/blockly/tests/mocha/contextmenu_items_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/contextmenu_test.js b/packages/blockly/tests/mocha/contextmenu_test.js index 8cd71172766..3aa2ce5fcca 100644 --- a/packages/blockly/tests/mocha/contextmenu_test.js +++ b/packages/blockly/tests/mocha/contextmenu_test.js @@ -4,9 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {callbackFactory} from '../../build/src/core/contextmenu.js'; -import * as xmlUtils from '../../build/src/core/utils/xml.js'; -import {assert} from '../../node_modules/chai/index.js'; +import {callbackFactory} from '#core/contextmenu.js'; +import * as xmlUtils from '#core/utils/xml.js'; +import {assert} from 'chai'; import { defineRowBlock, defineStackBlock, diff --git a/packages/blockly/tests/mocha/dialog_test.js b/packages/blockly/tests/mocha/dialog_test.js index ef8e17cf714..7ade12362ce 100644 --- a/packages/blockly/tests/mocha/dialog_test.js +++ b/packages/blockly/tests/mocha/dialog_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/dragger_test.js b/packages/blockly/tests/mocha/dragger_test.js deleted file mode 100644 index 7aeb7d84d09..00000000000 --- a/packages/blockly/tests/mocha/dragger_test.js +++ /dev/null @@ -1,303 +0,0 @@ -/** - * @license - * Copyright 2026 Raspberry Pi Foundation - * SPDX-License-Identifier: Apache-2.0 - */ - -import {assert} from '../../node_modules/chai/index.js'; -import { - defineBasicBlockWithField, - defineStackBlock, -} from './test_helpers/block_definitions.js'; -import { - sharedTestSetup, - sharedTestTeardown, -} from './test_helpers/setup_teardown.js'; - -suite('Dragger', function () { - /** - * @param {!Blockly.BlockSvg} block The block to measure. - * @returns {{x: number, y: number}} Viewport coordinates at the block center. - */ - function blockCenterClient(block) { - const boundingRect = block.getSvgRoot().getBoundingClientRect(); - return { - x: (boundingRect.left + boundingRect.right) / 2, - y: (boundingRect.top + boundingRect.bottom) / 2, - }; - } - - /** - * @param {!Blockly.BlockSvg} block The block to measure. - * @returns {{x: number, y: number}} Viewport coordinates at the block origin. - */ - function blockOriginClient(block) { - const ws = block.workspace; - let point = block.getRelativeToSurfaceXY(); - if (ws.isMutator) { - point = point.scale(ws.options.parentWorkspace.scale); - } - const screenCoords = Blockly.utils.svgMath.wsToScreenCoordinates(ws, point); - return {x: screenCoords.x, y: screenCoords.y}; - } - - /** - * @param {!Blockly.utils.Rect} rect The rectangle to measure. - * @returns {{x: number, y: number}} Viewport coordinates at the rect center. - */ - function rectCenterClient(rect) { - return { - x: (rect.left + rect.right) / 2, - y: (rect.top + rect.bottom) / 2, - }; - } - - /** - * @param {number} clientX The viewport x coordinate. - * @param {number} clientY The viewport y coordinate. - * @param {string=} type The pointer event type. - * @returns {!PointerEvent} A synthetic pointer event at the given location. - */ - function pointerAt(clientX, clientY, type = 'pointermove') { - return new PointerEvent(type, {clientX, clientY}); - } - - function hasDeleteStyle(block) { - return block.getSvgRoot().classList.contains('blocklyDraggingDelete'); - } - - /** - * @param {!Blockly.WorkspaceSvg} workspace The workspace with a trashcan. - * @returns {boolean} Whether the trashcan lid open style is applied. - */ - function hasTrashLidOpen(workspace) { - return workspace.trashcan.svgGroup.classList.contains('blocklyTrashOpen'); - } - - /** - * @param {!Blockly.WorkspaceSvg} workspace The workspace to zoom. - * @param {number} scale The target zoom factor. - */ - function setWorkspaceScale(workspace, scale) { - workspace.setScale(scale); - } - - /** - * Simulates pressing on the block center and dragging to a viewport point. - * - * @param {!Blockly.BlockSvg} block The block to drag. - * @param {{x: number, y: number}} pointerEnd The viewport point to drag to. - * @returns {{dragger: !Blockly.dragging.Dragger, dragEvent: !PointerEvent, block: !Blockly.BlockSvg}} - * The dragger, final pointer event, and block being dragged. - */ - function dragBlock(block, pointerEnd) { - const start = blockCenterClient(block); - const totalDelta = new Blockly.utils.Coordinate( - pointerEnd.x - start.x, - pointerEnd.y - start.y, - ); - - const dragger = new Blockly.dragging.Dragger(block); - const dragStartEvent = pointerAt(start.x, start.y, 'pointerdown'); - const dragEvent = pointerAt(pointerEnd.x, pointerEnd.y); - - dragger.onDragStart(dragStartEvent); - dragger.onDrag(dragEvent, totalDelta); - - return {dragger, dragEvent, block: dragger.draggable}; - } - - setup(function () { - sharedTestSetup.call(this); - defineBasicBlockWithField(); - defineStackBlock(); - const toolbox = document.getElementById('toolbox-categories'); - this.workspace = Blockly.inject('blocklyDiv', {toolbox, trashcan: true}); - this.workspace.recordDragTargets(); - this.trashRect = this.workspace.trashcan.getClientRect(); - this.toolboxRect = this.workspace.toolbox.getClientRect(); - assert.isNotNull(this.trashRect); - assert.isNotNull(this.toolboxRect); - - this.block = this.workspace.newBlock('stack_block'); - this.block.initSvg(); - this.block.render(); - }); - - teardown(function () { - sharedTestTeardown.call(this); - }); - - const zoomLevels = [ - {name: 'default scale', scale: null}, - {name: 'zoomed in', scale: 1.5}, - {name: 'zoomed out', scale: 0.7}, - ]; - - zoomLevels.forEach(({name: zoomName, scale}) => { - [ - {name: 'trashcan', rectKey: 'trashRect', checkLid: true}, - {name: 'toolbox', rectKey: 'toolboxRect', checkLid: false}, - ].forEach(({name, rectKey, checkLid}) => { - test(`applies delete styling and deletes when dragged to ${name} at ${zoomName}`, function () { - if (scale !== null) { - setWorkspaceScale(this.workspace, scale); - this.trashRect = this.workspace.trashcan.getClientRect(); - this.toolboxRect = this.workspace.toolbox.getClientRect(); - } - - const deleteRect = this[rectKey]; - const {dragger, dragEvent, block} = dragBlock( - this.block, - rectCenterClient(deleteRect), - ); - - assert.isTrue( - deleteRect.contains(dragEvent.clientX, dragEvent.clientY), - `Expected cursor to be inside ${name} delete area`, - ); - assert.isTrue(hasDeleteStyle(block)); - if (checkLid) { - assert.isTrue( - hasTrashLidOpen(this.workspace), - 'Expected trashcan lid to be open', - ); - } - - dragger.onDragEnd(dragEvent); - assert.isTrue(block.isDeadOrDying()); - }); - }); - }); - - test('does not apply delete styling when only block origin overlaps delete area', function () { - const start = blockCenterClient(this.block); - const originBefore = blockOriginClient(this.block); - const deleteAreaRect = this.toolboxRect; - const desiredOrigin = { - x: deleteAreaRect.right - 5, - y: originBefore.y, - }; - const {dragger, dragEvent, block} = dragBlock(this.block, { - x: start.x + desiredOrigin.x - originBefore.x, - y: start.y + desiredOrigin.y - originBefore.y, - }); - - const originAfter = blockOriginClient(block); - assert.isTrue( - deleteAreaRect.contains(originAfter.x, originAfter.y), - 'Expected block origin to overlap delete area', - ); - assert.isFalse( - deleteAreaRect.contains(dragEvent.clientX, dragEvent.clientY), - 'Expected cursor to be outside delete area', - ); - assert.isFalse(hasDeleteStyle(block)); - - dragger.onDragEnd(dragEvent); - assert.isFalse(block.isDeadOrDying()); - }); - - suite('Mutator', function () { - /** - * Opens a mutator on a controls_if block and returns the mutator workspace. - * - * @param {!Blockly.WorkspaceSvg} workspace The main workspace. - * @returns {!Promise} The mutator workspace. - */ - async function openMutator(workspace) { - const block = Blockly.serialization.blocks.append( - { - 'type': 'controls_if', - 'extraState': { - 'elseIfCount': 0, - }, - }, - workspace, - ); - block.initSvg(); - block.render(); - const icon = block.getIcon(Blockly.icons.MutatorIcon.TYPE); - await icon.setBubbleVisible(true); - return icon.getWorkspace(); - } - - test('deletes flyout block when pointer is over flyout delete area at zoomed scale', async function () { - for (let i = 0; i < 3; i++) { - this.workspace.zoomCenter(1); - } - - const mutatorWorkspace = await openMutator(this.workspace); - this.clock.runAll(); - mutatorWorkspace.recordDragTargets(); - - const flyout = mutatorWorkspace.getFlyout(); - const flyoutRect = flyout.getClientRect(); - assert.isNotNull(flyoutRect); - - const flyoutBlock = flyout - .getWorkspace() - .getBlocksByType('controls_if_elseif')[0]; - flyoutBlock.initSvg(); - flyoutBlock.render(); - - const {dragger, dragEvent, block} = dragBlock( - flyoutBlock, - rectCenterClient(flyoutRect), - ); - - assert.isTrue( - flyoutRect.contains(dragEvent.clientX, dragEvent.clientY), - 'Expected cursor to be inside flyout delete area', - ); - assert.isTrue(hasDeleteStyle(block)); - - dragger.onDragEnd(dragEvent); - assert.isTrue(block.isDeadOrDying()); - }); - - test('does not apply delete styling when only block origin overlaps flyout delete area at zoomed scale', async function () { - for (let i = 0; i < 3; i++) { - this.workspace.zoomCenter(1); - } - - const mutatorWorkspace = await openMutator(this.workspace); - this.clock.runAll(); - mutatorWorkspace.recordDragTargets(); - - const flyout = mutatorWorkspace.getFlyout(); - const flyoutRect = flyout.getClientRect(); - assert.isNotNull(flyoutRect); - - const workspaceBlock = mutatorWorkspace.newBlock('controls_if_elseif'); - workspaceBlock.initSvg(); - workspaceBlock.render(); - workspaceBlock.moveBy(200, 50); - - const start = blockCenterClient(workspaceBlock); - const originBefore = blockOriginClient(workspaceBlock); - const desiredOrigin = { - x: flyoutRect.right - 5, - y: originBefore.y, - }; - const {dragger, dragEvent, block} = dragBlock(workspaceBlock, { - x: start.x + desiredOrigin.x - originBefore.x, - y: start.y + desiredOrigin.y - originBefore.y, - }); - - const originAfter = blockOriginClient(block); - assert.isTrue( - flyoutRect.contains(originAfter.x, originAfter.y), - 'Expected block origin to overlap flyout delete area', - ); - assert.isFalse( - flyoutRect.contains(dragEvent.clientX, dragEvent.clientY), - 'Expected cursor to be outside flyout delete area', - ); - assert.isFalse(hasDeleteStyle(block)); - - dragger.onDragEnd(dragEvent); - assert.isFalse(block.isDeadOrDying()); - }); - }); -}); diff --git a/packages/blockly/tests/mocha/dropdowndiv_test.js b/packages/blockly/tests/mocha/dropdowndiv_test.js index 62e8c3dc923..2f1c3b9a993 100644 --- a/packages/blockly/tests/mocha/dropdowndiv_test.js +++ b/packages/blockly/tests/mocha/dropdowndiv_test.js @@ -4,9 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {Rect} from '../../build/src/core/utils/rect.js'; -import * as style from '../../build/src/core/utils/style.js'; -import {assert} from '../../node_modules/chai/index.js'; +import {Rect} from '#core/utils/rect.js'; +import * as style from '#core/utils/style.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, @@ -36,6 +36,7 @@ suite('DropDownDiv', function () { teardown(function () { sharedTestTeardown.call(this); document.getElementById('blocklyDiv').style.visibility = 'hidden'; + Blockly.common.setParentContainer(null); }); suite('Positioning', function () { @@ -182,20 +183,10 @@ suite('DropDownDiv', function () { ); }); - test('with bounds set positions and shows div near specified location', function () { - Blockly.DropDownDiv.setBoundsElement(document.body); - const block = this.setUpBlockWithField(); - const field = Array.from(block.getFields())[0]; - - Blockly.DropDownDiv.show(field, false, 50, 60, 70, 80, false); - - const dropDownDivElem = document.querySelector('.blocklyDropDownDiv'); - assert.strictEqual(dropDownDivElem.style.opacity, '1'); - assert.strictEqual(dropDownDivElem.style.left, '45px'); - assert.strictEqual(dropDownDivElem.style.top, '60px'); - }); - test('sets the dropdowndiv as owned by the workspace', function () { + // Set the bounds element explicitly rather than relying on a previous + // test having set this module-global, so this test is order-independent. + Blockly.DropDownDiv.setBoundsElement(document.body); const block = this.setUpBlockWithField(); const field = Array.from(block.getFields())[0]; diff --git a/packages/blockly/tests/mocha/event_block_change_test.js b/packages/blockly/tests/mocha/event_block_change_test.js index 9e1f9c3103e..de459a9c039 100644 --- a/packages/blockly/tests/mocha/event_block_change_test.js +++ b/packages/blockly/tests/mocha/event_block_change_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import {defineMutatorBlocks} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/packages/blockly/tests/mocha/event_block_create_test.js b/packages/blockly/tests/mocha/event_block_create_test.js index b0f5001de33..2cdbcba232a 100644 --- a/packages/blockly/tests/mocha/event_block_create_test.js +++ b/packages/blockly/tests/mocha/event_block_create_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {EventType} from '../../build/src/core/events/type.js'; -import {assert} from '../../node_modules/chai/index.js'; +import {EventType} from '#core/events/type.js'; +import {assert} from 'chai'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import {assertEventFired} from './test_helpers/events.js'; import { diff --git a/packages/blockly/tests/mocha/event_block_delete_test.js b/packages/blockly/tests/mocha/event_block_delete_test.js index e2fb5b8ce88..f423a4064bb 100644 --- a/packages/blockly/tests/mocha/event_block_delete_test.js +++ b/packages/blockly/tests/mocha/event_block_delete_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/packages/blockly/tests/mocha/event_block_drag_test.js b/packages/blockly/tests/mocha/event_block_drag_test.js index cc71e3bf084..c4859ddc086 100644 --- a/packages/blockly/tests/mocha/event_block_drag_test.js +++ b/packages/blockly/tests/mocha/event_block_drag_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/packages/blockly/tests/mocha/event_block_field_intermediate_change_test.js b/packages/blockly/tests/mocha/event_block_field_intermediate_change_test.js index d917dadcdd5..7a4c23bd575 100644 --- a/packages/blockly/tests/mocha/event_block_field_intermediate_change_test.js +++ b/packages/blockly/tests/mocha/event_block_field_intermediate_change_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/event_block_move_test.js b/packages/blockly/tests/mocha/event_block_move_test.js index 6d1890eebeb..c29d9a6ae8d 100644 --- a/packages/blockly/tests/mocha/event_block_move_test.js +++ b/packages/blockly/tests/mocha/event_block_move_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/packages/blockly/tests/mocha/event_bubble_open_test.js b/packages/blockly/tests/mocha/event_bubble_open_test.js index a445a6a7819..12ccd774b29 100644 --- a/packages/blockly/tests/mocha/event_bubble_open_test.js +++ b/packages/blockly/tests/mocha/event_bubble_open_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import {defineMutatorBlocks} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/packages/blockly/tests/mocha/event_click_test.js b/packages/blockly/tests/mocha/event_click_test.js index 5c4afbcadf9..00d3feb2fcf 100644 --- a/packages/blockly/tests/mocha/event_click_test.js +++ b/packages/blockly/tests/mocha/event_click_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/packages/blockly/tests/mocha/event_comment_change_test.js b/packages/blockly/tests/mocha/event_comment_change_test.js index edb539ef555..e25b008b93c 100644 --- a/packages/blockly/tests/mocha/event_comment_change_test.js +++ b/packages/blockly/tests/mocha/event_comment_change_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/event_comment_collapse_test.js b/packages/blockly/tests/mocha/event_comment_collapse_test.js index 5c3f61054a1..d7cd95aa071 100644 --- a/packages/blockly/tests/mocha/event_comment_collapse_test.js +++ b/packages/blockly/tests/mocha/event_comment_collapse_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/event_comment_create_test.js b/packages/blockly/tests/mocha/event_comment_create_test.js index 71ef8ed1b75..1ba9bd2ea69 100644 --- a/packages/blockly/tests/mocha/event_comment_create_test.js +++ b/packages/blockly/tests/mocha/event_comment_create_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/event_comment_delete_test.js b/packages/blockly/tests/mocha/event_comment_delete_test.js index dd9f0dd2286..7b880c34ee0 100644 --- a/packages/blockly/tests/mocha/event_comment_delete_test.js +++ b/packages/blockly/tests/mocha/event_comment_delete_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/event_comment_drag_test.js b/packages/blockly/tests/mocha/event_comment_drag_test.js index f6685cc5bdf..2e9c385a460 100644 --- a/packages/blockly/tests/mocha/event_comment_drag_test.js +++ b/packages/blockly/tests/mocha/event_comment_drag_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/event_comment_move_test.js b/packages/blockly/tests/mocha/event_comment_move_test.js index b3acea990a6..68e28f91d32 100644 --- a/packages/blockly/tests/mocha/event_comment_move_test.js +++ b/packages/blockly/tests/mocha/event_comment_move_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/event_comment_resize_test.js b/packages/blockly/tests/mocha/event_comment_resize_test.js index bed3e733a65..7b092497ab7 100644 --- a/packages/blockly/tests/mocha/event_comment_resize_test.js +++ b/packages/blockly/tests/mocha/event_comment_resize_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/event_selected_test.js b/packages/blockly/tests/mocha/event_selected_test.js index d5fefea0d75..b0c4d1d4130 100644 --- a/packages/blockly/tests/mocha/event_selected_test.js +++ b/packages/blockly/tests/mocha/event_selected_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import {createChangeListenerSpy} from './test_helpers/events.js'; import { diff --git a/packages/blockly/tests/mocha/event_test.js b/packages/blockly/tests/mocha/event_test.js index 0bd9dd6b961..eb43b099deb 100644 --- a/packages/blockly/tests/mocha/event_test.js +++ b/packages/blockly/tests/mocha/event_test.js @@ -4,9 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as Blockly from '../../build/src/core/blockly.js'; -import * as eventUtils from '../../build/src/core/events/utils.js'; -import {assert} from '../../node_modules/chai/index.js'; +import * as Blockly from '#core/blockly.js'; +import * as eventUtils from '#core/events/utils.js'; +import {assert} from 'chai'; import { assertEventEquals, assertNthCallEventArgEquals, diff --git a/packages/blockly/tests/mocha/event_theme_change_test.js b/packages/blockly/tests/mocha/event_theme_change_test.js index 396347c9e13..f514599d131 100644 --- a/packages/blockly/tests/mocha/event_theme_change_test.js +++ b/packages/blockly/tests/mocha/event_theme_change_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/event_toolbox_item_select_test.js b/packages/blockly/tests/mocha/event_toolbox_item_select_test.js index 02484c35bc1..5bbb879e304 100644 --- a/packages/blockly/tests/mocha/event_toolbox_item_select_test.js +++ b/packages/blockly/tests/mocha/event_toolbox_item_select_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/event_trashcan_open_test.js b/packages/blockly/tests/mocha/event_trashcan_open_test.js index 47da09a075a..80869b4d50d 100644 --- a/packages/blockly/tests/mocha/event_trashcan_open_test.js +++ b/packages/blockly/tests/mocha/event_trashcan_open_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/event_var_create_test.js b/packages/blockly/tests/mocha/event_var_create_test.js index 79af41281de..730c5fcd236 100644 --- a/packages/blockly/tests/mocha/event_var_create_test.js +++ b/packages/blockly/tests/mocha/event_var_create_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/event_var_delete_test.js b/packages/blockly/tests/mocha/event_var_delete_test.js index 93d9ef0ba2d..098b5a6a43c 100644 --- a/packages/blockly/tests/mocha/event_var_delete_test.js +++ b/packages/blockly/tests/mocha/event_var_delete_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/event_var_rename_test.js b/packages/blockly/tests/mocha/event_var_rename_test.js index b6d77cb35bd..1edefe3a4f3 100644 --- a/packages/blockly/tests/mocha/event_var_rename_test.js +++ b/packages/blockly/tests/mocha/event_var_rename_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/event_var_type_change_test.js b/packages/blockly/tests/mocha/event_var_type_change_test.js index 066c145a3ef..3a7cd890c47 100644 --- a/packages/blockly/tests/mocha/event_var_type_change_test.js +++ b/packages/blockly/tests/mocha/event_var_type_change_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/event_viewport_test.js b/packages/blockly/tests/mocha/event_viewport_test.js index cd11079fa32..fbf0c780e36 100644 --- a/packages/blockly/tests/mocha/event_viewport_test.js +++ b/packages/blockly/tests/mocha/event_viewport_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/extensions_test.js b/packages/blockly/tests/mocha/extensions_test.js index 8c41861d5d8..515d70378c3 100644 --- a/packages/blockly/tests/mocha/extensions_test.js +++ b/packages/blockly/tests/mocha/extensions_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/field_checkbox_test.js b/packages/blockly/tests/mocha/field_checkbox_test.js index 2c5a249e992..e432ecd73f4 100644 --- a/packages/blockly/tests/mocha/field_checkbox_test.js +++ b/packages/blockly/tests/mocha/field_checkbox_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/index.js'; +import * as Blockly from '#core/blockly.js'; +import {assert} from 'chai'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import { assertFieldValue, diff --git a/packages/blockly/tests/mocha/field_colour_test.js b/packages/blockly/tests/mocha/field_colour_test.js deleted file mode 100644 index 975d5a01d4a..00000000000 --- a/packages/blockly/tests/mocha/field_colour_test.js +++ /dev/null @@ -1,395 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/index.js'; -import { - createTestBlock, - defineRowBlock, -} from './test_helpers/block_definitions.js'; -import { - assertFieldValue, - runConstructorSuiteTests, - runFromJsonSuiteTests, - runSetValueTests, -} from './test_helpers/fields.js'; -import { - sharedTestSetup, - sharedTestTeardown, - workspaceTeardown, -} from './test_helpers/setup_teardown.js'; - -suite('Colour Fields', function () { - setup(function () { - sharedTestSetup.call(this); - }); - teardown(function () { - sharedTestTeardown.call(this); - }); - /** - * Configuration for field tests with invalid values. - * @type {!Array} - */ - const invalidValueTestCases = [ - {title: 'Undefined', value: undefined}, - {title: 'Null', value: null}, - {title: 'NaN', value: NaN}, - {title: 'Non-Parsable String', value: 'bad-string'}, - {title: 'Integer', value: 1}, - {title: 'Float', value: 1.5}, - {title: 'Infinity', value: Infinity, expectedValue: Infinity}, - {title: 'Negative Infinity', value: -Infinity, expectedValue: -Infinity}, - ]; - /** - * Configuration for field tests with valid values. - * @type {!Array} - */ - - const validValueTestCases = [ - { - title: '#AAAAAA', - value: '#AAAAAA', - expectedValue: '#aaaaaa', - expectedText: '#aaa', - }, - { - title: '#aaaaaa', - value: '#aaaaaa', - expectedValue: '#aaaaaa', - expectedText: '#aaa', - }, - { - title: '#AAAA00', - value: '#AAAA00', - expectedValue: '#aaaa00', - expectedText: '#aa0', - }, - { - title: '#aaaA00', - value: '#aaaA00', - expectedValue: '#aaaa00', - expectedText: '#aa0', - }, - { - title: '#BCBCBC', - value: '#BCBCBC', - expectedValue: '#bcbcbc', - expectedText: '#bcbcbc', - }, - { - title: '#bcbcbc', - value: '#bcbcbc', - expectedValue: '#bcbcbc', - expectedText: '#bcbcbc', - }, - { - title: '#AA0', - value: '#AA0', - expectedValue: '#aaaa00', - expectedText: '#aa0', - }, - { - title: '#aa0', - value: '#aa0', - expectedValue: '#aaaa00', - expectedText: '#aa0', - }, - { - title: 'rgb(170, 170, 0)', - value: 'rgb(170, 170, 0)', - expectedValue: '#aaaa00', - expectedText: '#aa0', - }, - { - title: 'red', - value: 'red', - expectedValue: '#ff0000', - expectedText: '#f00', - }, - ]; - const addArgsAndJson = function (testCase) { - testCase.args = [testCase.value]; - testCase.json = {'colour': testCase.value}; - }; - invalidValueTestCases.forEach(addArgsAndJson); - validValueTestCases.forEach(addArgsAndJson); - - /** - * The expected default value for the field being tested. - * @type {*} - */ - const defaultFieldValue = Blockly.FieldColour.COLOURS[0]; - /** - * The expected default text for the field being tested. - * @type {*} - */ - const defaultTextValue = (function () { - let expectedText = defaultFieldValue; - const m = defaultFieldValue.match(/^#(.)\1(.)\2(.)\3$/); - if (m) { - expectedText = '#' + m[1] + m[2] + m[3]; - } - return expectedText; - })(); - /** - * Asserts that the field property values are set to default. - * @param {FieldTemplate} field The field to check. - */ - const assertFieldDefault = function (field) { - assertFieldValue(field, defaultFieldValue, defaultTextValue); - }; - /** - * Asserts that the field properties are correct based on the test case. - * @param {!Blockly.FieldAngle} field The field to check. - * @param {!FieldValueTestCase} testCase The test case. - */ - const validTestCaseAssertField = function (field, testCase) { - assertFieldValue(field, testCase.expectedValue, testCase.expectedText); - }; - - runConstructorSuiteTests( - Blockly.FieldColour, - validValueTestCases, - invalidValueTestCases, - validTestCaseAssertField, - assertFieldDefault, - ); - - runFromJsonSuiteTests( - Blockly.FieldColour, - validValueTestCases, - invalidValueTestCases, - validTestCaseAssertField, - assertFieldDefault, - ); - - suite('setValue', function () { - suite('Empty -> New Value', function () { - setup(function () { - this.field = new Blockly.FieldColour(); - }); - runSetValueTests( - validValueTestCases, - invalidValueTestCases, - defaultFieldValue, - defaultTextValue, - ); - test('With source block', function () { - this.field.setSourceBlock(createTestBlock()); - this.field.setValue('#bcbcbc'); - assertFieldValue(this.field, '#bcbcbc', '#bcbcbc'); - }); - }); - suite('Value -> New Value', function () { - setup(function () { - this.field = new Blockly.FieldColour('#aaaaaa'); - }); - runSetValueTests( - validValueTestCases, - invalidValueTestCases, - '#aaaaaa', - '#aaa', - ); - test('With source block', function () { - this.field.setSourceBlock(createTestBlock()); - this.field.setValue('#bcbcbc'); - assertFieldValue(this.field, '#bcbcbc', '#bcbcbc'); - }); - }); - }); - suite('Validators', function () { - setup(function () { - this.field = new Blockly.FieldColour('#aaaaaa'); - }); - const testSuites = [ - { - title: 'Null Validator', - validator: function () { - return null; - }, - value: '#000000', - expectedValue: '#aaaaaa', - expectedText: '#aaa', - }, - { - title: 'Force Full Red Validator', - validator: function (newValue) { - return '#ff' + newValue.substr(3, 4); - }, - value: '#000000', - expectedValue: '#ff0000', - expectedText: '#f00', - }, - { - title: 'Returns Undefined Validator', - validator: function () {}, - value: '#000000', - expectedValue: '#000000', - expectedText: '#000', - }, - ]; - testSuites.forEach(function (suiteInfo) { - suite(suiteInfo.title, function () { - setup(function () { - this.field.setValidator(suiteInfo.validator); - }); - test('New Value', function () { - this.field.setValue(suiteInfo.value); - assertFieldValue( - this.field, - suiteInfo.expectedValue, - suiteInfo.expectedText, - ); - }); - }); - }); - }); - suite('Customizations', function () { - suite('Colours and Titles', function () { - function assertColoursAndTitles(field, colours, titles) { - field.dropdownCreate(); - let index = 0; - let node = field.picker.firstChild.firstChild; - while (node) { - assert.equal(node.getAttribute('title'), titles[index]); - assert.equal( - Blockly.utils.colour.parse(node.style.backgroundColor), - colours[index], - ); - - let nextNode = node.nextSibling; - if (!nextNode) { - nextNode = node.parentElement.nextSibling; - if (!nextNode) { - break; - } - nextNode = nextNode.firstChild; - } - node = nextNode; - - index++; - } - } - test('Constants', function () { - const colours = Blockly.FieldColour.COLOURS; - const titles = Blockly.FieldColour.TITLES; - // Note: Developers shouldn't actually do this. IMO they should - // change the file and then recompile. But this is fine for testing. - Blockly.FieldColour.COLOURS = ['#aaaaaa']; - Blockly.FieldColour.TITLES = ['grey']; - const field = new Blockly.FieldColour(); - - assertColoursAndTitles(field, ['#aaaaaa'], ['grey']); - - Blockly.FieldColour.COLOURS = colours; - Blockly.FieldColour.TITLES = titles; - }); - test('JS Constructor', function () { - const field = new Blockly.FieldColour('#aaaaaa', null, { - colourOptions: ['#aaaaaa'], - colourTitles: ['grey'], - }); - assertColoursAndTitles(field, ['#aaaaaa'], ['grey']); - }); - test('JSON Definition', function () { - const field = Blockly.FieldColour.fromJson({ - colour: '#aaaaaa', - colourOptions: ['#aaaaaa'], - colourTitles: ['grey'], - }); - assertColoursAndTitles(field, ['#aaaaaa'], ['grey']); - }); - test('setColours', function () { - const field = new Blockly.FieldColour(); - field.setColours(['#aaaaaa'], ['grey']); - assertColoursAndTitles(field, ['#aaaaaa'], ['grey']); - }); - test('Titles Undefined', function () { - const field = new Blockly.FieldColour(); - field.setColours(['#aaaaaa']); - assertColoursAndTitles(field, ['#aaaaaa'], ['#aaaaaa']); - }); - test('Some Titles Undefined', function () { - const field = new Blockly.FieldColour(); - field.setColours(['#aaaaaa', '#ff0000'], ['grey']); - assertColoursAndTitles( - field, - ['#aaaaaa', '#ff0000'], - ['grey', '#ff0000'], - ); - }); - // This is kinda derpy behavior, but I wanted to document it. - test('Overwriting Colours While Leaving Titles', function () { - const field = new Blockly.FieldColour(); - field.setColours(['#aaaaaa'], ['grey']); - field.setColours(['#ff0000']); - assertColoursAndTitles(field, ['#ff0000'], ['grey']); - }); - }); - suite('Columns', function () { - function assertColumns(field, columns) { - field.dropdownCreate(); - assert.equal(field.picker.firstChild.children.length, columns); - } - test('Constants', function () { - const columns = Blockly.FieldColour.COLUMNS; - // Note: Developers shouldn't actually do this. IMO they should edit - // the file and then recompile. But this is fine for testing. - Blockly.FieldColour.COLUMNS = 3; - const field = new Blockly.FieldColour(); - - assertColumns(field, 3); - - Blockly.FieldColour.COLUMNS = columns; - }); - test('JS Constructor', function () { - const field = new Blockly.FieldColour('#ffffff', null, { - columns: 3, - }); - assertColumns(field, 3); - }); - test('JSON Definition', function () { - const field = Blockly.FieldColour.fromJson({ - 'colour': '#ffffff', - 'columns': 3, - }); - assertColumns(field, 3); - }); - test('setColumns', function () { - const field = new Blockly.FieldColour(); - field.setColumns(3); - assertColumns(field, 3); - }); - }); - }); - - suite('Serialization', function () { - setup(function () { - this.workspace = new Blockly.Workspace(); - defineRowBlock(); - - this.assertValue = (value) => { - const block = this.workspace.newBlock('row_block'); - const field = new Blockly.FieldColour(value); - block.getInput('INPUT').appendField(field, 'COLOUR'); - const jso = Blockly.serialization.blocks.save(block); - assert.deepEqual(jso['fields'], {'COLOUR': value}); - }; - }); - - teardown(function () { - workspaceTeardown.call(this, this.workspace); - }); - - test('Three char', function () { - this.assertValue('#001122'); - }); - - test('Six char', function () { - this.assertValue('#012345'); - }); - }); -}); diff --git a/packages/blockly/tests/mocha/field_dropdown_test.js b/packages/blockly/tests/mocha/field_dropdown_test.js index e08b2202f60..20f77c7e69a 100644 --- a/packages/blockly/tests/mocha/field_dropdown_test.js +++ b/packages/blockly/tests/mocha/field_dropdown_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/index.js'; +import * as Blockly from '#core/blockly.js'; +import {assert} from 'chai'; import { createTestBlock, defineRowBlock, diff --git a/packages/blockly/tests/mocha/field_image_test.js b/packages/blockly/tests/mocha/field_image_test.js index 36607f9e0b5..83d867a9e15 100644 --- a/packages/blockly/tests/mocha/field_image_test.js +++ b/packages/blockly/tests/mocha/field_image_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/index.js'; +import * as Blockly from '#core/blockly.js'; +import {assert} from 'chai'; import { assertFieldValue, runConstructorSuiteTests, diff --git a/packages/blockly/tests/mocha/field_label_serializable_test.js b/packages/blockly/tests/mocha/field_label_serializable_test.js index 443cc6d1753..3cb6a622be4 100644 --- a/packages/blockly/tests/mocha/field_label_serializable_test.js +++ b/packages/blockly/tests/mocha/field_label_serializable_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/index.js'; +import * as Blockly from '#core/blockly.js'; +import {assert} from 'chai'; import { createTestBlock, defineRowBlock, diff --git a/packages/blockly/tests/mocha/field_label_test.js b/packages/blockly/tests/mocha/field_label_test.js index 6e5cd15226d..586efeb857e 100644 --- a/packages/blockly/tests/mocha/field_label_test.js +++ b/packages/blockly/tests/mocha/field_label_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/index.js'; +import * as Blockly from '#core/blockly.js'; +import {assert} from 'chai'; import {createTestBlock} from './test_helpers/block_definitions.js'; import { assertFieldValue, diff --git a/packages/blockly/tests/mocha/field_number_test.js b/packages/blockly/tests/mocha/field_number_test.js index b950f2b0329..3f21f93612e 100644 --- a/packages/blockly/tests/mocha/field_number_test.js +++ b/packages/blockly/tests/mocha/field_number_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/index.js'; +import * as Blockly from '#core/blockly.js'; +import {assert} from 'chai'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import {runTestCases} from './test_helpers/common.js'; import { diff --git a/packages/blockly/tests/mocha/field_registry_test.js b/packages/blockly/tests/mocha/field_registry_test.js index 1f19477dee1..956a300df0e 100644 --- a/packages/blockly/tests/mocha/field_registry_test.js +++ b/packages/blockly/tests/mocha/field_registry_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/index.js'; +import * as Blockly from '#core/blockly.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/field_test.js b/packages/blockly/tests/mocha/field_test.js index adfc6b43c97..44a588fc636 100644 --- a/packages/blockly/tests/mocha/field_test.js +++ b/packages/blockly/tests/mocha/field_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/index.js'; +import * as Blockly from '#core/blockly.js'; +import {assert} from 'chai'; import { addBlockTypeToCleanup, addMessageToCleanup, diff --git a/packages/blockly/tests/mocha/field_textinput_test.js b/packages/blockly/tests/mocha/field_textinput_test.js index a0faaeba7bc..2dd3a87df77 100644 --- a/packages/blockly/tests/mocha/field_textinput_test.js +++ b/packages/blockly/tests/mocha/field_textinput_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/index.js'; +import * as Blockly from '#core/blockly.js'; +import {assert} from 'chai'; import { createTestBlock, defineRowBlock, diff --git a/packages/blockly/tests/mocha/field_variable_test.js b/packages/blockly/tests/mocha/field_variable_test.js index 06ca1f5b7e4..13f22edc2ed 100644 --- a/packages/blockly/tests/mocha/field_variable_test.js +++ b/packages/blockly/tests/mocha/field_variable_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/index.js'; +import * as Blockly from '#core/blockly.js'; +import {assert} from 'chai'; import { createTestBlock, defineRowBlock, diff --git a/packages/blockly/tests/mocha/flyout_test.js b/packages/blockly/tests/mocha/flyout_test.js index e2812b25ba3..e0e50b1ceb8 100644 --- a/packages/blockly/tests/mocha/flyout_test.js +++ b/packages/blockly/tests/mocha/flyout_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/index.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/focus_manager_test.js b/packages/blockly/tests/mocha/focus_manager_test.js index 490fa4301b8..dc47c7ab561 100644 --- a/packages/blockly/tests/mocha/focus_manager_test.js +++ b/packages/blockly/tests/mocha/focus_manager_test.js @@ -4,78 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - FocusManager, - getFocusManager, -} from '../../build/src/core/focus_manager.js'; -import {assert} from '../../node_modules/chai/index.js'; +import {FocusManager, getFocusManager} from '#core/focus_manager.js'; +import {assert} from 'chai'; +import {FocusableTreeImpl} from './test_helpers/focusable_tree.js'; import { sharedTestSetup, sharedTestTeardown, } from './test_helpers/setup_teardown.js'; -class FocusableNodeImpl { - constructor(element, tree) { - this.element = element; - this.tree = tree; - } - - getFocusableElement() { - return this.element; - } - - getFocusableTree() { - return this.tree; - } - - onNodeFocus() {} - - onNodeBlur() {} - - canBeFocused() { - return true; - } -} - -class FocusableTreeImpl { - constructor(rootElement, nestedTrees) { - this.nestedTrees = nestedTrees; - this.idToNodeMap = {}; - this.rootNode = this.addNode(rootElement); - this.fallbackNode = null; - } - - addNode(element) { - const node = new FocusableNodeImpl(element, this); - this.idToNodeMap[element.id] = node; - return node; - } - - removeNode(node) { - delete this.idToNodeMap[node.getFocusableElement().id]; - } - - getRootFocusableNode() { - return this.rootNode; - } - - getRestoredFocusableNode() { - return this.fallbackNode; - } - - getNestedTrees() { - return this.nestedTrees; - } - - lookUpFocusableNode(id) { - return this.idToNodeMap[id]; - } - - onTreeFocus() {} - - onTreeBlur() {} -} - suite('FocusManager', function () { const ACTIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME}`; const PASSIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME}`; @@ -1302,27 +1238,6 @@ suite('FocusManager', function () { this.testFocusableNestedTree4Node1, ); }); - - test('deletion after focusNode() returns null', function () { - const rootElem = document.createElement('div'); - const nodeElem = document.createElement('div'); - rootElem.setAttribute('id', 'focusRoot'); - rootElem.setAttribute('tabindex', '-1'); - nodeElem.setAttribute('id', 'focusNode'); - nodeElem.setAttribute('tabindex', '-1'); - nodeElem.textContent = 'Focusable node'; - rootElem.appendChild(nodeElem); - document.body.appendChild(rootElem); - const root = this.createFocusableTree('focusRoot'); - const node = this.createFocusableNode(root, 'focusNode'); - this.focusManager.registerTree(root); - this.focusManager.focusNode(node); - - node.getFocusableElement().remove(); - - assert.notStrictEqual(this.focusManager.getFocusedNode(), node); - rootElem.remove(); // Cleanup. - }); }); suite('CSS classes', function () { test('registered tree focusTree()ed no prev focus root elem has active property', function () { @@ -2290,28 +2205,6 @@ suite('FocusManager', function () { ); }); - test('deletion after focus() returns null', function () { - const rootElem = document.createElement('div'); - const nodeElem = document.createElement('div'); - rootElem.setAttribute('id', 'focusRoot'); - rootElem.setAttribute('tabindex', '-1'); - nodeElem.setAttribute('id', 'focusNode'); - nodeElem.setAttribute('tabindex', '-1'); - nodeElem.textContent = 'Focusable node'; - rootElem.appendChild(nodeElem); - document.body.appendChild(rootElem); - const root = this.createFocusableTree('focusRoot'); - const node = this.createFocusableNode(root, 'focusNode'); - this.focusManager.registerTree(root); - document.getElementById('focusNode').tabIndex = -1; - document.getElementById('focusNode').focus(); - - node.getFocusableElement().remove(); - - assert.notStrictEqual(this.focusManager.getFocusedNode(), node); - rootElem.remove(); // Cleanup. - }); - test('after focus() after trying to focusNode() an unfocusable node updates returns focus()ed node', function () { this.testFocusableTree1Node1.canBeFocused = () => false; document.getElementById('testFocusableTree1.node2').tabIndex = -1; @@ -2743,42 +2636,6 @@ suite('FocusManager', function () { ); }); - test('registered tree focus()ed other tree node passively focused tree node now has active property', function () { - this.focusManager.registerTree(this.testFocusableTree1); - this.focusManager.registerTree(this.testFocusableTree2); - document.getElementById('testFocusableTree1.node1').tabIndex = -1; - document.getElementById('testFocusableTree2.node1').tabIndex = -1; - document.getElementById('testFocusableTree1').tabIndex = -1; - document.getElementById('testFocusableTree1.node1').focus(); - document.getElementById('testFocusableTree2.node1').focus(); - - document.getElementById('testFocusableTree1').focus(); - - // Directly refocusing a tree's root should have functional parity with focusTree(). That - // means the tree's previous node should now have active focus again and its root should - // have no focus indication. - const rootElem = this.testFocusableTree1 - .getRootFocusableNode() - .getFocusableElement(); - const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); - assert.includesClass( - nodeElem.classList, - FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, - ); - assert.notIncludesClass( - nodeElem.classList, - FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, - ); - assert.notIncludesClass( - rootElem.classList, - FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, - ); - assert.notIncludesClass( - rootElem.classList, - FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, - ); - }); - test('focus on root, node in diff tree, then node in first tree; root should have focus gone', function () { this.focusManager.registerTree(this.testFocusableTree1); this.focusManager.registerTree(this.testFocusableTree2); @@ -4711,42 +4568,6 @@ suite('FocusManager', function () { ); }); - test('registered tree focus()ed other tree node passively focused tree node now has active property', function () { - this.focusManager.registerTree(this.testFocusableGroup1); - this.focusManager.registerTree(this.testFocusableGroup2); - document.getElementById('testFocusableGroup1.node1').tabIndex = -1; - document.getElementById('testFocusableGroup2.node1').tabIndex = -1; - document.getElementById('testFocusableGroup1').tabIndex = -1; - document.getElementById('testFocusableGroup1.node1').focus(); - document.getElementById('testFocusableGroup2.node1').focus(); - - document.getElementById('testFocusableGroup1').focus(); - - // Directly refocusing a tree's root should have functional parity with focusTree(). That - // means the tree's previous node should now have active focus again and its root should - // have no focus indication. - const rootElem = this.testFocusableGroup1 - .getRootFocusableNode() - .getFocusableElement(); - const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); - assert.includesClass( - nodeElem.classList, - FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, - ); - assert.notIncludesClass( - nodeElem.classList, - FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, - ); - assert.notIncludesClass( - rootElem.classList, - FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, - ); - assert.notIncludesClass( - rootElem.classList, - FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, - ); - }); - test('focus on root, node in diff tree, then node in first tree; root should have focus gone', function () { this.focusManager.registerTree(this.testFocusableGroup1); this.focusManager.registerTree(this.testFocusableGroup2); diff --git a/packages/blockly/tests/mocha/focusable_tree_traverser_test.js b/packages/blockly/tests/mocha/focusable_tree_traverser_test.js index a384dd4be45..a59653b6325 100644 --- a/packages/blockly/tests/mocha/focusable_tree_traverser_test.js +++ b/packages/blockly/tests/mocha/focusable_tree_traverser_test.js @@ -4,71 +4,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {FocusManager} from '../../build/src/core/focus_manager.js'; -import {FocusableTreeTraverser} from '../../build/src/core/utils/focusable_tree_traverser.js'; -import {assert} from '../../node_modules/chai/index.js'; +import {FocusManager} from '#core/focus_manager.js'; +import {FocusableTreeTraverser} from '#core/utils/focusable_tree_traverser.js'; +import {assert} from 'chai'; +import {FocusableTreeImpl} from './test_helpers/focusable_tree.js'; import { sharedTestSetup, sharedTestTeardown, } from './test_helpers/setup_teardown.js'; -class FocusableNodeImpl { - constructor(element, tree) { - this.element = element; - this.tree = tree; - } - - getFocusableElement() { - return this.element; - } - - getFocusableTree() { - return this.tree; - } - - onNodeFocus() {} - - onNodeBlur() {} - - canBeFocused() { - return true; - } -} - -class FocusableTreeImpl { - constructor(rootElement, nestedTrees) { - this.nestedTrees = nestedTrees; - this.idToNodeMap = {}; - this.rootNode = this.addNode(rootElement); - } - - addNode(element) { - const node = new FocusableNodeImpl(element, this); - this.idToNodeMap[element.id] = node; - return node; - } - - getRootFocusableNode() { - return this.rootNode; - } - - getRestoredFocusableNode() { - return null; - } - - getNestedTrees() { - return this.nestedTrees; - } - - lookUpFocusableNode(id) { - return this.idToNodeMap[id]; - } - - onTreeFocus() {} - - onTreeBlur() {} -} - suite('FocusableTreeTraverser', function () { setup(function () { sharedTestSetup.call(this); diff --git a/packages/blockly/tests/mocha/generator_test.js b/packages/blockly/tests/mocha/generator_test.js index 3c377e7c1ab..1fe42a43b90 100644 --- a/packages/blockly/tests/mocha/generator_test.js +++ b/packages/blockly/tests/mocha/generator_test.js @@ -4,13 +4,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as Blockly from '../../build/src/core/blockly.js'; -import {DartGenerator} from '../../build/src/generators/dart/dart_generator.js'; -import {JavascriptGenerator} from '../../build/src/generators/javascript/javascript_generator.js'; -import {LuaGenerator} from '../../build/src/generators/lua/lua_generator.js'; -import {PhpGenerator} from '../../build/src/generators/php/php_generator.js'; -import {PythonGenerator} from '../../build/src/generators/python/python_generator.js'; -import {assert} from '../../node_modules/chai/index.js'; +import * as Blockly from '#core/blockly.js'; +import {DartGenerator} from '#generators/dart/dart_generator.js'; +import {JavascriptGenerator} from '#generators/javascript/javascript_generator.js'; +import {LuaGenerator} from '#generators/lua/lua_generator.js'; +import {PhpGenerator} from '#generators/php/php_generator.js'; +import {PythonGenerator} from '#generators/python/python_generator.js'; +import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/gesture_test.js b/packages/blockly/tests/mocha/gesture_test.js index 686f4f7aa09..648f5754caa 100644 --- a/packages/blockly/tests/mocha/gesture_test.js +++ b/packages/blockly/tests/mocha/gesture_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {EventType} from '../../build/src/core/events/type.js'; -import {assert} from '../../node_modules/chai/index.js'; +import {EventType} from '#core/events/type.js'; +import {assert} from 'chai'; import {defineBasicBlockWithField} from './test_helpers/block_definitions.js'; import {assertEventFired, assertEventNotFired} from './test_helpers/events.js'; import { diff --git a/packages/blockly/tests/mocha/icon_test.js b/packages/blockly/tests/mocha/icon_test.js index c3e11879ce2..43c87338fd7 100644 --- a/packages/blockly/tests/mocha/icon_test.js +++ b/packages/blockly/tests/mocha/icon_test.js @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/index.js'; +import * as Blockly from '#core/blockly.js'; +import {assert} from 'chai'; import {defineEmptyBlock} from './test_helpers/block_definitions.js'; import {MockIcon, MockSerializableIcon} from './test_helpers/icon_mocks.js'; import { @@ -390,6 +390,7 @@ suite('Icon', function () { suite('Contextual menus', function () { setup(function () { + Blockly.common.setParentContainer(document.firstElementChild); this.workspace = Blockly.inject('blocklyDiv', {}); Blockly.icons.registry.register( new Blockly.icons.IconType('test'), @@ -400,6 +401,11 @@ suite('Icon', function () { this.block.initSvg(); }); + teardown(function () { + sharedTestTeardown.call(this); + Blockly.common.setParentContainer(null); + }); + test('are shown when icons are right clicked', function () { const icon = new TestIcon(this.block); this.block.addIcon(icon); diff --git a/packages/blockly/tests/mocha/index.html b/packages/blockly/tests/mocha/index.html index 53304cb63be..bc77544301c 100644 --- a/packages/blockly/tests/mocha/index.html +++ b/packages/blockly/tests/mocha/index.html @@ -4,7 +4,22 @@ Mocha Tests for Blockly - + +