From 8241fca23f9b86913da9623c1a8d386003c8d1f8 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Wed, 2 Aug 2023 17:31:51 -0700 Subject: [PATCH] chore(tests): cleanup and toolbox drag tests (#7350) * chore(tests): use helpers for the basic drag test in the playground * chore(tests): miscellaneous test cleanup * chore: format * feat(tests): add test that drags out every block from the toolbox * feat(tests): add RTL version of toolbox drag tests * chore: lint * chore(tests): respond to PR feedback --- tests/browser/test/basic_playground_test.js | 21 +- tests/browser/test/block_undo_test.js | 23 +-- tests/browser/test/extensive_test.js | 19 +- tests/browser/test/field_edits_test.js | 39 ++-- tests/browser/test/test_setup.js | 29 ++- tests/browser/test/toolbox_drag_test.js | 208 ++++++++++++++++++++ 6 files changed, 273 insertions(+), 66 deletions(-) create mode 100644 tests/browser/test/toolbox_drag_test.js diff --git a/tests/browser/test/basic_playground_test.js b/tests/browser/test/basic_playground_test.js index 0c0bb221abc..7d1de3b6387 100644 --- a/tests/browser/test/basic_playground_test.js +++ b/tests/browser/test/basic_playground_test.js @@ -46,24 +46,13 @@ suite('Testing Connecting Blocks', function () { this.browser = await testSetup(testFileLocations.PLAYGROUND); }); - test('Testing Block Flyout', async function () { - const logicButton = await this.browser.$('#blockly-0'); - logicButton.click(); - const ifDoBlock = await this.browser.$( - '#blocklyDiv > div > svg:nth-child(7) > g > g.blocklyBlockCanvas > g:nth-child(3)', - ); - await ifDoBlock.dragAndDrop({x: 20, y: 20}); - await this.browser.pause(200); - const blockOnWorkspace = await this.browser.execute(() => { - const newBlock = Blockly.getMainWorkspace().getAllBlocks(false)[0]; - if (newBlock.id) { - return true; - } else { - return false; - } + test('dragging a block from the flyout results in a block on the workspace', async function () { + await dragBlockTypeFromFlyout(this.browser, 'Logic', 'controls_if', 20, 20); + const blockCount = await this.browser.execute(() => { + return Blockly.getMainWorkspace().getAllBlocks(false).length; }); - chai.assert.isTrue(blockOnWorkspace); + chai.assert.equal(blockCount, 1); }); }); diff --git a/tests/browser/test/block_undo_test.js b/tests/browser/test/block_undo_test.js index 94f5a6f71ca..3dc5babc5ea 100644 --- a/tests/browser/test/block_undo_test.js +++ b/tests/browser/test/block_undo_test.js @@ -13,45 +13,38 @@ const {Key} = require('webdriverio'); const { testSetup, testFileLocations, - switchRTL, dragBlockTypeFromFlyout, screenDirection, + getAllBlocks, } = require('./test_setup'); suite('Testing undo block movement', function (done) { // Setting timeout to unlimited as the webdriver takes a longer time to run than most mocha test this.timeout(0); - // Setup Selenium for all of the tests - suiteSetup(async function () { - this.browser = await testSetup(testFileLocations.PLAYGROUND); - }); - test('Undoing Block Movement LTR', async function () { + this.browser = await testSetup(testFileLocations.PLAYGROUND); await testUndoBlock(this.browser, screenDirection.LTR); }); test('Undoing Block Movement RTL', async function () { - await switchRTL(this.browser); + this.browser = await testSetup(testFileLocations.PLAYGROUND_RTL); await testUndoBlock(this.browser, screenDirection.RTL); }); }); -async function testUndoBlock(browser, delta) { +async function testUndoBlock(browser, direction) { // Drag out first function - const defReturnBlock = await dragBlockTypeFromFlyout( + await dragBlockTypeFromFlyout( browser, 'Functions', 'procedures_defreturn', - 50 * delta, + 50 * direction, 20, ); await browser.keys([Key.Ctrl, 'z']); - const blockOnWorkspace = await browser.execute(() => { - return !!Blockly.getMainWorkspace().getAllBlocks(false)[0]; - }); - - chai.assert.isFalse(blockOnWorkspace); + const allBlocks = await getAllBlocks(browser); + chai.assert.equal(allBlocks.length, 0); } diff --git a/tests/browser/test/extensive_test.js b/tests/browser/test/extensive_test.js index 2dfb5dc8875..578ed85c960 100644 --- a/tests/browser/test/extensive_test.js +++ b/tests/browser/test/extensive_test.js @@ -13,6 +13,7 @@ const { testSetup, testFileLocations, getBlockElementById, + getAllBlocks, } = require('./test_setup'); const {Key} = require('webdriverio'); @@ -25,12 +26,12 @@ suite('This tests loading Large Configuration and Deletion', function (done) { this.browser = await testSetup(testFileLocations.PLAYGROUND); }); - test('This test loading from JSON results in the correct number of blocks', async function () { + test('loading from JSON results in the correct number of blocks', async function () { const blockNum = await testingJSONLoad(this.browser); chai.assert.equal(blockNum, 13); }); - test('This test deleting block results in the correct number of blocks', async function () { + test('deleting block results in the correct number of blocks', async function () { const fourthRepeatDo = await getBlockElementById( this.browser, 'E8bF[-r:B~cabGLP#QYd', @@ -38,19 +39,15 @@ suite('This tests loading Large Configuration and Deletion', function (done) { await fourthRepeatDo.click({x: -100, y: -40}); await this.browser.keys([Key.Delete]); await this.browser.pause(100); - const blockNum = await this.browser.execute(() => { - return Blockly.getMainWorkspace().getAllBlocks(false).length; - }); - chai.assert.equal(blockNum, 10); + const allBlocks = await getAllBlocks(this.browser); + chai.assert.equal(allBlocks.length, 10); }); - test('This test undoing delete block results in the correct number of blocks', async function () { + test('undoing delete block results in the correct number of blocks', async function () { await this.browser.keys([Key.Ctrl, 'z']); await this.browser.pause(100); - const blockNum = await this.browser.execute(() => { - return Blockly.getMainWorkspace().getAllBlocks(false).length; - }); - chai.assert.equal(blockNum, 13); + const allBlocks = await getAllBlocks(this.browser); + chai.assert.equal(allBlocks.length, 13); }); }); diff --git a/tests/browser/test/field_edits_test.js b/tests/browser/test/field_edits_test.js index 925347779a6..cd720b0f309 100644 --- a/tests/browser/test/field_edits_test.js +++ b/tests/browser/test/field_edits_test.js @@ -12,8 +12,6 @@ const chai = require('chai'); const { testSetup, testFileLocations, - getSelectedBlockElement, - switchRTL, dragBlockTypeFromFlyout, screenDirection, } = require('./test_setup'); @@ -23,47 +21,42 @@ suite('Testing Field Edits', function (done) { // Setting timeout to unlimited as the webdriver takes a longer time to run than most mocha test this.timeout(0); - // Setup Selenium for all of the tests - suiteSetup(async function () { - this.browser = await testSetup(testFileLocations.PLAYGROUND); - }); - test('Testing Field Edits LTR', async function () { + this.browser = await testSetup(testFileLocations.PLAYGROUND); await testFieldEdits(this.browser, screenDirection.LTR); }); test('Testing Field Edits RTL', async function () { - await switchRTL(this.browser); + this.browser = await testSetup(testFileLocations.PLAYGROUND_RTL); await testFieldEdits(this.browser, screenDirection.RTL); }); }); -async function testFieldEdits(browser, delta) { - const mathNumber = await dragBlockTypeFromFlyout( +async function testFieldEdits(browser, direction) { + const numberBlock = await dragBlockTypeFromFlyout( browser, 'Math', 'math_number', - 50 * delta, + 50 * direction, 20, ); - await browser.pause(200); // Click on the field to change the value - const numeric = await getSelectedBlockElement(browser); - await numeric.doubleClick(); + await numberBlock.click(); await browser.keys([Key.Delete]); - await numeric.doubleClick(); + await numberBlock.click(); await browser.keys(['1093']); - // Click on the workspace + // Click on the workspace to exit the field editor const workspace = await browser.$('#blocklyDiv > div > svg.blocklySvg > g'); await workspace.click(); await browser.pause(200); - // Get value of the number - const numericText = await browser - .$( - '#blocklyDiv > div > svg.blocklySvg > g > g.blocklyBlockCanvas > g.blocklyDraggable > g > text', - ) - .getHTML(); - chai.assert.isTrue(numericText.includes('1093')); + const fieldValue = await browser.execute((id) => { + return Blockly.getMainWorkspace() + .getBlockById(id) + .getField('NUM') + .getValue(); + }, numberBlock.id); + + chai.assert.equal(fieldValue, '1093'); } diff --git a/tests/browser/test/test_setup.js b/tests/browser/test/test_setup.js index 634286a337f..ef2dd02c397 100644 --- a/tests/browser/test/test_setup.js +++ b/tests/browser/test/test_setup.js @@ -94,6 +94,10 @@ const testFileLocations = { 'file://' + posixPath(path.join(__dirname, '..', '..')) + '/playground.html', + PLAYGROUND_RTL: + 'file://' + + posixPath(path.join(__dirname, '..', '..')) + + '/playground.html?dir=rtl', }; /** @@ -164,7 +168,6 @@ async function getCategory(browser, categoryName) { async function getNthBlockOfCategory(browser, categoryName, n) { const category = await getCategory(browser, categoryName); await category.click(); - await browser.pause(100); const block = await browser.$( `.blocklyFlyout .blocklyBlockCanvas > g:nth-child(${3 + n * 2})`, ); @@ -440,6 +443,29 @@ 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 thte flyout in the y direction. Positive is down. + * @return A Promise that resolves when the actions are completed. + */ +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(50); + const scrollbarHandle = await browser + .$$(`.blocklyFlyoutScrollbar`)[1] + .$(`rect.blocklyScrollbarHandle`); + await scrollbarHandle.dragAndDrop({x: xDelta, y: yDelta}); + await browser.pause(50); +} + module.exports = { testSetup, testFileLocations, @@ -459,4 +485,5 @@ module.exports = { screenDirection, getBlockTypeFromWorkspace, getAllBlocks, + scrollFlyout, }; diff --git a/tests/browser/test/toolbox_drag_test.js b/tests/browser/test/toolbox_drag_test.js new file mode 100644 index 00000000000..1e277efc859 --- /dev/null +++ b/tests/browser/test/toolbox_drag_test.js @@ -0,0 +1,208 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Tests for the dragging out of the toolbox and flyout. + */ + +const chai = require('chai'); +const { + testSetup, + testFileLocations, + getCategory, + scrollFlyout, + screenDirection, +} = require('./test_setup'); + +const PAUSE_TIME = 50; + +// Categories in the basic toolbox. +const basicCategories = [ + 'Logic', + 'Loops', + 'Math', + 'Text', + 'Lists', + 'Colour', + 'Variables', + 'Functions', +]; + +// Categories in the test blocks toolbox. +const testCategories = [ + 'Align', + 'Basic', + // Skip connections because it's an accordion that is already open. + // 'Connections', + 'Row', + 'Stack', + 'Statement', + 'Drag', + // Skip fields because it's an accordion that is already open. + // 'Fields', + 'Defaults', + 'Numbers', + 'Angles', + 'Drop-downs', + // Note: images has a block that has a bad image source, but still builds and renders + // just fine. + 'Images', + 'Emoji! o((*^ᴗ^*))o', + 'Validators', + 'Mutators', + 'Style', + 'Serialization', +]; + +/** + * Check whether an element is fully inside the bounds of the Blockly div. You can use this + * 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 element The element to look for. + * @returns A Promise resolving to true if the element is in bounds and false otherwise. + */ +async function elementInBounds(browser, element) { + return await browser.execute((elem) => { + const rect = elem.getBoundingClientRect(); + + const blocklyDiv = document.getElementById('blocklyDiv'); + const blocklyRect = blocklyDiv.getBoundingClientRect(); + + const vertInView = + rect.top >= blocklyRect.top && rect.bottom <= blocklyRect.bottom; + const horInView = + rect.left >= blocklyRect.left && rect.right <= blocklyRect.right; + + return vertInView && horInView; + }, element); +} + +/** + * Get how many top-level blocks there are in the specified category. + * @param browser The active WebdriverIO Browser object. + * @param categoryName The name of the category to inspect. + * @returns A Promise resolving to the number of top-level blocks in the specified + * category's flyout. + */ +async function getBlockCount(browser, categoryName) { + const category = await getCategory(browser, categoryName); + await category.click(); + await browser.pause(PAUSE_TIME); + + const blockCount = await browser.execute(() => { + return Blockly.getMainWorkspace() + .getFlyout() + .getWorkspace() + .getTopBlocks(false).length; + }); + + // Unicode escape to close flyout. + await browser.keys(['\uE00C']); + await browser.pause(PAUSE_TIME); + return blockCount; +} + +/** + * Check whether the block at the given index in the flyout is disabled. + * @param browser The active WebdriverIO Browser object. + * @param i The index of the block in the currently open flyout. + * @returns A Promise resolving to true if the ith block in the flyout is + * disabled, and false otherwise. + */ +async function isBlockDisabled(browser, i) { + return await browser.execute((n) => { + return !Blockly.getMainWorkspace() + .getFlyout() + .getWorkspace() + .getTopBlocks() + [n].isEnabled(); + }, i); +} + +/** + * Loop over a list of categories and click on each one to open it. + * @param browser The WebdriverIO Browser instance for this test. + * @param categoryList An array of category names, as strings. + * @param directionMultiplier 1 for LTR and -1 for RTL. + * @returns A Promise that resolves when all actions have finished. + */ +async function openCategories(browser, categoryList, directionMultiplier) { + let failureCount = 0; + for (const categoryName of categoryList) { + const blockCount = await getBlockCount(browser, categoryName); + + try { + for (let i = 0; i < blockCount; i++) { + const category = await getCategory(browser, categoryName); + await category.click(); + if (await isBlockDisabled(browser, i)) { + // Unicode escape to close flyout. + await browser.keys(['\uE00C']); + await browser.pause(PAUSE_TIME); + } else { + const flyoutBlock = await browser.$( + `.blocklyFlyout .blocklyBlockCanvas > g:nth-child(${3 + i * 2})`, + ); + if (!(await elementInBounds(browser, flyoutBlock))) { + await scrollFlyout(browser, 0, 500); + } + + await flyoutBlock.dragAndDrop({x: directionMultiplier * 50, y: 0}); + await browser.pause(PAUSE_TIME); + // Should be one top level block on the workspace. + const topBlockCount = await browser.execute(() => { + return Blockly.getMainWorkspace().getTopBlocks(false).length; + }); + + if (topBlockCount != 1) { + failureCount++; + console.log(`fail: block ${i} in category ${categoryName}`); + } + + // Clean up between blocks so they can't interact with each other. + await browser.execute(() => { + Blockly.getMainWorkspace().clear(); + }); + await browser.pause(PAUSE_TIME); + } + } + } catch (e) { + failureCount++; + throw e; + } + } + chai.assert.equal(failureCount, 0); +} + +suite.skip('Open toolbox categories', function () { + this.timeout(0); + + test('opening every toolbox category in the category toolbox in LTR', async function () { + this.browser = await testSetup(testFileLocations.PLAYGROUND); + await openCategories(this.browser, basicCategories, screenDirection.LTR); + }); + + test('opening every toolbox category in the category toolbox in RTL', async function () { + this.browser = await testSetup(testFileLocations.PLAYGROUND_RTL); + await openCategories(this.browser, basicCategories, screenDirection.RTL); + }); + + test('opening every toolbox category in the test toolbox in LTR', async function () { + this.browser = await testSetup( + testFileLocations.PLAYGROUND + '?toolbox=test-blocks', + ); + await openCategories(this.browser, testCategories, screenDirection.LTR); + }); + + test('opening every toolbox category in the test toolbox in RTL', async function () { + this.browser = await testSetup( + testFileLocations.PLAYGROUND + '?toolbox=test-blocks&dir=rtl', + ); + await openCategories(this.browser, testCategories, screenDirection.RTL); + }); +});