Skip to content

Commit 23231d3

Browse files
feat: add initial clipboard tests (#381)
1 parent a60e07c commit 23231d3

File tree

2 files changed

+265
-0
lines changed

2 files changed

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

test/webdriverio/test/test_setup.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* identifiers that Selenium can use to find those elements.
1717
*/
1818

19+
import * as Blockly from 'blockly';
1920
import * as webdriverio from 'webdriverio';
2021
import * as path from 'path';
2122
import {fileURLToPath} from 'url';
@@ -140,3 +141,95 @@ export const testFileLocations = {
140141
new URLSearchParams({renderer: 'geras', rtl: 'true'}),
141142
),
142143
};
144+
145+
/**
146+
* Copied from blockly browser test_setup.mjs and amended for typescript
147+
*
148+
* @param browser The active WebdriverIO Browser object.
149+
* @returns A Promise that resolves to the ID of the currently selected block.
150+
*/
151+
export async function getSelectedBlockId(browser: WebdriverIO.Browser) {
152+
return await browser.execute(() => {
153+
// Note: selected is an ICopyable and I am assuming that it is a BlockSvg.
154+
return Blockly.common.getSelected()?.id;
155+
});
156+
}
157+
158+
export interface ElementWithId extends WebdriverIO.Element {
159+
id: string;
160+
}
161+
162+
/**
163+
* Copied from blockly browser test_setup.mjs and amended for typescript
164+
*
165+
* @param browser The active WebdriverIO Browser object.
166+
* @param id The ID of the Blockly block to search for.
167+
* @returns A Promise that resolves to the root SVG element of the block with
168+
* the given ID, as an interactable browser element.
169+
*/
170+
export async function getBlockElementById(
171+
browser: WebdriverIO.Browser,
172+
id: string,
173+
) {
174+
const elem = (await browser.$(
175+
`[data-id="${id}"]`,
176+
)) as unknown as ElementWithId;
177+
elem['id'] = id;
178+
return elem;
179+
}
180+
181+
/**
182+
* Copied from blockly browser test_setup.mjs and amended for typescript
183+
*
184+
* Find a clickable element on the block and click it.
185+
* We can't always use the block's SVG root because clicking will always happen
186+
* in the middle of the block's bounds (including children) by default, which
187+
* causes problems if it has holes (e.g. statement inputs). Instead, this tries
188+
* to get the first text field on the block. It falls back on the block's SVG root.
189+
*
190+
* @param browser The active WebdriverIO Browser object.
191+
* @param block The block to click, as an interactable element.
192+
* @param clickOptions The options to pass to webdriverio's element.click function.
193+
* @return A Promise that resolves when the actions are completed.
194+
*/
195+
export async function clickBlock(
196+
browser: WebdriverIO.Browser,
197+
block: ElementWithId,
198+
clickOptions: webdriverio.ClickOptions,
199+
) {
200+
const findableId = 'clickTargetElement';
201+
// In the browser context, find the element that we want and give it a findable ID.
202+
await browser.execute(
203+
(blockId, newElemId) => {
204+
const block = Blockly.getMainWorkspace().getBlockById(blockId);
205+
if (block) {
206+
for (const input of block.inputList) {
207+
for (const field of input.fieldRow) {
208+
if (field instanceof Blockly.FieldLabel) {
209+
const fieldSvg = field.getSvgRoot();
210+
if (fieldSvg) {
211+
fieldSvg.id = newElemId;
212+
return;
213+
}
214+
}
215+
}
216+
}
217+
}
218+
// No label field found. Fall back to the block's SVG root.
219+
(block as Blockly.BlockSvg).getSvgRoot().id = findableId;
220+
},
221+
block.id,
222+
findableId,
223+
);
224+
225+
// In the test context, get the Webdriverio Element that we've identified.
226+
const elem = await browser.$(`#${findableId}`);
227+
228+
await elem.click(clickOptions);
229+
230+
// In the browser context, remove the ID.
231+
await browser.execute((elemId) => {
232+
const clickElem = document.getElementById(elemId);
233+
clickElem?.removeAttribute('id');
234+
}, findableId);
235+
}

0 commit comments

Comments
 (0)