From a30b6d74155f9427266974bf29ef4d8daba91b15 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Fri, 24 Apr 2026 11:41:20 +0800 Subject: [PATCH] feat: replace all e2e/UI tests with autotest, add revealInProjectExplorer - Delete test/e2e/ (Playwright) and test/ui/ (vscode-extension-tester) - Remove @playwright/test and vscode-extension-tester deps - Remove test-ui script, update test-e2e to use autotest - Add revealInProjectExplorer test to project-explorer plan - Add Write Job Summary step to CI workflows - Add LLM env vars to CI for failure analysis Coverage: 48 steps across 2 plans covering 8 scenarios: - Project Explorer: focus, link, unlink, revealInProjectExplorer - File Operations: create class, create package, rename, delete Scenarios NOT migrated (require native OS file dialogs): - addLibraries, addLibraryFolders, java.project.create Local verification: 48/48 passed (31/31 + 17/17) --- .github/workflows/linuxUI.yml | 7 + .github/workflows/windowsUI.yml | 8 + package.json | 5 +- test/e2e-plans/java-dep-project-explorer.yaml | 29 +- test/e2e/fixtures/baseTest.ts | 191 ------- test/e2e/globalSetup.ts | 34 -- test/e2e/playwright.config.ts | 29 - test/e2e/tests/fileOperations.test.ts | 130 ----- test/e2e/tests/libraries.test.ts | 39 -- test/e2e/tests/projectExplorer.test.ts | 62 --- test/e2e/utils/constants.ts | 61 --- test/e2e/utils/javaOperator.ts | 145 ----- test/e2e/utils/vscodeOperator.ts | 283 ---------- test/invisible/lib/simple.jar | Bin 0 -> 901 bytes test/ui/command.test.ts | 513 ------------------ test/ui/index.ts | 25 - 16 files changed, 42 insertions(+), 1519 deletions(-) delete mode 100644 test/e2e/fixtures/baseTest.ts delete mode 100644 test/e2e/globalSetup.ts delete mode 100644 test/e2e/playwright.config.ts delete mode 100644 test/e2e/tests/fileOperations.test.ts delete mode 100644 test/e2e/tests/libraries.test.ts delete mode 100644 test/e2e/tests/projectExplorer.test.ts delete mode 100644 test/e2e/utils/constants.ts delete mode 100644 test/e2e/utils/javaOperator.ts delete mode 100644 test/e2e/utils/vscodeOperator.ts create mode 100644 test/invisible/lib/simple.jar delete mode 100644 test/ui/command.test.ts delete mode 100644 test/ui/index.ts diff --git a/.github/workflows/linuxUI.yml b/.github/workflows/linuxUI.yml index 583db06a..ac24e494 100644 --- a/.github/workflows/linuxUI.yml +++ b/.github/workflows/linuxUI.yml @@ -64,3 +64,10 @@ jobs: name: e2e-results-linux path: test-results/ retention-days: 7 + + - name: Write Job Summary + if: always() + run: | + if [ -f test-results/summary.md ]; then + cat test-results/summary.md >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/.github/workflows/windowsUI.yml b/.github/workflows/windowsUI.yml index bd3c274f..c92c6640 100644 --- a/.github/workflows/windowsUI.yml +++ b/.github/workflows/windowsUI.yml @@ -62,3 +62,11 @@ jobs: name: e2e-results-windows path: test-results/ retention-days: 7 + + - name: Write Job Summary + if: always() + shell: bash + run: | + if [ -f test-results/summary.md ]; then + cat test-results/summary.md >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/package.json b/package.json index a058e657..e62892da 100644 --- a/package.json +++ b/package.json @@ -1150,8 +1150,7 @@ "compile": "tsc -p . && webpack --config webpack.config.js --mode development", "watch": "webpack --mode development --watch", "test": "tsc -p . && webpack --config webpack.config.js --mode development && node ./dist/test/index.js", - "test-ui": "tsc -p . && webpack --config webpack.config.js --mode development && node ./dist/test/ui/index.js", - "test-e2e": "npx playwright test --config test/e2e/playwright.config.ts", + "test-e2e": "autotest run-all test/e2e-plans --no-llm", "build-server": "node scripts/buildJdtlsExt.js", "vscode:prepublish": "tsc -p ./ && webpack --mode production", "tslint": "tslint -t verbose --project tsconfig.json" @@ -1172,8 +1171,6 @@ "ts-loader": "^9.4.2", "tslint": "^6.1.3", "typescript": "^4.9.4", - "vscode-extension-tester": "^8.23.0", - "@playwright/test": "^1.50.0", "webpack": "^5.105.0", "webpack-cli": "^4.10.0" }, diff --git a/test/e2e-plans/java-dep-project-explorer.yaml b/test/e2e-plans/java-dep-project-explorer.yaml index e14a4f81..1bdb3656 100644 --- a/test/e2e-plans/java-dep-project-explorer.yaml +++ b/test/e2e-plans/java-dep-project-explorer.yaml @@ -1,17 +1,18 @@ # Test Plan: Java Dependency — Project Explorer # -# Covers projectExplorer.test.ts scenarios: +# Covers scenarios: # - javaProjectExplorer.focus shows Java Projects section # - linkWithFolderExplorer reveals active file in tree # - unlinkWithFolderExplorer stops auto-reveal +# - revealInProjectExplorer reveals file from File Explorer context menu # # Usage: # npx autotest run test/e2e-plans/java-dep-project-explorer.yaml --vsix name: "Java Dependency — Project Explorer" description: | - Tests the Java Projects explorer view: focus, link/unlink with editor. - Replaces test/e2e/tests/projectExplorer.test.ts. + Tests the Java Projects explorer view: focus, link/unlink with editor, + reveal in project explorer. setup: extension: "redhat.java" @@ -85,3 +86,25 @@ steps: - id: "relink-editor" action: "run command Java: Link with Editor" verify: "Editor re-linked with tree" + + # ── Test 4: revealInProjectExplorer ── + # Collapse all tree nodes, then reveal App.java from editor + - id: "collapse-all" + action: "run command View: Collapse All" + verify: "Collapse tree to reset state" + + - id: "open-app-file" + action: "open file App.java" + waitBefore: 2 + + - id: "reveal-in-project-explorer" + action: "run command Java: Reveal in Java Project Explorer" + waitBefore: 2 + + - id: "verify-revealed" + action: "wait 2 seconds" + verify: "App class should be revealed in Java Projects tree" + verifyTreeItem: + name: "App" + exact: true + timeout: 15 diff --git a/test/e2e/fixtures/baseTest.ts b/test/e2e/fixtures/baseTest.ts deleted file mode 100644 index 83c7f8a9..00000000 --- a/test/e2e/fixtures/baseTest.ts +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -/** - * Playwright test fixture that launches VS Code via Electron, - * opens a temporary copy of a test project, and tears everything - * down after the test. - * - * Usage in test files: - * - * import { test, expect } from "../fixtures/baseTest"; - * - * test("my test", async ({ page }) => { - * // `page` is a Playwright Page attached to VS Code - * }); - */ - -import { _electron, test as base, type Page } from "@playwright/test"; -import { downloadAndUnzipVSCode } from "@vscode/test-electron"; -import * as fs from "fs-extra"; -import * as os from "os"; -import * as path from "path"; - -export { expect } from "@playwright/test"; - -// Root of the extension source tree -const EXTENSION_ROOT = path.join(__dirname, "..", "..", ".."); -// Root of the test data projects -const TEST_DATA_ROOT = path.join(EXTENSION_ROOT, "test"); - -export type TestOptions = { - /** VS Code version to download, default "stable" */ - vscodeVersion: string; - /** Relative path under `test/` to the project to open (e.g. "maven") */ - testProjectDir: string; -}; - -type TestFixtures = TestOptions & { - /** Playwright Page connected to the VS Code Electron window */ - page: Page; -}; - -export const test = base.extend({ - vscodeVersion: [process.env.VSCODE_VERSION || "stable", { option: true }], - testProjectDir: ["maven", { option: true }], - - page: async ({ vscodeVersion, testProjectDir }, use, testInfo) => { - // 1. Create a temp directory and copy the test project into it. - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "java-dep-e2e-")); - const projectName = path.basename(testProjectDir); - const projectDir = path.join(tmpDir, projectName); - fs.copySync(path.join(TEST_DATA_ROOT, testProjectDir), projectDir); - - // Write VS Code settings to suppress telemetry prompts and notification noise - const vscodeDir = path.join(projectDir, ".vscode"); - fs.ensureDirSync(vscodeDir); - const settingsPath = path.join(vscodeDir, "settings.json"); - let existingSettings: Record = {}; - if (fs.existsSync(settingsPath)) { - // settings.json may contain JS-style comments (JSONC), strip them before parsing - const raw = fs.readFileSync(settingsPath, "utf-8"); - const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""); - try { - existingSettings = JSON.parse(stripped); - } catch { - // If still invalid, start fresh — our injected settings are more important - existingSettings = {}; - } - } - const mergedSettings = { - ...existingSettings, - "telemetry.telemetryLevel": "off", - "redhat.telemetry.enabled": false, - "workbench.colorTheme": "Default Dark Modern", - "update.mode": "none", - "extensions.ignoreRecommendations": true, - }; - fs.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 4)); - - // 2. Resolve VS Code executable. - const vscodePath = await downloadAndUnzipVSCode(vscodeVersion); - // resolveCliArgsFromVSCodeExecutablePath returns CLI-specific args - // (e.g. --ms-enable-electron-run-as-node) that are unsuitable for - // Electron UI launch. Extract only --extensions-dir and --user-data-dir. - const vscodeTestDir = path.join(EXTENSION_ROOT, ".vscode-test"); - const extensionsDir = path.join(vscodeTestDir, "extensions"); - const userDataDir = path.join(vscodeTestDir, "user-data"); - - // 3. Launch VS Code as an Electron app. - const electronApp = await _electron.launch({ - executablePath: vscodePath, - env: { ...process.env, NODE_ENV: "development" }, - args: [ - "--no-sandbox", - "--disable-gpu-sandbox", - "--disable-updates", - "--skip-welcome", - "--skip-release-notes", - "--disable-workspace-trust", - "--password-store=basic", - // Suppress notifications that block UI interactions - "--disable-telemetry", - `--extensions-dir=${extensionsDir}`, - `--user-data-dir=${userDataDir}`, - `--extensionDevelopmentPath=${EXTENSION_ROOT}`, - projectDir, - ], - }); - - const page = await electronApp.firstWindow(); - - // Auto-dismiss Electron native dialogs (e.g. redhat.java refactoring - // confirmation, delete file confirmation). These dialogs are outside - // the renderer DOM and cannot be handled via Playwright Page API. - // Monkey-patch dialog.showMessageBox to find and click the confirm - // button by label, falling back to the first button. - await electronApp.evaluate(({ dialog }) => { - const confirmLabels = /^(OK|Delete|Move to Recycle Bin|Move to Trash)$/i; - dialog.showMessageBox = async (_win: any, opts: any) => { - const options = opts || _win; - const buttons: string[] = options?.buttons || []; - let idx = buttons.findIndex((b: string) => confirmLabels.test(b)); - if (idx < 0) idx = 0; - return { response: idx, checkboxChecked: true }; - }; - dialog.showMessageBoxSync = (_win: any, opts: any) => { - const options = opts || _win; - const buttons: string[] = options?.buttons || []; - let idx = buttons.findIndex((b: string) => confirmLabels.test(b)); - if (idx < 0) idx = 0; - return idx; - }; - }); - - // Dismiss any startup notifications/dialogs before handing off to tests - await page.waitForTimeout(3_000); - await dismissAllNotifications(page); - - // Tracing is handled by Playwright's built-in `use.trace` config - // (see playwright.config.ts). No manual tracing.start/stop needed. - - // ---- hand off to the test ---- - await use(page); - - // ---- teardown ---- - // Trace saving is handled automatically by Playwright's `use.trace` - // config — no manual tracing.stop() needed here. - - await electronApp.close(); - - // Clean up temp directory - try { - fs.rmSync(tmpDir, { force: true, recursive: true }); - } catch (e) { - console.warn(`Warning: failed to clean up ${tmpDir}: ${e}`); - } - }, -}); - -/** - * Dismiss all VS Code notification toasts (telemetry prompts, theme suggestions, etc.). - * These notifications can steal focus and block Quick Open / Command Palette interactions. - */ -async function dismissAllNotifications(page: Page): Promise { - try { - // Click "Clear All Notifications" if the notification center button is visible - const clearAll = page.locator(".notifications-toasts .codicon-notifications-clear-all, .notification-toast .codicon-close"); - let count = await clearAll.count().catch(() => 0); - while (count > 0) { - await clearAll.first().click(); - await page.waitForTimeout(500); - count = await clearAll.count().catch(() => 0); - } - - // Also try the command palette approach as a fallback - const notificationToasts = page.locator(".notification-toast"); - if (await notificationToasts.count().catch(() => 0) > 0) { - // Use keyboard shortcut to clear all notifications - await page.keyboard.press("Control+Shift+P"); - const input = page.locator(".quick-input-widget input.input"); - if (await input.isVisible({ timeout: 3_000 }).catch(() => false)) { - await input.fill("Notifications: Clear All Notifications"); - await page.waitForTimeout(500); - await input.press("Enter"); - await page.waitForTimeout(500); - } - } - } catch { - // Best effort - } -} diff --git a/test/e2e/globalSetup.ts b/test/e2e/globalSetup.ts deleted file mode 100644 index 37eabd23..00000000 --- a/test/e2e/globalSetup.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -import { downloadAndUnzipVSCode, resolveCliArgsFromVSCodeExecutablePath } from "@vscode/test-electron"; -import * as childProcess from "child_process"; - -/** - * Global setup runs once before all test files. - * It downloads VS Code and installs the redhat.java extension so that - * every test run starts from an identical, pre-provisioned state. - * - * Our own extension is loaded at launch time via --extensionDevelopmentPath - * (see baseTest.ts), so there is no need to install a VSIX here. - */ -export default async function globalSetup(): Promise { - // Download VS Code stable (or the version configured via VSCODE_VERSION env). - const vscodeVersion = process.env.VSCODE_VERSION || "stable"; - console.log(`[globalSetup] Downloading VS Code ${vscodeVersion}…`); - const vscodePath = await downloadAndUnzipVSCode(vscodeVersion); - const [cli, ...cliArgs] = resolveCliArgsFromVSCodeExecutablePath(vscodePath); - - // On Windows, the CLI is a .cmd batch file which requires shell: true. - const isWindows = process.platform === "win32"; - const execOptions: childProcess.ExecFileSyncOptions = { - encoding: "utf-8", - stdio: "inherit", - timeout: 120_000, - shell: isWindows, - }; - - // Install the Language Support for Java extension from the Marketplace. - console.log("[globalSetup] Installing redhat.java extension…"); - childProcess.execFileSync(cli, [...cliArgs, "--install-extension", "redhat.java"], execOptions); -} diff --git a/test/e2e/playwright.config.ts b/test/e2e/playwright.config.ts deleted file mode 100644 index 4af6182b..00000000 --- a/test/e2e/playwright.config.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -import { defineConfig } from "@playwright/test"; -import * as path from "path"; - -export default defineConfig({ - testDir: path.join(__dirname, "tests"), - reporter: process.env.CI - ? [["list"], ["junit", { outputFile: path.join(__dirname, "..", "..", "test-results", "e2e-results.xml") }]] - : "list", - // Java Language Server can take 2-3 minutes to fully index on first run. - timeout: 240_000, - // Run tests sequentially — launching multiple VS Code instances is too resource-heavy. - workers: 1, - // Allow one retry in CI to handle transient environment issues. - retries: process.env.CI ? 1 : 0, - expect: { - timeout: 30_000, - }, - globalSetup: path.join(__dirname, "globalSetup.ts"), - use: { - // Capture full trace on every test run locally (includes screenshots, - // DOM snapshots, and network at each Playwright action). In CI, - // retain traces only for failing tests to limit artifact size. - trace: process.env.CI ? "retain-on-failure" : "on", - }, - outputDir: path.join(__dirname, "..", "..", "test-results", "e2e"), -}); diff --git a/test/e2e/tests/fileOperations.test.ts b/test/e2e/tests/fileOperations.test.ts deleted file mode 100644 index 97998e2e..00000000 --- a/test/e2e/tests/fileOperations.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -/** - * E2E tests for file / resource operations in the Java Projects view. - * - * Covers: - * - java.view.package.newJavaClass - * - java.view.package.newPackage - * - java.view.package.renameFile - * - java.view.package.moveFileToTrash - */ - -import { test, expect } from "../fixtures/baseTest"; -import { Timeout, VSCode } from "../utils/constants"; -import JavaOperator from "../utils/javaOperator"; -import VscodeOperator from "../utils/vscodeOperator"; - -test.describe("File Operations", () => { - - test.use({ testProjectDir: "maven" }); - - test.beforeEach(async ({ page }) => { - await VscodeOperator.dismissModalDialog(page); - await JavaOperator.openFile(page, "App.java"); - await JavaOperator.waitForJavaLSReady(page); - await JavaOperator.focusJavaProjects(page); - }); - - test("create new Java class", async ({ page }) => { - // Trigger New... on the project node - await JavaOperator.triggerNewResource(page, "my-app"); - - // Select "Java Class" (first item) - await VscodeOperator.selectQuickPickIndex(page, 0); - - // Select source folder "src/main/java" - await VscodeOperator.selectQuickPickItem(page, "src/main/java"); - - // Type class name and confirm - await VscodeOperator.fillQuickInput(page, "App2"); - - // Editor should open with the new file - const tabFound = await VscodeOperator.waitForEditorTab(page, "App2.java"); - expect(tabFound).toBeTruthy(); - }); - - test("create new package", async ({ page }) => { - await JavaOperator.triggerNewResource(page, "my-app"); - - // Select "Package" - await VscodeOperator.selectQuickPickItem(page, "Package"); - - // Select source folder - await VscodeOperator.selectQuickPickItem(page, "src/main/java"); - - // Type package name and confirm - await VscodeOperator.fillQuickInput(page, "com.mycompany.newpkg"); - - // Wait briefly for directory creation - await page.waitForTimeout(Timeout.TREE_EXPAND); - }); - - test("rename Java file", async ({ page }) => { - await JavaOperator.collapseFileExplorer(page); - - // Expand to AppToRename - await JavaOperator.expandTreePath(page, "my-app", "src/main/java", "com.mycompany.app"); - - // Right-click AppToRename to select it AND open the context menu. - // We do NOT left-click first because that opens the file in the editor - // and steals focus away from the tree view. - const appToRename = page.getByRole(VSCode.TREE_ITEM_ROLE, { name: "AppToRename" }).first(); - await VscodeOperator.selectContextMenuItem(page, appToRename, /^Rename/); - - // The extension shows a showInputBox (quick-input) for the new name - await VscodeOperator.fillQuickInput(page, "AppRenamed"); - - // Handle extension's own rename confirmation dialog if it appears. - // The Electron native refactoring dialog from redhat.java is - // auto-dismissed by the showMessageBox monkey-patch in baseTest.ts. - try { - await VscodeOperator.clickDialogButton(page, "OK", 5_000); - } catch { - // Dialog may not appear in all cases - } - - // On Linux, if the refactoring dialog resolved to "Show Preview", - // VS Code shows a Refactor Preview panel with "Apply" / "Discard" - // buttons. Click "Apply" to complete the rename. - try { - const applyBtn = page.getByRole(VSCode.BUTTON_ROLE, { name: "Apply" }); - if (await applyBtn.isVisible({ timeout: 3_000 }).catch(() => false)) { - await applyBtn.click(); - await page.waitForTimeout(Timeout.CLICK); - } - } catch { - // No refactor preview - } - - // Editor should open with renamed file - const tabFound = await VscodeOperator.waitForEditorTab(page, "AppRenamed.java"); - expect(tabFound).toBeTruthy(); - }); - - test("delete Java file", async ({ page }) => { - await JavaOperator.collapseFileExplorer(page); - await JavaOperator.expandTreePath(page, "my-app", "src/main/java", "com.mycompany.app"); - - // Right-click AppToDelete directly (no left-click to avoid opening - // the file and losing tree focus). - const appToDelete = page.getByRole(VSCode.TREE_ITEM_ROLE, { name: "AppToDelete" }).first(); - await VscodeOperator.selectContextMenuItem(page, appToDelete, /^Delete/); - - // Confirm deletion in dialog - try { - const dialog = page.locator(".monaco-dialog-box"); - await dialog.waitFor({ state: "visible", timeout: 5_000 }); - const confirmBtn = dialog.getByRole(VSCode.BUTTON_ROLE) - .filter({ hasText: /Move to Trash|Move to Recycle Bin|Delete|OK/ }); - await confirmBtn.first().click(); - } catch { - // Dialog may not appear - } - - // Wait for tree item to disappear - const gone = await VscodeOperator.waitForTreeItemGone(page, "AppToDelete"); - expect(gone).toBeTruthy(); - }); -}); diff --git a/test/e2e/tests/libraries.test.ts b/test/e2e/tests/libraries.test.ts deleted file mode 100644 index 0fb5844e..00000000 --- a/test/e2e/tests/libraries.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -/** - * E2E tests for library management and project creation. - * - * Covers: - * - java.project.addLibraries - * - java.project.addLibraryFolders - * - java.project.create - */ - -import { test } from "../fixtures/baseTest"; - -test.describe("Libraries & Project Creation", () => { - - test.describe("invisible project library management", () => { - - test.use({ testProjectDir: "invisible" }); - - test.skip("add and remove JAR library", async () => { - // Skip: the addLibraries command opens a native OS file dialog - // (vscode.window.showOpenDialog) which Playwright cannot automate. - // This test requires Electron dialog mocking support. - }); - }); - - test.describe("create new project", () => { - - test.use({ testProjectDir: "invisible" }); - - test.skip("java.project.create with no build tools", async () => { - // Skip: after selecting "No build tools", scaffoldSimpleProject() - // calls vscode.window.showOpenDialog() which opens a native OS file - // dialog that Playwright cannot automate. This test requires - // Electron dialog mocking support. - }); - }); -}); diff --git a/test/e2e/tests/projectExplorer.test.ts b/test/e2e/tests/projectExplorer.test.ts deleted file mode 100644 index b32a58df..00000000 --- a/test/e2e/tests/projectExplorer.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -/** - * E2E tests for the Java Projects explorer view. - * - * Covers: - * - javaProjectExplorer.focus - * - java.view.package.linkWithFolderExplorer - * - java.view.package.unlinkWithFolderExplorer - */ - -import { test, expect } from "../fixtures/baseTest"; -import { Timeout, VSCode } from "../utils/constants"; -import JavaOperator from "../utils/javaOperator"; -import VscodeOperator from "../utils/vscodeOperator"; - -test.describe("Project Explorer", () => { - - test.use({ testProjectDir: "maven" }); - - test.beforeEach(async ({ page }) => { - await VscodeOperator.dismissModalDialog(page); - // Open a Java file so the language server activates - await JavaOperator.openFile(page, "App.java"); - await JavaOperator.waitForJavaLSReady(page); - await JavaOperator.focusJavaProjects(page); - }); - - test("javaProjectExplorer.focus shows Java Projects section", async ({ page }) => { - // beforeEach already focuses Java Projects; verify the tree is populated - const found = await VscodeOperator.waitForTreeItem(page, "my-app", 15_000); - expect(found).toBeTruthy(); - }); - - test("linkWithFolderExplorer reveals active file in tree", async ({ page }) => { - // Expand project to source level - await JavaOperator.expandTreePath(page, "my-app", "src/main/java"); - - // The package node should expand and reveal the class - const packageVisible = await VscodeOperator.waitForTreeItem(page, "com.mycompany.app", 15_000); - expect(packageVisible).toBeTruthy(); - - // Use exact match to avoid matching "app", "App.java", "AppToDelete" etc. - const classVisible = await page.getByRole(VSCode.TREE_ITEM_ROLE, { name: "App", exact: true }) - .isVisible().catch(() => false); - expect(classVisible).toBeTruthy(); - }); - - test("unlinkWithFolderExplorer stops auto-reveal", async ({ page }) => { - // Use command to unlink - await VscodeOperator.executeCommand(page, "Java: Unlink with Editor"); - await page.waitForTimeout(Timeout.CLICK); - - // Open a different file — tree should NOT auto-expand - await JavaOperator.openFile(page, "AppToRename.java"); - await page.waitForTimeout(Timeout.TREE_EXPAND); - - // Re-link for cleanup - await VscodeOperator.executeCommand(page, "Java: Link with Editor"); - }); -}); diff --git a/test/e2e/utils/constants.ts b/test/e2e/utils/constants.ts deleted file mode 100644 index 8a409e74..00000000 --- a/test/e2e/utils/constants.ts +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -/** - * Centralized constants for VS Code E2E tests. - * Keep ARIA role strings, timeouts, and selector patterns here - * so that tests and operators stay tidy. - */ - -// --------------------------------------------------------------------------- -// Timeouts (milliseconds) -// --------------------------------------------------------------------------- - -export const Timeout = { - /** Short pause after a click or keystroke */ - CLICK: 1_000, - /** Longer pause after expanding a tree item (DOM needs time to render children) */ - TREE_EXPAND: 3_000, - /** Wait before the first assertion in a test (let VS Code settle) */ - PREPARE: 5_000, - /** Wait for a heavy extension to activate */ - EXTENSION_ACTIVATE: 10_000, - /** Maximum wait for Java Language Server to report "Ready" */ - JAVA_LS_READY: 180_000, - /** Interval between polls when waiting for LS readiness */ - JAVA_LS_POLL_INTERVAL: 2_000, -} as const; - -// --------------------------------------------------------------------------- -// VS Code ARIA roles & selectors -// --------------------------------------------------------------------------- - -export const VSCode = { - // Command palette - CMD_PALETTE_KEY: "F1", - CMD_PALETTE_ROLE: "combobox" as const, - CMD_PALETTE_INPUT_NAME: "INPUT", - OPTION_ROLE: "option" as const, - LISTBOX_ROLE: "listbox" as const, - // Side bar / activity bar - TAB_ROLE: "tab" as const, - // Tree view - TREE_ITEM_ROLE: "treeitem" as const, - // Buttons & toolbars - BUTTON_ROLE: "button" as const, - TOOLBAR_ROLE: "toolbar" as const, - // Keys - ENTER: "Enter", - ESCAPE: "Escape", - // Elements - LINK: "a", -} as const; - -// --------------------------------------------------------------------------- -// Java-specific -// --------------------------------------------------------------------------- - -export const Java = { - JAVA_PROJECTS_SECTION: "Java Projects", - JAVA_LS_STATUS_LABEL: "Language Status", -} as const; diff --git a/test/e2e/utils/javaOperator.ts b/test/e2e/utils/javaOperator.ts deleted file mode 100644 index 67d2657f..00000000 --- a/test/e2e/utils/javaOperator.ts +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -/** - * Java-specific helpers for E2E tests. - * - * The most important one is `waitForJavaLSReady()` which polls the status bar - * until Language Support for Java reports a "ready" state, using Playwright's - * `expect.poll` so that the test automatically retries and fails cleanly if - * the LS never reaches readiness. - */ - -import { expect, Page } from "@playwright/test"; -import { Timeout, VSCode } from "./constants"; -import VscodeOperator from "./vscodeOperator"; - -export default class JavaOperator { - - /** - * Waits for the Java Language Server to finish indexing. - * - * Strategy: poll the status bar for a button whose accessible name - * contains "Java: Ready" (e.g. "coffee Java: Ready, Show Java status menu"). - * This is more reliable than clicking a hover because it doesn't depend - * on internal VS Code DOM IDs that vary across versions. - */ - static async waitForJavaLSReady(page: Page, timeoutMs = Timeout.JAVA_LS_READY): Promise { - // Give the extension a moment to register its status bar item - await page.waitForTimeout(Timeout.EXTENSION_ACTIVATE); - - await expect.poll(async () => { - try { - const javaReadyButton = page.getByRole(VSCode.BUTTON_ROLE, { name: /Java:\s*Ready/i }); - if (await javaReadyButton.isVisible().catch(() => false)) { - return "ready"; - } - return "not-ready"; - } catch { - return "not-ready"; - } - }, { - message: "Java Language Server did not become ready in time", - timeout: timeoutMs, - intervals: [Timeout.JAVA_LS_POLL_INTERVAL], - }).toBe("ready"); - } - - /** - * Focuses the Java Projects view and waits for it to render. - * - * Directly clicks the "Java Projects Section" button in the Explorer - * sidebar rather than going through the command palette, which may - * fail to find the view-focus command by its ID. - */ - static async focusJavaProjects(page: Page): Promise { - const sectionButton = page.getByRole(VSCode.BUTTON_ROLE, { name: /Java Projects Section/i }); - if (await sectionButton.isVisible({ timeout: 5_000 }).catch(() => false)) { - // Only click to expand if the section is currently collapsed - const expanded = await sectionButton.getAttribute("aria-expanded"); - if (expanded !== "true") { - await sectionButton.click(); - } - } else { - // Fallback: try via command palette - await VscodeOperator.executeCommand(page, "Java Projects: Focus on Java Projects View"); - } - await page.waitForTimeout(Timeout.TREE_EXPAND); - } - - /** - * Expands tree items along a path (e.g. "my-app" → "src/main/java" → "com.mycompany.app"). - * - * Waits for each item's `aria-expanded` attribute to appear before clicking, - * because VS Code only sets it after the tree data provider's `getChildren()` - * has returned — until then the node is treated as a leaf. - */ - static async expandTreePath(page: Page, ...labels: string[]): Promise { - for (const label of labels) { - const item = page.getByRole(VSCode.TREE_ITEM_ROLE, { name: label }).first(); - await item.waitFor({ state: "visible", timeout: 15_000 }); - - // Wait for the node to become expandable (aria-expanded present). - await expect.poll(async () => { - return await item.getAttribute("aria-expanded"); - }, { - message: `Tree item "${label}" did not become expandable`, - timeout: 15_000, - }).toBeTruthy(); - - const expanded = await item.getAttribute("aria-expanded"); - if (expanded !== "true") { - await item.click(); - await page.waitForTimeout(Timeout.TREE_EXPAND); - } - } - } - - /** - * Collapses the default file explorer section so that tree items in the - * Java Projects view are within the viewport. - */ - static async collapseFileExplorer(page: Page): Promise { - try { - // Try to collapse any expanded section above Java Projects - const sections = page.locator(".split-view-view .pane-header[aria-expanded='true']"); - const count = await sections.count(); - if (count > 0) { - await sections.first().click(); - await page.waitForTimeout(Timeout.CLICK); - } - } catch { - // Best-effort - } - } - - /** - * Opens a file in the editor via Quick Open (Ctrl+P). - */ - static async openFile(page: Page, filePath: string): Promise { - // Use Ctrl+P directly instead of going through command palette - await page.keyboard.press("Control+P"); - const input = page.locator(".quick-input-widget input.input"); - await input.waitFor({ state: "visible", timeout: 10_000 }); - await input.fill(filePath); - await page.waitForTimeout(Timeout.CLICK); - // Wait for file matches to appear, then select the first one - const firstMatch = page.locator(".quick-input-widget .quick-input-list .monaco-list-row").first(); - if (await firstMatch.isVisible({ timeout: 3_000 }).catch(() => false)) { - await firstMatch.click(); - } else { - await input.press(VSCode.ENTER); - } - await page.waitForTimeout(Timeout.TREE_EXPAND); - } - - /** - * Triggers the "New..." action on a project node. - * This opens the resource-type quick-pick. - */ - static async triggerNewResource(page: Page, projectName: string): Promise { - await JavaOperator.collapseFileExplorer(page); - await VscodeOperator.clickTreeItem(page, projectName); - await VscodeOperator.clickTreeItemAction(page, projectName, "New..."); - } -} diff --git a/test/e2e/utils/vscodeOperator.ts b/test/e2e/utils/vscodeOperator.ts deleted file mode 100644 index 6dd34a80..00000000 --- a/test/e2e/utils/vscodeOperator.ts +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -/** - * Generic VS Code UI helpers built on Playwright Page. - * - * These helpers prefer ARIA roles / labels over CSS classes where possible - * to better survive VS Code version upgrades, but some fall back to CSS - * selectors when needed. - */ - -import { Page } from "@playwright/test"; -import { Timeout, VSCode } from "./constants"; - -export default class VscodeOperator { - - // ----------------------------------------------------------------------- - // Command palette - // ----------------------------------------------------------------------- - - /** - * Opens the command palette, types the given command, and runs it. - */ - static async executeCommand(page: Page, command: string): Promise { - await page.keyboard.press(VSCode.CMD_PALETTE_KEY); - // Wait for the quick-input widget to appear - const input = page.locator(".quick-input-widget input.input"); - await input.waitFor({ state: "visible", timeout: 10_000 }); - // F1 opens command palette with ">" prefix; fill() must preserve it - // so VS Code searches commands rather than files. - await input.fill(">" + command); - await page.waitForTimeout(Timeout.CLICK); - // Press Enter on the first matching option in the list - const firstOption = page.locator(".quick-input-widget .quick-input-list .monaco-list-row").first(); - if (await firstOption.isVisible({ timeout: 3_000 }).catch(() => false)) { - await firstOption.click(); - } else { - await input.press(VSCode.ENTER); - } - await page.waitForTimeout(Timeout.CLICK); - } - - /** - * Select a quick-pick option by its visible label text. - */ - static async selectQuickPickItem(page: Page, label: string): Promise { - const option = page.locator(".quick-input-widget .quick-input-list .monaco-list-row", { hasText: label }); - await option.first().waitFor({ state: "visible", timeout: 10_000 }); - await option.first().click(); - await page.waitForTimeout(Timeout.CLICK); - } - - /** - * Select a quick-pick option by its zero-based index. - */ - static async selectQuickPickIndex(page: Page, index: number): Promise { - const option = page.locator(".quick-input-widget .quick-input-list .monaco-list-row").nth(index); - await option.waitFor({ state: "visible", timeout: 10_000 }); - await option.click(); - await page.waitForTimeout(Timeout.CLICK); - } - - // ----------------------------------------------------------------------- - // Quick input box - // ----------------------------------------------------------------------- - - /** - * Waits for the quick-input box to appear and returns the input locator. - */ - static async getQuickInput(page: Page): Promise> { - const input = page.locator(".quick-input-widget input.input"); - await input.waitFor({ state: "visible", timeout: 10_000 }); - return input; - } - - /** - * Types text into the quick-input and confirms with Enter. - */ - static async fillQuickInput(page: Page, text: string): Promise { - const input = await VscodeOperator.getQuickInput(page); - await input.fill(text); - await page.waitForTimeout(Timeout.CLICK); - await input.press(VSCode.ENTER); - await page.waitForTimeout(Timeout.CLICK); - } - - // ----------------------------------------------------------------------- - // Side bar / Activity bar - // ----------------------------------------------------------------------- - - /** - * Clicks a side-bar tab by its accessibility label (e.g. "Explorer", "Java Projects"). - */ - static async activateSideTab(page: Page, tabName: string, timeout = Timeout.CLICK): Promise { - await page.getByRole(VSCode.TAB_ROLE, { name: tabName }).locator(VSCode.LINK).click(); - await page.waitForTimeout(timeout); - } - - static async isSideTabVisible(page: Page, tabName: string): Promise { - return page.getByRole(VSCode.TAB_ROLE, { name: tabName }).isVisible(); - } - - // ----------------------------------------------------------------------- - // Tree items - // ----------------------------------------------------------------------- - - /** - * Returns whether a tree item with the given name is visible. - */ - static async isTreeItemVisible(page: Page, name: string): Promise { - return page.getByRole(VSCode.TREE_ITEM_ROLE, { name }).isVisible(); - } - - /** - * Clicks a tree item by name. - */ - static async clickTreeItem(page: Page, name: string): Promise { - await page.getByRole(VSCode.TREE_ITEM_ROLE, { name }).locator(VSCode.LINK).first().click(); - await page.waitForTimeout(Timeout.TREE_EXPAND); - } - - /** - * Waits for a tree item to appear in the DOM and become visible. - * Returns `true` if the item was found within `timeoutMs`. - */ - static async waitForTreeItem(page: Page, name: string, timeoutMs = 30_000): Promise { - try { - await page.getByRole(VSCode.TREE_ITEM_ROLE, { name }).first().waitFor({ - state: "visible", - timeout: timeoutMs, - }); - return true; - } catch { - return false; - } - } - - /** - * Waits until a tree item disappears from the view. - */ - static async waitForTreeItemGone(page: Page, name: string, timeoutMs = 15_000): Promise { - try { - await page.getByRole(VSCode.TREE_ITEM_ROLE, { name }).first().waitFor({ - state: "hidden", - timeout: timeoutMs, - }); - return true; - } catch { - return false; - } - } - - /** - * Clicks an inline action button on a tree item (the small icons that appear on hover). - * Uses aria-label matching so it works across VS Code versions. - */ - static async clickTreeItemAction(page: Page, itemName: string, actionLabel: string): Promise { - const treeItem = page.getByRole(VSCode.TREE_ITEM_ROLE, { name: itemName }); - // Hover to reveal inline action buttons - await treeItem.hover(); - await page.waitForTimeout(500); - await treeItem.locator(`a.action-label[role="button"][aria-label*="${actionLabel}"]`).click(); - await page.waitForTimeout(Timeout.CLICK); - } - - // ----------------------------------------------------------------------- - // Context menus - // ----------------------------------------------------------------------- - - /** - * Right-clicks an element and selects an item from the context menu. - * - * Scopes the search to `.monaco-menu-container .monaco-menu` to avoid - * matching menubar items. Hovers the item first to trigger VS Code's - * menu focus, then waits for the `.focused` CSS class before clicking. - */ - static async selectContextMenuItem(page: Page, target: ReturnType, menuItemName: string | RegExp): Promise { - await target.click({ button: "right" }); - const menu = page.locator(".monaco-menu-container .monaco-menu"); - await menu.waitFor({ state: "visible", timeout: 5_000 }); - const menuItem = menu.getByRole("menuitem", { name: menuItemName }); - await menuItem.first().waitFor({ state: "visible", timeout: 5_000 }); - await menuItem.first().hover(); - await page.locator(".monaco-menu-container .action-item.focused").waitFor({ - state: "visible", - timeout: 5_000, - }); - await menuItem.first().click(); - await page.waitForTimeout(Timeout.CLICK); - } - - // ----------------------------------------------------------------------- - // Dialogs - // ----------------------------------------------------------------------- - - /** - * Tries to dismiss a modal dialog (workspace-trust, update prompts, etc.) - * by clicking a button whose label matches one of the well-known accept labels. - * Silently succeeds if no dialog is present. - */ - static async dismissModalDialog(page: Page): Promise { - const acceptLabels = ["Yes, I trust the authors", "OK", "Yes", "Continue", "I Trust the Authors"]; - try { - // Handle modal dialogs - const dialog = page.locator(".monaco-dialog-box"); - if (await dialog.isVisible({ timeout: 2_000 }).catch(() => false)) { - for (const label of acceptLabels) { - const btn = dialog.getByRole(VSCode.BUTTON_ROLE, { name: label }); - if (await btn.isVisible().catch(() => false)) { - await btn.click(); - await page.waitForTimeout(Timeout.CLICK); - break; - } - } - } - } catch { - // No modal dialog — nothing to dismiss - } - - // Also dismiss notification toasts (telemetry prompts, theme suggestions, etc.) - try { - const closeButtons = page.locator(".notification-toast .codicon-close"); - let count = await closeButtons.count().catch(() => 0); - while (count > 0) { - await closeButtons.first().click(); - await page.waitForTimeout(500); - count = await closeButtons.count().catch(() => 0); - } - } catch { - // Best effort - } - } - - /** - * Waits for a modal dialog to appear and clicks a button by its label. - */ - static async clickDialogButton(page: Page, buttonLabel: string, timeoutMs = 10_000): Promise { - const dialog = page.locator(".monaco-dialog-box"); - await dialog.waitFor({ state: "visible", timeout: timeoutMs }); - await dialog.getByRole(VSCode.BUTTON_ROLE, { name: buttonLabel }).click(); - await page.waitForTimeout(Timeout.CLICK); - } - - /** - * Clicks a button inside a notification toast (e.g. refactoring confirmations - * from extensions that use `window.showInformationMessage` with action buttons). - */ - static async clickNotificationButton(page: Page, buttonLabel: string, timeoutMs = 10_000): Promise { - const notification = page.locator(".notification-toast"); - await notification.first().waitFor({ state: "visible", timeout: timeoutMs }); - const btn = notification.getByRole(VSCode.BUTTON_ROLE, { name: buttonLabel }); - await btn.first().waitFor({ state: "visible", timeout: timeoutMs }); - await btn.first().click(); - await page.waitForTimeout(Timeout.CLICK); - } - - // ----------------------------------------------------------------------- - // Editor - // ----------------------------------------------------------------------- - - /** - * Waits for an editor tab with the given title to become active. - */ - static async waitForEditorTab(page: Page, title: string, timeoutMs = 15_000): Promise { - try { - await page.getByRole(VSCode.TAB_ROLE, { name: title }).first().waitFor({ - state: "visible", - timeout: timeoutMs, - }); - return true; - } catch { - return false; - } - } - - /** - * Saves the currently active editor using the command palette. - * (More reliable than Ctrl+S because focus might not be on the editor.) - */ - static async saveActiveEditor(page: Page): Promise { - await VscodeOperator.executeCommand(page, "workbench.action.files.save"); - } -} diff --git a/test/invisible/lib/simple.jar b/test/invisible/lib/simple.jar new file mode 100644 index 0000000000000000000000000000000000000000..d8cc4e6f4c1e70f26f29749bda44b67b6c644ce6 GIT binary patch literal 901 zcmWIWW@Zs#;Nak3n4R}0hye+RFt9NAx`sIFdiuHP`#So0y1532==r++JH^28+4sz8 zA8%c~i@e^tTIbH3-yCFc#rVO~Prhf)TrNH5siU_o=!3`ElPCGl>!0#``1H|JCWZiS zc8(*LzepJatuO@Q0Jz07(JU4K%6g>c$Cd^0s&DOS!vU z7Ur-s$h~}9@Wb;@rHoHEUN%ztpu9%0VS&SztXs!E$1P9XvHnY2D#wvke!<&c?aDs& z{m6=n_GcR#!sdppxp3>EZ?wY1>fpIy>nhh}ZrA=O3ss-}W$-sO0>e%rne8Yr^fnf?WD8(@{i7+4%J926SC3aMRE9oPf0!q2aHh@wt0#pK- X=;=7Xo0Scuf(Zy)fb?si+ZY%C!fI$z literal 0 HcmV?d00001 diff --git a/test/ui/command.test.ts b/test/ui/command.test.ts deleted file mode 100644 index 3f317918..00000000 --- a/test/ui/command.test.ts +++ /dev/null @@ -1,513 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -import * as assert from "assert"; -import * as fse from "fs-extra"; -import { platform, tmpdir } from "os"; -import * as path from "path"; -import * as seleniumWebdriver from "selenium-webdriver"; -import { ActivityBar, By, InputBox, ModalDialog, SideBarView, StatusBar, TextEditor, TreeItem, VSBrowser, ViewSection, Workbench } from "vscode-extension-tester"; -import { sleep } from "../util"; - -// tslint:disable: only-arrow-functions -const newProjectName = "helloworld"; -const testFolder = path.join(__dirname, "..", "..", "..", "test"); -const mavenProjectPath = path.join(testFolder, "maven"); -const mavenJavaFilePath = path.join("src", "main", "java", "com", "mycompany", "app", "App.java"); -const invisibleProjectPath = path.join(testFolder, "invisible"); -const invisibleJavaFilePath = path.join("src", "App.java"); - -// async function pauseInPipeline(timeInMs: number): Promise { -// if (process.env.GITHUB_ACTIONS) { -// return sleep(timeInMs); -// } else { -// return Promise.resolve(); -// } -// } - -describe("Command Tests", function() { - - this.timeout(5 * 60 * 1000 /*ms*/); - const mavenProjectTmpFolders: string[] = []; - let currentProjectPath: string | undefined; - let statusBar: StatusBar; - - function createTmpProjectFolder(projectName: string) { - const tmpFolder = fse.mkdtempSync(path.join(tmpdir(), 'vscode-java-dependency-ui-test')); - // Keep the folder name. - const projectFolder = path.join(tmpFolder, projectName); - fse.mkdirSync(projectFolder); - mavenProjectTmpFolders.push(tmpFolder); - return projectFolder; - } - - async function openProject(projectPath: string) { - const projectFolder = createTmpProjectFolder(path.basename(projectPath)); - // Copy to avoid restoring after each test run to revert changes done during the test. - fse.copySync(projectPath, projectFolder); - await VSBrowser.instance.openResources(projectFolder); - currentProjectPath = projectFolder; - await ensureExplorerIsOpen(); - } - - async function openFile(filePath: string) { - statusBar = new StatusBar(); - if (path.isAbsolute(filePath)) { - await VSBrowser.instance.openResources(filePath); - } else { - await VSBrowser.instance.openResources(path.join(currentProjectPath!, filePath)); - } - } - - async function waitForLanguageServerReady() { - // Wait until the language server is no longer indexing. - // Use a max wait to avoid infinite loops if the status UI changes between VS Code versions. - const maxWaitMs = 3 * 60 * 1000; - const startTime = Date.now(); - while (Date.now() - startTime < maxWaitMs) { - try { - const languageStatus = await statusBar.findElement(By.xpath('//*[@id="status.languageStatus"]')); - await languageStatus.click(); - // Accept either codicon-thumbsup (older VS Code) or codicon-pass (newer VS Code) - await languageStatus.findElement(By.xpath( - `//div[contains(@class, 'context-view')]//div[contains(@class, 'hover-language-status')]//*[contains(@class, 'codicon-thumbsup') or contains(@class, 'codicon-pass')]` - )); - break; - } catch (e) { - await sleep(1000); - } - } - } - - before(async function() { - await openProject(mavenProjectPath); - await openFile(mavenJavaFilePath); - await waitForLanguageServerReady(); - }); - - after(async function() { - for (const mavenProjectTmpFolder of mavenProjectTmpFolders) { - try { - fse.rmSync(mavenProjectTmpFolder, {force: true, recursive: true}); - } catch (e) { - // Ignore EBUSY and other cleanup errors on Windows when VS Code still holds file locks - console.warn(`Warning: failed to clean up temp folder ${mavenProjectTmpFolder}: ${e}`); - } - } - }); - - - it("Test javaProjectExplorer.focus", async function() { - await new Workbench().executeCommand("javaProjectExplorer.focus"); - // Retry finding the section since it may take time to render after the command - let section: ViewSection | undefined; - for (let i = 0; i < 5; i++) { - try { - section = await new SideBarView().getContent().getSection("Java Projects"); - break; - } catch (_e) { - await sleep(2000); - await new Workbench().executeCommand("javaProjectExplorer.focus"); - } - } - assert.ok(section, `Section "Java Projects" should be found`); - assert.ok(section!.isExpanded(), `Section "Java Projects" should be expanded`); - }); - - (platform() === "darwin" ? it.skip : it)("Test java.view.package.linkWithFolderExplorer", async function() { - await openFile(mavenJavaFilePath); - await sleep(1000); - const [, section] = await expandInJavaProjects('my-app'); - const packageNode = await section.findItem("com.mycompany.app") as TreeItem; - assert.ok(await packageNode.isExpanded(), `Package node "com.mycompany.app" should be expanded`); - const classNode = await section.findItem("App") as TreeItem; - assert.ok(await classNode.isDisplayed(), `Class node "App" should be revealed`); - await packageNode.collapse(); - }); - - (platform() === "darwin" ? it.skip : it)("Test java.view.package.unLinkWithFolderExplorer", async function() { - const [, section] = await expandInJavaProjects('my-app'); - await section.click(); - let moreActions = await section.moreActions(); - const desynchronize = await moreActions!.getItem("Unlink with Editor"); - await desynchronize!.click(); - await openFile(mavenJavaFilePath); - await sleep(1000); - const packageNode = await section.findItem("com.mycompany.app") as TreeItem; - assert.ok(!await packageNode.isExpanded(), `Package "com.mycompany.app" should not be expanded`); - moreActions = await section.moreActions(); - const link = await moreActions!.getItem("Link with Editor"); - await link!.click(); - }); - - it("Test java.view.package.newJavaClass", async function() { - let inputBox = await createJavaResource(); - const javaClassQuickPick = await inputBox.findQuickPick(0); - await javaClassQuickPick!.click(); - assert.ok(await inputBox.getPlaceHolder() === "Choose a source folder", `InputBox "Choose a source folder" should appear`); - const quickPick = await inputBox.findQuickPick("src/main/java"); - assert.ok(quickPick, `Quickpick item "src/main/java" should be found`); - await quickPick!.click(); - inputBox = await InputBox.create(); - assert.ok(await inputBox.getPlaceHolder() === "Input the class name", `InputBox "Input the class name" should appear`); - await inputBox.setText("App2"); - await inputBox.confirm(); - const editor = await waitForEditorTitle("App2.java"); - assert.ok(editor, `Editor's title should be "App2.java"`); - await editor!.save(); - assert.ok(await fse.pathExists(path.join(currentProjectPath!, "src", "main", "java", "App2.java")), `"App2.java" should be created in correct path`); - }); - - (platform() === "darwin" ? it.skip : it)("Test java.view.package.newPackage", async function() { - // The current UI test framework doesn't support mac title bar and context menus. - // See: https://github.com/redhat-developer/vscode-extension-tester#requirements - // So we dismiss some UI tests on mac. - let inputBox = await createJavaResource(); - const packageQuickPick = await inputBox.findQuickPick('Package'); - await packageQuickPick!.click(); - const quickPick = await inputBox.findQuickPick("src/main/java"); - assert.ok(quickPick, `"src/main/java" should be found in quickpick items`); - await quickPick!.click(); - inputBox = await InputBox.create(); - await inputBox.setText("com.mycompany.app2"); - await inputBox.confirm(); - assert.ok(await waitForFileExists(path.join(currentProjectPath!, "src", "main", "java", "com", "mycompany", "app2")), `New package should be created in correct path`); - }); - - (platform() === "darwin" ? it.skip : it)("Test java.view.package.revealInProjectExplorer", async function() { - // Make sure App.java is not currently revealed in Java Projects - const section = await new SideBarView().getContent().getSection("Java Projects"); - const item = await section.findItem("my-app") as TreeItem; - await item.collapse(); - const [fileSection, fileNode] = await openAppJavaSourceCode(); - await fileNode.openContextMenu(); - // menu.getItem(label) does not work. I did not investigate this further. - // This is a global selector on purpose. The context-menu is located near the root node. - const revealItem = await fileNode.findElement(By.xpath(`//div[contains(@class, 'context-view')]//a[@role='menuitem' and span[contains(text(), 'Reveal in Java Project Explorer')]]`)); - // const revealItem = await menu.getItem("Reveal in Java Project Explorer"); - assert.ok(revealItem, `Item "Reveal in Java Project Explorer" should be found in context menu`); - await revealItem!.click(); - const classNode = await section.findItem("App") as TreeItem; - assert.ok(await classNode.isDisplayed(), `Class Node "App" should be revealed`); - await fileSection.collapse(); - }); - - (platform() === "darwin" ? it.skip : it)("Test java.view.package.renameFile", async function() { - // Collapse file section to make sure that the AppToRename tree item fits in the current viewport. - // .findItem will only find tree items in the current viewport. - await collapseFileSection(); - const section = await expandMainCodeInJavaProjects(); - const classNode = await section.findItem("AppToRename") as TreeItem; - assert.ok(classNode, `AppToRename.java should be found`); - await classNode.click(); - const menu = await classNode.openContextMenu(); - const renameItem = await menu.getItem("Rename"); - assert.ok(renameItem, `"Rename" item should be found`); - await renameItem!.click(); - const inputBox = await InputBox.create(); - await inputBox.setText("AppRenamed"); - await inputBox.confirm(); - const dialog = await waitForModalDialog(); - assert.ok(dialog, `Rename confirmation dialog should appear`); - const buttons = await dialog!.getButtons(); - for (const button of buttons) { - if (await button.getText() === "OK") { - await button.click(); - break; - } - } - const editor = await waitForEditorTitle("AppRenamed.java"); - assert.ok(editor, `Editor's title should be "AppRenamed.java"`); - // Use command palette to save because the editor input area may not be - // interactable right after the rename refactoring dialog is dismissed. - await new Workbench().executeCommand('workbench.action.files.save'); - assert.ok(await section.findItem("AppRenamed"), `Item in Java Project section should be "AppRenamed"`); - }); - - (platform() === "darwin" ? it.skip : it)("Test java.view.package.moveFileToTrash", async function() { - // Collapse file section to make sure that the AppToRename tree item fits in the current viewport. - // .findItem will only find tree items in the current viewport. - await collapseFileSection(); - const section = await expandMainCodeInJavaProjects(); - const classNode = await section.findItem("AppToDelete") as TreeItem; - await classNode.click(); - const menu = await classNode.openContextMenu(); - let deleteItem = await menu.getItem("Delete"); - // Not sure why sometimes one is visible and other times the other. - if (deleteItem === undefined) { - deleteItem = await menu.getItem("Delete Permanently"); - } - assert.ok(deleteItem, `"Delete" item should be found`); - await deleteItem!.click(); - const dialog = await waitForModalDialog(); - if (dialog) { - const buttons = await dialog.getButtons(); - for (const button of buttons) { - const text = await button.getText(); - if (text === "Move to Recycle Bin" || text === "Delete") { - await button.click(); - break; - } - } - } - assert.ok(await waitForFileGone(path.join(currentProjectPath!, "src", "main", "java", "AppToDelete.java")), `The source file "AppToDelete.java" should be deleted`); - }); - - it("Test change to invisible project", async function() { - await openProject(invisibleProjectPath); - // Allow VS Code to finish the workspace transition before opening files - await sleep(3000); - // Dismiss any modal dialog (e.g. workspace trust) that may appear after opening a new project - await dismissModalDialogIfPresent(); - await openFile(invisibleJavaFilePath); - await waitForLanguageServerReady(); - const fileSections = await new SideBarView().getContent().getSections(); - await fileSections[0].collapse(); - await new Workbench().executeCommand("javaProjectExplorer.focus"); - }); - - it("Test java.project.addLibraries", async function() { - // tslint:disable-next-line:prefer-const - let [referencedItem, section] = await expandInJavaProjects('invisible', 'Referenced Libraries'); - await referencedItem.click(); - await clickActionButton(referencedItem, `Add Jar Libraries to Project Classpath...`); - const input = await InputBox.create(); - await input.setText(path.join(invisibleProjectPath, "libSource", "simple.jar")); - await input.confirm(); - await sleep(1000); - referencedItem = await section.findItem("Referenced Libraries") as TreeItem; - await referencedItem.expand(); - const simpleItem = await waitForTreeItem(section, "simple.jar") as TreeItem; - assert.ok(simpleItem, `Library "simple.jar" should be found`); - await simpleItem.click(); - await clickActionButton(simpleItem, 'Remove from Project Classpath'); - assert.ok(await waitForTreeItemGone(section, "simple.jar"), `Library "simple.jar" should not be found`); - }); - - it("Test java.project.addLibraryFolders", async function() { - // tslint:disable-next-line:prefer-const - let [referencedItem, section] = await expandInJavaProjects('invisible', 'Referenced Libraries'); - await referencedItem.click(); - const button = await getActionButton(referencedItem, `Add Jar Libraries to Project Classpath...`); - await button.getDriver().actions() - // .mouseMove(buttons[0]) - .keyDown(seleniumWebdriver.Key.ALT) - .click(button) - .keyUp(seleniumWebdriver.Key.ALT) - .perform(); - const input = await InputBox.create(); - await input.setText(path.join(invisibleProjectPath, "libSource")); - await input.confirm(); - await sleep(1000); - referencedItem = await section.findItem("Referenced Libraries") as TreeItem; - await referencedItem.expand(); - assert.ok(await waitForTreeItem(section, "simple.jar"), `Library "simple.jar" should be found`); - }); - - it("Test java.project.create", async function() { - await dismissModalDialogIfPresent(); - const projectFolder = createTmpProjectFolder("newProject"); - await fse.ensureDir(projectFolder); - await new Workbench().executeCommand("java.project.create"); - let inputBox = await InputBox.create(); - const picks = await inputBox.getQuickPicks(); - assert.equal("No build tools", await picks[0].getLabel()); - await picks[0].select(); - await sleep(1000); - inputBox = await InputBox.create(); - await inputBox.setText(projectFolder); - await inputBox.confirm(); - await sleep(1000); - inputBox = await InputBox.create(); - await inputBox.setText(newProjectName); - await inputBox.confirm(); - assert.ok(await waitForFileExists(path.join(projectFolder, newProjectName, "src", "App.java")), `The template source file should be created`); - assert.ok(await waitForFileExists(path.join(projectFolder, newProjectName, "README.md")), `The template README file should be created`); - }); - - -}); - -async function collapseFileSection() { - const fileSections = await new SideBarView().getContent().getSections(); - await fileSections[0].collapse(); -} - -async function expandMainCodeInJavaProjects() { - const section = await new SideBarView().getContent().getSection("Java Projects"); - await section.click(); - const appNode = await section.findItem("my-app") as TreeItem; - await appNode.expand(); - const srcFolderNode = await section.findItem('src/main/java') as TreeItem; - await srcFolderNode.expand(); - const packageNode = await section.findItem("com.mycompany.app") as TreeItem; - await packageNode.expand(); - return section; -} - -async function expandInJavaProjects(label: string, ...otherLabels: string[]): Promise<[TreeItem, ViewSection]> { - // Dismiss any lingering modal dialog that could block sidebar clicks - await dismissModalDialogIfPresent(); - // Collapse file section to make sure that the AppToRename tree item fits in the current viewport. - // .findItem will only find tree items in the current viewport. - await collapseFileSection(); - const section = await new SideBarView().getContent().getSection("Java Projects"); - await section.click(); - let lastNode = await section.findItem(label) as TreeItem; - await lastNode.expand(); - for (const otherLabel of otherLabels) { - lastNode = await section.findItem(otherLabel) as TreeItem; - await lastNode.expand(); - } - return [lastNode, section]; -} - -async function openAppJavaSourceCode(): Promise<[ViewSection, TreeItem]> { - const fileSections = await new SideBarView().getContent().getSections(); - await fileSections[0].expand(); - const srcNode = await fileSections[0].findItem("src") as TreeItem; - await srcNode.expand(); - const folderNode = await fileSections[0].findItem("main") as TreeItem; - await folderNode.expand(); - const subFolderNode = await fileSections[0].findItem("com") as TreeItem; - await subFolderNode.expand(); - const appFolderNode = await fileSections[0].findItem("app") as TreeItem; - await appFolderNode.expand(); - const fileNode = await fileSections[0].findItem("App.java") as TreeItem; - await fileNode.click(); - return [fileSections[0], fileNode]; -} - -async function createJavaResource() { - await collapseFileSection(); - const section = await new SideBarView().getContent().getSection("Java Projects"); - const item = await section.findItem("my-app") as TreeItem; - assert.ok(item, `Project "my-app" should be found`); - await item.click(); - await clickActionButton(item, 'New...'); - const inputBox = await InputBox.create(); - assert.ok(await inputBox.getPlaceHolder() === "Select resource type to create.", - `InputBox "Select resource type to create" should appear.`); - return inputBox; -} - -async function clickActionButton(item: TreeItem, label: string) { - const button = await getActionButton(item, label); - await button.click(); -} - -async function getActionButton(item: TreeItem, label: string) { - // Using item.getActionButton('New...') throws an error: - // tslint:disable-next-line:max-line-length - // "no such element: Unable to locate element: {\"method\":\"xpath\",\"selector\":\".//a[contains(@class, 'action-label') and @role='button' and @title='New...']\"} - // This should be filled as an issue (I haven't find one). - // The problem is the @title='New...' which should be @aria-label='New...' for vscode 1.83.1 (and probably above). - return item.findElement(By.xpath(`.//a[contains(@class, 'action-label') and @role='button' and contains(@aria-label, '${label}')]`)); -} - -async function dismissModalDialogIfPresent() { - try { - const dialog = new ModalDialog(); - const buttons = await dialog.getButtons(); - for (const button of buttons) { - const text = await button.getText(); - if (["Yes, I trust the authors", "OK", "Yes", "Continue", "I Trust the Authors"].includes(text)) { - await button.click(); - await sleep(1000); - return; - } - } - // Dismiss by clicking the first available button as a fallback - if (buttons.length > 0) { - await buttons[0].click(); - await sleep(1000); - } - } catch (_e) { - // No modal dialog present — nothing to dismiss - } -} - -async function waitForTreeItem(section: ViewSection, label: string, timeoutMs = 15000): Promise { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - const item = await section.findItem(label) as TreeItem; - if (item) { - return item; - } - await sleep(1000); - } - return undefined; -} - -async function waitForTreeItemGone(section: ViewSection, label: string, timeoutMs = 15000): Promise { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - const item = await section.findItem(label) as TreeItem; - if (!item) { - return true; - } - await sleep(1000); - } - return false; -} - -async function waitForFileExists(filePath: string, timeoutMs = 15000): Promise { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - if (await fse.pathExists(filePath)) { - return true; - } - await sleep(1000); - } - return false; -} - -async function waitForFileGone(filePath: string, timeoutMs = 15000): Promise { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - if (!await fse.pathExists(filePath)) { - return true; - } - await sleep(1000); - } - return false; -} - -async function waitForModalDialog(timeoutMs = 10000): Promise { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - try { - const dialog = new ModalDialog(); - await dialog.getButtons(); - return dialog; - } catch (_e) { - await sleep(500); - } - } - return undefined; -} - -async function waitForEditorTitle(expectedTitle: string, timeoutMs = 15000): Promise { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - try { - const editor = new TextEditor(); - if (await editor.getTitle() === expectedTitle) { - return editor; - } - } catch (_e) { - // Editor may not be ready yet - } - await sleep(1000); - } - return undefined; -} - -async function ensureExplorerIsOpen() { - const control = await new ActivityBar().getViewControl('Explorer'); - if (control === undefined) { - throw new Error(`Explorer control should not be null.`); - } - await control.openView(); -} - diff --git a/test/ui/index.ts b/test/ui/index.ts deleted file mode 100644 index e8067d24..00000000 --- a/test/ui/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -import * as path from "path"; -import { ExTester } from "vscode-extension-tester"; - -/* tslint:disable:no-console */ -async function main(): Promise { - try { - // Run UI command tests - const testPath = path.join(__dirname, "command.test.js"); - const exTester = new ExTester(); - await exTester.downloadCode(); - await exTester.installVsix(); - await exTester.installFromMarketplace("redhat.java"); - await exTester.downloadChromeDriver(); - await exTester.setupRequirements(); - process.exit(await exTester.runTests(testPath, {resources: []})); - } catch (err) { - console.log(err); - process.exit(1); - } -} - -main();