Merge pull request #9183 from RoboErikG/fix-browser-tests-2025-06

fix: Fix more browser tests
This commit is contained in:
RoboErikG
2025-07-08 15:20:41 -07:00
committed by GitHub
6 changed files with 116 additions and 135 deletions

View File

@@ -8,6 +8,7 @@ import * as chai from 'chai';
import {Key} from 'webdriverio'; import {Key} from 'webdriverio';
import { import {
clickBlock, clickBlock,
clickWorkspace,
contextMenuSelect, contextMenuSelect,
getAllBlocks, getAllBlocks,
getBlockElementById, getBlockElementById,
@@ -176,8 +177,7 @@ suite('Delete blocks', function (done) {
); );
}); });
// TODO(#9029) enable this test once deleting a block doesn't lose focus test('Undo block deletion', async function () {
test.skip('Undo block deletion', async function () {
const before = (await getAllBlocks(this.browser)).length; const before = (await getAllBlocks(this.browser)).length;
// Get first print block, click to select it, and delete it using backspace key. // Get first print block, click to select it, and delete it using backspace key.
await clickBlock(this.browser, this.firstBlock.id, {button: 1}); await clickBlock(this.browser, this.firstBlock.id, {button: 1});
@@ -204,6 +204,7 @@ suite('Delete blocks', function (done) {
await this.browser.keys([Key.Ctrl, 'z']); await this.browser.keys([Key.Ctrl, 'z']);
await this.browser.pause(PAUSE_TIME); await this.browser.pause(PAUSE_TIME);
// Redo // Redo
await clickWorkspace(this.browser);
await this.browser.keys([Key.Ctrl, Key.Shift, 'z']); await this.browser.keys([Key.Ctrl, Key.Shift, 'z']);
await this.browser.pause(PAUSE_TIME); await this.browser.pause(PAUSE_TIME);
const after = (await getAllBlocks(this.browser)).length; const after = (await getAllBlocks(this.browser)).length;

View File

@@ -40,8 +40,7 @@ suite('This tests loading Large Configuration and Deletion', function (done) {
chai.assert.equal(allBlocks.length, 10); chai.assert.equal(allBlocks.length, 10);
}); });
// TODO(#8793) Re-enable test after deleting a block updates focus correctly. test('undoing delete block results in the correct number of blocks', async function () {
test.skip('undoing delete block results in the correct number of blocks', async function () {
await this.browser.keys([Key.Ctrl, 'z']); await this.browser.keys([Key.Ctrl, 'z']);
await this.browser.pause(PAUSE_TIME); await this.browser.pause(PAUSE_TIME);
const allBlocks = await getAllBlocks(this.browser); const allBlocks = await getAllBlocks(this.browser);

View File

@@ -11,9 +11,8 @@
import * as chai from 'chai'; import * as chai from 'chai';
import { import {
connect, connect,
getBlockTypeFromCategory, dragBlockTypeFromFlyout,
getNthBlockOfCategory, dragNthBlockFromFlyout,
getSelectedBlockElement,
PAUSE_TIME, PAUSE_TIME,
testFileLocations, testFileLocations,
testSetup, testSetup,
@@ -33,43 +32,41 @@ suite('Testing Connecting Blocks', function (done) {
test('Testing Procedure', async function () { test('Testing Procedure', async function () {
// Drag out first function // Drag out first function
let proceduresDefReturn = await getBlockTypeFromCategory( const doSomething = await dragBlockTypeFromFlyout(
this.browser, this.browser,
'Functions', 'Functions',
'procedures_defreturn', 'procedures_defreturn',
50,
20,
); );
await proceduresDefReturn.dragAndDrop({x: 50, y: 20});
const doSomething = await getSelectedBlockElement(this.browser);
// Drag out second function. const doSomething2 = await dragBlockTypeFromFlyout(
proceduresDefReturn = await getBlockTypeFromCategory(
this.browser, this.browser,
'Functions', 'Functions',
'procedures_defreturn', 'procedures_defreturn',
50,
20,
); );
await proceduresDefReturn.dragAndDrop({x: 300, y: 200});
const doSomething2 = await getSelectedBlockElement(this.browser);
// Drag out numeric const numeric = await dragBlockTypeFromFlyout(
const mathNumeric = await getBlockTypeFromCategory(
this.browser, this.browser,
'Math', 'Math',
'math_number', 'math_number',
50,
20,
); );
await mathNumeric.dragAndDrop({x: 50, y: 20});
const numeric = await getSelectedBlockElement(this.browser);
// Connect numeric to first procedure // Connect numeric to first procedure
await connect(this.browser, numeric, 'OUTPUT', doSomething, 'RETURN'); await connect(this.browser, numeric, 'OUTPUT', doSomething, 'RETURN');
// Drag out doSomething caller from flyout. // Drag out doSomething caller from flyout.
const doSomethingFlyout = await getNthBlockOfCategory( const doSomethingCaller = await dragNthBlockFromFlyout(
this.browser, this.browser,
'Functions', 'Functions',
3, 3,
50,
20,
); );
await doSomethingFlyout.dragAndDrop({x: 50, y: 20});
const doSomethingCaller = await getSelectedBlockElement(this.browser);
// Connect the doSomething caller to doSomething2 // Connect the doSomething caller to doSomething2
await connect( await connect(
@@ -81,22 +78,22 @@ suite('Testing Connecting Blocks', function (done) {
); );
// Drag out print from flyout. // Drag out print from flyout.
const printFlyout = await getBlockTypeFromCategory( const print = await dragBlockTypeFromFlyout(
this.browser, this.browser,
'Text', 'Text',
'text_print', 'text_print',
50,
0,
); );
await printFlyout.dragAndDrop({x: 50, y: 20});
const print = await getSelectedBlockElement(this.browser);
// Drag out doSomething2 caller from flyout. // Drag out doSomething2 caller from flyout.
const doSomething2Flyout = await getNthBlockOfCategory( const doSomething2Caller = await dragNthBlockFromFlyout(
this.browser, this.browser,
'Functions', 'Functions',
4, 4,
50,
20,
); );
await doSomething2Flyout.dragAndDrop({x: 130, y: 20});
const doSomething2Caller = await getSelectedBlockElement(this.browser);
// Connect doSomething2 caller with print. // Connect doSomething2 caller with print.
await connect(this.browser, doSomething2Caller, 'OUTPUT', print, 'TEXT'); await connect(this.browser, doSomething2Caller, 'OUTPUT', print, 'TEXT');

View File

@@ -62,6 +62,8 @@ export async function driverSetup() {
// Use Selenium to bring up the page // Use Selenium to bring up the page
console.log('Starting webdriverio...'); console.log('Starting webdriverio...');
driver = await webdriverio.remote(options); driver = await webdriverio.remote(options);
driver.setWindowSize(800, 600);
driver.setViewport({width: 800, height: 600});
return driver; return driver;
} }
@@ -170,43 +172,52 @@ export async function getBlockElementById(browser, id) {
* @return A Promise that resolves when the actions are completed. * @return A Promise that resolves when the actions are completed.
*/ */
export async function clickBlock(browser, blockId, clickOptions) { export async function clickBlock(browser, blockId, clickOptions) {
const findableId = 'clickTargetElement';
// In the browser context, find the element that we want and give it a findable ID. // In the browser context, find the element that we want and give it a findable ID.
await browser.execute( const elem = await getTargetableBlockElement(browser, blockId, false);
(blockId, newElemId) => { await elem.click(clickOptions);
const block = Blockly.getMainWorkspace().getBlockById(blockId); }
// Ensure the block we want to click is within the viewport.
Blockly.getMainWorkspace().scrollBoundsIntoView( /**
block.getBoundingRectangleWithoutChildren(), * Find an element on the block that is suitable for a click or drag.
10, *
); * We can't always use the block's SVG root because clicking will always happen
* in the middle of the block's bounds (including children) by default, which
* causes problems if it has holes (e.g. statement inputs). Instead, this tries
* to get the first text field on the block. It falls back on the block's SVG root.
* @param browser The active WebdriverIO Browser object.
* @param blockId The id of the block to click, as an interactable element.
* @param toolbox True if this block is in the toolbox (which must be open already).
* @return A Promise that returns an appropriate element.
*/
async function getTargetableBlockElement(browser, blockId, toolbox) {
const id = await browser.execute(
(blockId, toolbox, newElemId) => {
const ws = toolbox
? Blockly.getMainWorkspace().getFlyout().getWorkspace()
: Blockly.getMainWorkspace();
const block = ws.getBlockById(blockId);
// Ensure the block we want to click/drag is within the viewport.
ws.scrollBoundsIntoView(block.getBoundingRectangleWithoutChildren(), 10);
if (!block.isCollapsed()) { if (!block.isCollapsed()) {
for (const input of block.inputList) { for (const input of block.inputList) {
for (const field of input.fieldRow) { for (const field of input.fieldRow) {
if (field instanceof Blockly.FieldLabel) { if (field instanceof Blockly.FieldLabel) {
field.getSvgRoot().id = newElemId; // Expose the id of the element we want to target
return; field.getSvgRoot().setAttribute('data-id', field.id_);
return field.getSvgRoot().id;
} }
} }
} }
} }
// No label field found. Fall back to the block's SVG root. // No label field found. Fall back to the block's SVG root, which should
block.getSvgRoot().id = newElemId; // already use the block id.
return block.id;
}, },
blockId, blockId,
findableId, toolbox,
); );
// In the test context, get the Webdriverio Element that we've identified. return await getBlockElementById(browser, id);
const elem = await browser.$(`#${findableId}`);
await elem.click(clickOptions);
// In the browser context, remove the ID.
await browser.execute((elemId) => {
const clickElem = document.getElementById(elemId);
clickElem.removeAttribute('id');
}, findableId);
} }
/** /**
@@ -215,7 +226,7 @@ export async function clickBlock(browser, blockId, clickOptions) {
* @return A Promise that resolves when the actions are completed. * @return A Promise that resolves when the actions are completed.
*/ */
export async function clickWorkspace(browser) { export async function clickWorkspace(browser) {
const workspace = await browser.$('#blocklyDiv > div > svg.blocklySvg > g'); const workspace = await browser.$('svg.blocklySvg > g');
await workspace.click(); await workspace.click();
await browser.pause(PAUSE_TIME); await browser.pause(PAUSE_TIME);
} }
@@ -253,27 +264,14 @@ export async function getCategory(browser, categoryName) {
} }
/** /**
* @param browser The active WebdriverIO Browser object. * Opens the specified category, finds the first block of the given type,
* @param categoryName The name of the toolbox category to search. * scrolls it into view, and returns a draggable element on that block.
* @param n Which block to select, 0-indexed from the top of the category. *
* @return A Promise that resolves to the root element of the nth
* block in the given category.
*/
export async function getNthBlockOfCategory(browser, categoryName, n) {
const category = await getCategory(browser, categoryName);
await category.click();
const block = (
await browser.$$(`.blocklyFlyout .blocklyBlockCanvas > .blocklyDraggable`)
)[n];
return block;
}
/**
* @param browser The active WebdriverIO Browser object. * @param browser The active WebdriverIO Browser object.
* @param categoryName The name of the toolbox category to search. * @param categoryName The name of the toolbox category to search.
* Null if the toolbox has no categories (simple). * Null if the toolbox has no categories (simple).
* @param blockType The type of the block to search for. * @param blockType The type of the block to search for.
* @return A Promise that resolves to the root element of the first * @return A Promise that resolves to a draggable element of the first
* block with the given type in the given category. * block with the given type in the given category.
*/ */
export async function getBlockTypeFromCategory( export async function getBlockTypeFromCategory(
@@ -286,13 +284,14 @@ export async function getBlockTypeFromCategory(
await category.click(); await category.click();
} }
await browser.pause(PAUSE_TIME);
const id = await browser.execute((blockType) => { const id = await browser.execute((blockType) => {
return Blockly.getMainWorkspace() const ws = Blockly.getMainWorkspace().getFlyout().getWorkspace();
.getFlyout() const block = ws.getBlocksByType(blockType)[0];
.getWorkspace() ws.scrollBoundsIntoView(block.getBoundingRectangleWithoutChildren());
.getBlocksByType(blockType)[0].id; return block.id;
}, blockType); }, blockType);
return getBlockElementById(browser, id); return getTargetableBlockElement(browser, id, true);
} }
/** /**
@@ -447,7 +446,16 @@ export async function switchRTL(browser) {
* created block. * created block.
*/ */
export async function dragNthBlockFromFlyout(browser, categoryName, n, x, y) { export async function dragNthBlockFromFlyout(browser, categoryName, n, x, y) {
const flyoutBlock = await getNthBlockOfCategory(browser, categoryName, n); const category = await getCategory(browser, categoryName);
await category.click();
await browser.pause(PAUSE_TIME);
const id = await browser.execute((n) => {
const ws = Blockly.getMainWorkspace().getFlyout().getWorkspace();
const block = ws.getTopBlocks(true)[n];
return block.id;
}, n);
const flyoutBlock = await getTargetableBlockElement(browser, id, true);
await flyoutBlock.dragAndDrop({x: x, y: y}); await flyoutBlock.dragAndDrop({x: x, y: y});
return await getSelectedBlockElement(browser); return await getSelectedBlockElement(browser);
} }
@@ -480,6 +488,7 @@ export async function dragBlockTypeFromFlyout(
type, type,
); );
await flyoutBlock.dragAndDrop({x: x, y: y}); await flyoutBlock.dragAndDrop({x: x, y: y});
await browser.pause(PAUSE_TIME);
return await getSelectedBlockElement(browser); return await getSelectedBlockElement(browser);
} }
@@ -584,26 +593,3 @@ export async function getAllBlocks(browser) {
})); }));
}); });
} }
/**
* Find the flyout's scrollbar and scroll by the specified amount.
* This makes several assumptions:
* - A flyout with a valid scrollbar exists, is open, and is in view.
* - The workspace has a trash can, which means it has a second (hidden) flyout.
* @param browser The active WebdriverIO Browser object.
* @param xDelta How far to drag the flyout in the x direction. Positive is right.
* @param yDelta How far to drag the flyout in the y direction. Positive is down.
* @return A Promise that resolves when the actions are completed.
*/
export async function scrollFlyout(browser, xDelta, yDelta) {
// There are two flyouts on the playground workspace: one for the trash can
// and one for the toolbox. We want the second one.
// This assumes there is only one scrollbar handle in the flyout, but it could
// be either horizontal or vertical.
await browser.pause(PAUSE_TIME);
const scrollbarHandle = await browser
.$$(`.blocklyFlyoutScrollbar`)[1]
.$(`rect.blocklyScrollbarHandle`);
await scrollbarHandle.dragAndDrop({x: xDelta, y: yDelta});
await browser.pause(PAUSE_TIME);
}

View File

@@ -9,11 +9,12 @@
*/ */
import * as chai from 'chai'; import * as chai from 'chai';
import {Key} from 'webdriverio';
import { import {
dragBlockTypeFromFlyout,
getCategory, getCategory,
PAUSE_TIME, PAUSE_TIME,
screenDirection, screenDirection,
scrollFlyout,
testFileLocations, testFileLocations,
testSetup, testSetup,
} from './test_setup.mjs'; } from './test_setup.mjs';
@@ -57,28 +58,29 @@ const testCategories = [
]; ];
/** /**
* Check whether an element is fully inside the bounds of the Blockly div. You can use this * Get the type of the nth block in the specified category.
* to determine whether a block on the workspace or flyout is inside the Blockly div.
* This does not check whether there are other Blockly elements (such as a toolbox or
* flyout) on top of the element. A partially visible block is considered out of bounds.
* @param browser The active WebdriverIO Browser object. * @param browser The active WebdriverIO Browser object.
* @param element The element to look for. * @param categoryName The name of the category to inspect.
* @returns A Promise resolving to true if the element is in bounds and false otherwise. * @param n The index of the block to get
* @returns A Promise resolving to the type the block in the specified
* category's flyout at index i.
*/ */
async function elementInBounds(browser, element) { async function getNthBlockType(browser, categoryName, n) {
return await browser.execute((elem) => { const category = await getCategory(browser, categoryName);
const rect = elem.getBoundingClientRect(); await category.click();
await browser.pause(PAUSE_TIME);
const blocklyDiv = document.getElementById('blocklyDiv'); const blockType = await browser.execute((i) => {
const blocklyRect = blocklyDiv.getBoundingClientRect(); return Blockly.getMainWorkspace()
.getFlyout()
.getWorkspace()
.getTopBlocks(false)[i].type;
}, n);
const vertInView = // Unicode escape to close flyout.
rect.top >= blocklyRect.top && rect.bottom <= blocklyRect.bottom; await browser.keys([Key.Escape]);
const horInView = await browser.pause(PAUSE_TIME);
rect.left >= blocklyRect.left && rect.right <= blocklyRect.right; return blockType;
return vertInView && horInView;
}, element);
} }
/** /**
@@ -101,7 +103,7 @@ async function getBlockCount(browser, categoryName) {
}); });
// Unicode escape to close flyout. // Unicode escape to close flyout.
await browser.keys(['\uE00C']); await browser.keys([Key.Escape]);
await browser.pause(PAUSE_TIME); await browser.pause(PAUSE_TIME);
return blockCount; return blockCount;
} }
@@ -141,18 +143,12 @@ async function openCategories(browser, categoryList, directionMultiplier) {
await category.click(); await category.click();
if (await isBlockDisabled(browser, i)) { if (await isBlockDisabled(browser, i)) {
// Unicode escape to close flyout. // Unicode escape to close flyout.
await browser.keys(['\uE00C']); await browser.keys([Key.Escape]);
await browser.pause(PAUSE_TIME); await browser.pause(PAUSE_TIME);
continue; continue;
} }
const flyoutBlock = await browser.$( const blockType = await getNthBlockType(browser, categoryName, i);
`.blocklyFlyout .blocklyBlockCanvas > g:nth-child(${3 + i * 2})`, dragBlockTypeFromFlyout(browser, categoryName, blockType, 50, 20);
);
while (!(await elementInBounds(browser, flyoutBlock))) {
await scrollFlyout(browser, 0, 50);
}
await flyoutBlock.dragAndDrop({x: directionMultiplier * 50, y: 0});
await browser.pause(PAUSE_TIME); await browser.pause(PAUSE_TIME);
// Should be one top level block on the workspace. // Should be one top level block on the workspace.
const topBlockCount = await browser.execute(() => { const topBlockCount = await browser.execute(() => {
@@ -178,7 +174,9 @@ async function openCategories(browser, categoryList, directionMultiplier) {
chai.assert.equal(failureCount, 0); chai.assert.equal(failureCount, 0);
} }
suite('Open toolbox categories', function () { // TODO (#9217) These take too long to run and are very flakey. Need to find a
// better way to test whatever this is trying to test.
suite.skip('Open toolbox categories', function () {
this.timeout(0); this.timeout(0);
test('opening every toolbox category in the category toolbox in LTR', async function () { test('opening every toolbox category in the category toolbox in LTR', async function () {

View File

@@ -206,13 +206,13 @@ suite('Workspace comments', function () {
'.blocklyComment .blocklyResizeHandle', '.blocklyComment .blocklyResizeHandle',
); );
await resizeHandle.dragAndDrop(delta); await resizeHandle.dragAndDrop(delta);
const newSize = await getCommentSize(this.browser, commentId);
chai.assert.deepEqual( chai.assert.isTrue(
await getCommentSize(this.browser, commentId), Math.abs(newSize.width - (origSize.width + delta.x)) < 1,
{ 'Expected the comment model size to match the resized size',
width: origSize.width + delta.x, );
height: origSize.height + delta.y, chai.assert.isTrue(
}, Math.abs(newSize.height - (origSize.height + delta.y)) < 1,
'Expected the comment model size to match the resized size', 'Expected the comment model size to match the resized size',
); );
}); });