Skip to content

Commit 286de59

Browse files
rachel-fenichelmicrobit-grace
authored andcommitted
feat: introduce Mover actions + experimental DragMover implementation (RaspberryPiFoundation#352)
* feat: Introduce Mover class, moving mode Introduce the Mover class, with shortcuts that will put a workspace into (and exit from) moving mode. While in moving mode, cursor navigation via the cursor keys is disabled. * feat: Save block starting location and connections * feat: Experiment: unconstrained movement using Dragger Create an experimental implementation of unconstrained dragging using the existing Dragger class. Implement this on a new Mover subclass DragMover, to keep this experimental code (that will probably be thrown away) separate from the main Mover implementation. Known bugs: * Blocks lurch around somewhat wildly while being moved. * Moving top-level blocks works reasonably well, but attempting to move a non-top-level block will result in it being deleted unless the move is aborted. * feat: Add Move Block (M) context menu item * fix: Allow ctrl+arrows to also perform unconstrained moves Alt+arrows causes browser navigation on Windows. :-( * fix: Use better fake PointerEvents to prevent random deletions There was a bug that would often cause blocks to be deleted. This was because the dragger thought the block was on the toolbox, which is a delete area. Fix this by giving the dragger much better fake PointerEvents, so that it knows where the block actually is - good enough that you can move the block to the trash can and it will be deleted! * chore: address feedback from RaspberryPiFoundation#317 * chore: lint and format * fix: remove allowCollision for m * fix: RaspberryPiFoundation#356 by hiding the connection preview before reverting a drag * fix: spelling --------- Co-authored-by: Christopher Allen <[email protected]> feat: heuristic insert (RaspberryPiFoundation#355) * chore: simplify tryToConnectNodes We only insert blocks so remove other cases. * feat: heurstic insertion logic - use first statement or value input - in statement inputs move to the last next connection in the chain - use next connections over previous connections chore: convert webdriverio tests to TypeScript (RaspberryPiFoundation#353) * chore: convert webdriverio tests to TypeScript * chore: comment typo fix feat: allow for toolbox with multiple tab stops (RaspberryPiFoundation#358) - Switch to focusin/out for toolbox (maybe in future elsewhere) - Drop the tab index fiddling as it's not practical to get it right in the face of more than one tab stop in the toolbox, especially if that toolbox has a DOM structure not known to the plugin. Given that we're reinstating the ability to shift-tab flyout->toolbox it makes sense to allow tab to the flyout. Shift-tab to the flyout remains not possible in the auto-hide case but that's not unreasonable. This version can be made to work with MakeCode without further modifications. feat: use ts and factor out helper functions
1 parent a21ccce commit 286de59

File tree

4 files changed

+221
-202
lines changed

4 files changed

+221
-202
lines changed

test/webdriverio/test/basic_test.ts

Lines changed: 0 additions & 47 deletions
This file was deleted.

test/webdriverio/test/clipboard_test.mjs

Lines changed: 0 additions & 155 deletions
This file was deleted.
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import * as chai from 'chai';
8+
import * as Blockly from 'blockly';
9+
import {
10+
testSetup,
11+
testFileLocations,
12+
PAUSE_TIME,
13+
getBlockElementById,
14+
getSelectedBlockId,
15+
} from './test_setup.js';
16+
import {
17+
ChainablePromiseElement,
18+
Key,
19+
KeyAction,
20+
PointerAction,
21+
WheelAction,
22+
} from 'webdriverio';
23+
24+
/**
25+
* Clicks a block, but slightly off-centre to avoid clicking on field.
26+
*
27+
* @param block Block to click.
28+
* @returns A Promise that resolves once action completes.
29+
*/
30+
async function clickBlockOffCenter(block: ChainablePromiseElement) {
31+
const offsetFromBlockCenter = -50;
32+
await block.click({x: offsetFromBlockCenter});
33+
}
34+
35+
/**
36+
* Gets blocks that are the same as the reference block in terms of class
37+
* they contain.
38+
*
39+
* @param browser The active WebdriverIO Browser object.
40+
* @param block The reference element.
41+
* @returns A Promise that resolves to blocks that are the same as the reference block.
42+
*/
43+
async function getSameBlocks(
44+
browser: WebdriverIO.Browser,
45+
block: ChainablePromiseElement,
46+
) {
47+
const elClass = await block.getAttribute('class');
48+
return browser.$$(`.${elClass.split(' ').join('.')}`);
49+
}
50+
51+
/**
52+
* Perform actions whilst dragging a given block around.
53+
*
54+
* @param browser The active WebdriverIO Browser object.
55+
* @param blockToDrag The block to drag around.
56+
* @param action Action to perform whilst dragging block.
57+
* @returns A Promise that resolves once action completes.
58+
*/
59+
async function performActionWhileDraggingBlock(
60+
browser: WebdriverIO.Browser,
61+
blockToDrag: ChainablePromiseElement,
62+
action: KeyAction | PointerAction | WheelAction,
63+
) {
64+
const blockLoc = await blockToDrag.getLocation();
65+
const blockX = Math.round(blockLoc.x);
66+
const blockY = Math.round(blockLoc.y);
67+
await browser.actions([
68+
browser
69+
.action('pointer')
70+
.move(blockX, blockY)
71+
.down()
72+
.move(blockX + 20, blockY + 20)
73+
.move(blockX, blockY),
74+
action,
75+
]);
76+
await browser.pause(PAUSE_TIME);
77+
}
78+
79+
/**
80+
* Serializes workspace blocks into JSON objects.
81+
*
82+
* @param browser The active WebdriverIO Browser object.
83+
* @returns A Promise that resolves to JSON object serialization of the workspace blocks.
84+
*/
85+
async function serializeWorkspaceBlocks(browser: WebdriverIO.Browser) {
86+
return await browser.execute(() => {
87+
return Blockly.serialization.workspaces.save(Blockly.getMainWorkspace());
88+
});
89+
}
90+
91+
suite('Clipboard test', function () {
92+
// Setting timeout to unlimited as these tests take a longer time to run than most mocha test
93+
this.timeout(0);
94+
95+
// Clear the workspace and load start blocks
96+
setup(async function () {
97+
this.browser = await testSetup(testFileLocations.BASE);
98+
await this.browser.pause(PAUSE_TIME);
99+
});
100+
101+
test('Copy and paste while block selected', async function () {
102+
const block = await getBlockElementById(this.browser, 'draw_circle_1');
103+
await clickBlockOffCenter(block);
104+
105+
// Copy and paste
106+
107+
await this.browser.keys([Key.Ctrl, 'c']);
108+
await this.browser.keys([Key.Ctrl, 'v']);
109+
await this.browser.pause(PAUSE_TIME);
110+
111+
const blocks = await getSameBlocks(this.browser, block);
112+
const selectedId = await getSelectedBlockId(this.browser);
113+
114+
chai.assert.equal(await blocks.length, 2);
115+
// Duplicated block should be selected and after the copied one.
116+
chai.assert.equal(selectedId, await blocks[1].getAttribute('data-id'));
117+
});
118+
119+
test('Cut and paste while connection selected', async function () {
120+
const block = await getBlockElementById(this.browser, 'draw_circle_1');
121+
await clickBlockOffCenter(block);
122+
123+
// Cut and paste
124+
await this.browser.keys([Key.Ctrl, 'x']);
125+
await block.waitForExist({reverse: true});
126+
await this.browser.keys([Key.Ctrl, 'v']);
127+
await block.waitForExist();
128+
await this.browser.pause(PAUSE_TIME);
129+
130+
const blocks = await getSameBlocks(this.browser, block);
131+
const selectedId = await getSelectedBlockId(this.browser);
132+
133+
chai.assert.equal(await blocks.length, 1);
134+
chai.assert.equal(selectedId, await blocks[0].getAttribute('data-id'));
135+
});
136+
137+
test('Copy and paste whilst dragging block', async function () {
138+
const initialWsBlocks = await serializeWorkspaceBlocks(this.browser);
139+
140+
// Simultaneously drag block and Ctrl+C then Ctrl+V
141+
const block = await getBlockElementById(this.browser, 'draw_circle_1');
142+
await performActionWhileDraggingBlock(
143+
this.browser,
144+
block,
145+
this.browser
146+
.action('key')
147+
.down(Key.Ctrl)
148+
.down('c')
149+
.pause(10)
150+
.up(Key.Ctrl)
151+
.up('c')
152+
.down(Key.Ctrl)
153+
.down('v')
154+
.pause(10)
155+
.up(Key.Ctrl)
156+
.up('v'),
157+
);
158+
159+
await this.browser.pause(PAUSE_TIME);
160+
161+
// Assert no change in workspace blocks
162+
chai.assert.deepEqual(
163+
initialWsBlocks,
164+
await serializeWorkspaceBlocks(this.browser),
165+
);
166+
});
167+
168+
test('Cut whilst dragging block', async function () {
169+
const initialWsBlocks = await serializeWorkspaceBlocks(this.browser);
170+
171+
// Simultaneously drag block and Ctrl+X
172+
const block = await getBlockElementById(this.browser, 'draw_circle_1');
173+
await performActionWhileDraggingBlock(
174+
this.browser,
175+
block,
176+
this.browser
177+
.action('key')
178+
.down(Key.Ctrl)
179+
.down('x')
180+
.pause(10)
181+
.up(Key.Ctrl)
182+
.up('x'),
183+
);
184+
await this.browser.pause(PAUSE_TIME);
185+
186+
// Assert no change in workspace blocks
187+
chai.assert.deepEqual(
188+
initialWsBlocks,
189+
await serializeWorkspaceBlocks(this.browser),
190+
);
191+
});
192+
});

0 commit comments

Comments
 (0)