Skip to content

Commit bc0741b

Browse files
wenytang-msCopilot
andcommitted
fix: context menu click with hover+focused wait, auto-dismiss native dialogs
Root causes found and fixed: 1. Context menu click requires hover first to trigger VS Code's menu focus state (.action-item.focused), then click works reliably. 2. redhat.java shows an Electron native dialog (dialog.showMessageBox) for refactoring confirmation on file rename. Playwright Page API cannot interact with native dialogs. Monkey-patch showMessageBox in the Electron main process to auto-return OK. Verified locally: both rename and delete tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c088e21 commit bc0741b

File tree

3 files changed

+43
-27
lines changed

3 files changed

+43
-27
lines changed

test/e2e/fixtures/baseTest.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@ export const test = base.extend<TestFixtures>({
103103

104104
const page = await electronApp.firstWindow();
105105

106+
// Auto-dismiss Electron native dialogs (e.g. redhat.java refactoring
107+
// confirmation "wants to make refactoring changes"). These dialogs are
108+
// outside the renderer DOM and cannot be handled via Playwright Page API.
109+
// Monkey-patch dialog.showMessageBox in the main process to auto-click OK.
110+
await electronApp.evaluate(({ dialog }) => {
111+
dialog.showMessageBox = async () => ({ response: 0, checkboxChecked: true });
112+
dialog.showMessageBoxSync = () => 0;
113+
});
114+
106115
// Dismiss any startup notifications/dialogs before handing off to tests
107116
await page.waitForTimeout(3_000);
108117
await dismissAllNotifications(page);

test/e2e/tests/fileOperations.test.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,18 @@ test.describe("File Operations", () => {
6969
// Expand to AppToRename
7070
await JavaOperator.expandTreePath(page, "my-app", "src/main/java", "com.mycompany.app");
7171

72-
// Select AppToRename in the tree and invoke rename via context menu.
73-
// The command is hidden from the command palette (when: false)
74-
// and keyboard shortcut requires focusedView which is unreliable,
75-
// so context menu is the only reliable UI path.
72+
// Right-click AppToRename to select it AND open the context menu.
73+
// We do NOT left-click first because that opens the file in the editor
74+
// and steals focus away from the tree view.
7675
const appToRename = page.getByRole(VSCode.TREE_ITEM_ROLE, { name: "AppToRename" }).first();
77-
await appToRename.click();
78-
await page.waitForTimeout(Timeout.CLICK);
79-
80-
await VscodeOperator.selectContextMenuItem(page, appToRename, "Rename");
76+
await VscodeOperator.selectContextMenuItem(page, appToRename, /^Rename/);
8177

8278
// The extension shows a showInputBox (quick-input) for the new name
8379
await VscodeOperator.fillQuickInput(page, "AppRenamed");
8480

85-
// Handle confirmation dialog if it appears
81+
// Handle extension's own rename confirmation dialog if it appears.
82+
// The Electron native refactoring dialog from redhat.java is
83+
// auto-dismissed by the showMessageBox monkey-patch in baseTest.ts.
8684
try {
8785
await VscodeOperator.clickDialogButton(page, "OK", 5_000);
8886
} catch {
@@ -98,11 +96,9 @@ test.describe("File Operations", () => {
9896
await JavaOperator.collapseFileExplorer(page);
9997
await JavaOperator.expandTreePath(page, "my-app", "src/main/java", "com.mycompany.app");
10098

101-
// Select AppToDelete and invoke delete via context menu.
99+
// Right-click AppToDelete directly (no left-click to avoid opening
100+
// the file and losing tree focus).
102101
const appToDelete = page.getByRole(VSCode.TREE_ITEM_ROLE, { name: "AppToDelete" }).first();
103-
await appToDelete.click();
104-
await page.waitForTimeout(Timeout.CLICK);
105-
106102
await VscodeOperator.selectContextMenuItem(page, appToDelete, /^Delete/);
107103

108104
// Confirm deletion in dialog

test/e2e/utils/vscodeOperator.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -169,24 +169,22 @@ export default class VscodeOperator {
169169
/**
170170
* Right-clicks an element and selects an item from the context menu.
171171
*
172-
* VS Code Electron context menus do not respond to Playwright's
173-
* `.click()` or `.dispatchEvent()` — the menu stays open. Instead,
174-
* we locate the menu item by role and use raw `page.mouse.click()`
175-
* with the element's bounding-box coordinates, which sends CDP-level
176-
* mouse events that Electron handles correctly.
172+
* Scopes the search to `.monaco-menu-container .monaco-menu` to avoid
173+
* matching menubar items. Hovers the item first to trigger VS Code's
174+
* menu focus, then waits for the `.focused` CSS class before clicking.
177175
*/
178-
static async selectContextMenuItem(page: Page, target: ReturnType<Page["locator"]>, menuItemLabel: string | RegExp): Promise<void> {
176+
static async selectContextMenuItem(page: Page, target: ReturnType<Page["locator"]>, menuItemName: string | RegExp): Promise<void> {
179177
await target.click({ button: "right" });
180-
const menu = page.locator(".monaco-menu-container");
178+
const menu = page.locator(".monaco-menu-container .monaco-menu");
181179
await menu.waitFor({ state: "visible", timeout: 5_000 });
182-
const menuItem = menu.getByRole("menuitem", { name: menuItemLabel });
180+
const menuItem = menu.getByRole("menuitem", { name: menuItemName });
183181
await menuItem.first().waitFor({ state: "visible", timeout: 5_000 });
184-
// Use raw mouse click at the centre of the element's bounding box.
185-
const box = await menuItem.first().boundingBox();
186-
if (!box) {
187-
throw new Error(`Could not get bounding box for menu item "${menuItemLabel}"`);
188-
}
189-
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
182+
await menuItem.first().hover();
183+
await page.locator(".monaco-menu-container .action-item.focused").waitFor({
184+
state: "visible",
185+
timeout: 5_000,
186+
});
187+
await menuItem.first().click();
190188
await page.waitForTimeout(Timeout.CLICK);
191189
}
192190

@@ -242,6 +240,19 @@ export default class VscodeOperator {
242240
await page.waitForTimeout(Timeout.CLICK);
243241
}
244242

243+
/**
244+
* Clicks a button inside a notification toast (e.g. refactoring confirmations
245+
* from extensions that use `window.showInformationMessage` with action buttons).
246+
*/
247+
static async clickNotificationButton(page: Page, buttonLabel: string, timeoutMs = 10_000): Promise<void> {
248+
const notification = page.locator(".notification-toast");
249+
await notification.first().waitFor({ state: "visible", timeout: timeoutMs });
250+
const btn = notification.getByRole(VSCode.BUTTON_ROLE, { name: buttonLabel });
251+
await btn.first().waitFor({ state: "visible", timeout: timeoutMs });
252+
await btn.first().click();
253+
await page.waitForTimeout(Timeout.CLICK);
254+
}
255+
245256
// -----------------------------------------------------------------------
246257
// Editor
247258
// -----------------------------------------------------------------------

0 commit comments

Comments
 (0)