Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/actions/action_menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,23 @@ export class ActionMenu {
private registerShortcut() {
const menuShortcut: ShortcutRegistry.KeyboardShortcut = {
name: Constants.SHORTCUT_NAMES.MENU,
preconditionFn: (workspace) =>
this.navigation.canCurrentlyNavigate(workspace),
preconditionFn: (workspace) => {
return (
this.navigation.canCurrentlyNavigate(workspace) &&
!workspace.isDragging()
);
},
callback: (workspace) => {
switch (this.navigation.getState(workspace)) {
case Constants.STATE.WORKSPACE:
return this.openActionMenu(workspace);
case Constants.STATE.FLYOUT: {
const flyoutWorkspace = workspace.getFlyout()?.getWorkspace();
if (flyoutWorkspace) {
return this.openActionMenu(flyoutWorkspace);
}
return false;
}
default:
return false;
}
Expand Down
13 changes: 7 additions & 6 deletions src/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,17 @@ export class Navigation {
getState(workspace: Blockly.WorkspaceSvg): Constants.STATE {
const focusedTree = Blockly.getFocusManager().getFocusedTree();
if (focusedTree instanceof Blockly.WorkspaceSvg) {
if (focusedTree.isFlyout && workspace === focusedTree.targetWorkspace) {
if (focusedTree.isFlyout) {
return Constants.STATE.FLYOUT;
} else if (workspace === focusedTree) {
} else {
return Constants.STATE.WORKSPACE;
}
} else if (focusedTree instanceof Blockly.Toolbox) {
if (workspace === focusedTree.getWorkspace()) {
return Constants.STATE.TOOLBOX;
}
} else if (focusedTree instanceof Blockly.Flyout) {
if (workspace === focusedTree.targetWorkspace) {
return Constants.STATE.FLYOUT;
}
return Constants.STATE.FLYOUT;
}
// Either a non-Blockly element currently has DOM focus, or a different
// workspace holds it.
Expand Down Expand Up @@ -822,8 +820,11 @@ export class Navigation {
* @returns whether keyboard navigation is currently allowed.
*/
canCurrentlyNavigate(workspace: Blockly.WorkspaceSvg) {
const accessibilityMode = workspace.isFlyout
? workspace.targetWorkspace?.keyboardAccessibilityMode
: workspace.keyboardAccessibilityMode;
return (
workspace.keyboardAccessibilityMode &&
!!accessibilityMode &&
this.getState(workspace) !== Constants.STATE.NOWHERE
);
}
Expand Down
72 changes: 72 additions & 0 deletions test/webdriverio/test/actions_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import * as chai from 'chai';
import {Key} from 'webdriverio';
import {
contextMenuExists,
moveToToolboxCategory,
PAUSE_TIME,
setCurrentCursorNodeById,
tabNavigateToWorkspace,
testFileLocations,
testSetup,
} from './test_setup.js';

suite('Menus test', function () {
// Setting timeout to unlimited as these tests take longer time to run
this.timeout(0);

// Clear the workspace and load start blocks
setup(async function () {
this.browser = await testSetup(testFileLocations.BASE);
await this.browser.pause(PAUSE_TIME);
});

test('Menu action opens menu', async function () {
// Navigate to draw_circle_1.
await tabNavigateToWorkspace(this.browser);
await setCurrentCursorNodeById(this.browser, 'draw_circle_1');
await this.browser.pause(PAUSE_TIME);
await this.browser.keys([Key.Ctrl, Key.Return]);
await this.browser.pause(PAUSE_TIME);
chai.assert.isTrue(
await contextMenuExists(this.browser, 'Duplicate'),
'The menu should be openable on a block',
);
});

test('Menu action returns true in the toolbox', async function () {
// Navigate to draw_circle_1.
await tabNavigateToWorkspace(this.browser);
await setCurrentCursorNodeById(this.browser, 'draw_circle_1');
// Navigate to a toolbox category
await moveToToolboxCategory(this.browser, 'Functions');
// Move to flyout.
await this.browser.keys(Key.ArrowRight);
await this.browser.keys([Key.Ctrl, Key.Return]);
await this.browser.pause(PAUSE_TIME);

chai.assert.isTrue(
await contextMenuExists(this.browser, 'Help'),
'The menu should be openable on a block in the toolbox',
);
});

test('Menu action returns false during drag', async function () {
// Navigate to draw_circle_1.
await tabNavigateToWorkspace(this.browser);
await setCurrentCursorNodeById(this.browser, 'draw_circle_1');
// Start moving the block
await this.browser.keys('m');
await this.browser.keys([Key.Ctrl, Key.Return]);
await this.browser.pause(PAUSE_TIME);
chai.assert.isTrue(
await contextMenuExists(this.browser, 'Duplicate', true),
'The menu should not be openable during a move',
);
});
});
53 changes: 53 additions & 0 deletions test/webdriverio/test/test_setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,3 +431,56 @@ export async function isDragging(
return workspaceSvg.isDragging();
});
}

/**
* Returns the result of the specificied action precondition.
*
* @param browser The active WebdriverIO Browser object.
* @param action The action to check the precondition for.
*/
export async function checkActionPrecondition(
browser: WebdriverIO.Browser,
action: string,
): Promise<boolean> {
return await browser.execute((action) => {
const node = Blockly.getFocusManager().getFocusedNode();
let workspace;
if (node instanceof Blockly.BlockSvg) {
workspace = node.workspace as Blockly.WorkspaceSvg;
} else if (node instanceof Blockly.Workspace) {
workspace = node as Blockly.WorkspaceSvg;
} else if (node instanceof Blockly.Field) {
workspace = node.getSourceBlock()?.workspace as Blockly.WorkspaceSvg;
}

if (!workspace) {
throw new Error('Unable to derive workspace from focused node');
}
const actionItem = Blockly.ShortcutRegistry.registry.getRegistry()[action];
if (!actionItem || !actionItem.preconditionFn) {
throw new Error(
`No registered action or missing precondition: ${action}`,
);
}
return actionItem.preconditionFn(workspace, {
focusedNode: node ?? undefined,
});
}, action);
}

/**
* Wait for the specified context menu item to exist.
*
* @param browser The active WebdriverIO Browser object.
* @param itemText The display text of the context menu item to click.
* @param reverse Whether to check for non-existence instead.
* @return A Promise that resolves when the actions are completed.
*/
export async function contextMenuExists(
browser: WebdriverIO.Browser,
itemText: string,
reverse = false,
): Promise<boolean> {
const item = await browser.$(`div=${itemText}`);
return await item.waitForExist({timeout: 200, reverse: reverse});
}
Loading