Skip to content

Commit aba24cc

Browse files
authored
feat: improve openResources to allow dynamic waits when workbench is loaded (#1917)
1 parent ccb8365 commit aba24cc

File tree

5 files changed

+85
-23
lines changed

5 files changed

+85
-23
lines changed

docs/Home.md

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,34 @@ Find documentation about the Page Object APIs: [[Page-Object-APIs]].
3838

3939
## Opening Files and Folders
4040

41-
Opening files and folders is basic functionality of VS Code.
41+
Opening files and folders is a fundamental part of automating interactions in VS Code. Since version **4.2.0**, the recommended approach is to use the `openResources` method provided by the `VSBrowser` API.
4242

43-
Since 4.2.0, the most convenient way to open files and folders is the `openResources` method in `VSBrowser`. Use it to open potentially multiple folders or files at the same time (method also waits for workbench to refresh). Files are opened in the editor, single folder will be opened directly in the explorer, multiple folders will be opened as a multi-workspace.
43+
### `openResources(...args: (string | (() => void | Promise<any>))[]): Promise<void>`
4444

45-
It is recommended to use absolute paths, relative paths are based on process' cwd.
45+
This method allows you to open one or more files and folders, **and optionally wait for additional conditions** once the workbench is loaded. It automatically waits for the workbench to be ready after opening resources.
4646

47-
```typescript
48-
await VSBrowser.instance.openResources(`${path/to/folder1}`, ${path/to/file1}, ${path/to/folder2})
47+
- **Single folder**: Opens the folder in the explorer.
48+
- **Multiple folders**: Opens a multi-root workspace.
49+
- **Files**: Opens each file in a new editor tab.
50+
- **Wait function** _(optional)_: Can be passed as the last argument (sync or async) and will be executed after the workbench is ready.
51+
52+
> **Tip:** Use **absolute paths** to avoid issues. Relative paths are resolved based on the current working directory (`process.cwd()`).
53+
54+
### Example
55+
56+
```ts
57+
import * as path from "path";
58+
import { VSBrowser } from "vscode-extension-tester";
59+
60+
await VSBrowser.instance.openResources(
61+
path.resolve(__dirname, "workspace/folder1"),
62+
path.resolve(__dirname, "workspace/file1.ts"),
63+
path.resolve(__dirname, "workspace/folder2"),
64+
async () => {
65+
// Optional: Wait for your custom UI element or state
66+
await new Promise((res) => setTimeout(res, 3000));
67+
},
68+
);
4969
```
5070

5171
### Using Dialogs

packages/extester/src/browser.ts

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -148,19 +148,43 @@ export class VSBrowser {
148148
}
149149

150150
/**
151-
* Waits until parts of the workbench are loaded
151+
* Waits for the VS Code workbench UI to be fully loaded and optionally performs
152+
* an additional async or sync check after the workbench appears.
153+
*
154+
* This method waits for the presence of the `.monaco-workbench` element within the specified timeout.
155+
* If a WebDriver error occurs (e.g. flaky startup), it retries after a short delay.
156+
* Additionally, a follow-up function (`waitForFn`) can be passed to perform custom
157+
* readiness checks (e.g. for UI elements, extensions, or custom content).
158+
*
159+
* @param timeout - Maximum time in milliseconds to wait for the workbench to appear (default: 30,000 ms).
160+
* @param waitForFn - Optional function (sync or async) to be executed after the workbench is located.
161+
*
162+
* @throws If the workbench is not found in time and no recoverable WebDriver error occurred.
163+
*
164+
* @example
165+
* // Wait for the workbench with default timeout
166+
* await waitForWorkbench();
167+
*
168+
* @example
169+
* // Wait for the workbench and ensure a custom UI element is present
170+
* await waitForWorkbench(10000, async () => {
171+
* await driver.wait(until.elementLocated(By.id('my-element')), 5000);
172+
* });
152173
*/
153-
async waitForWorkbench(timeout = 30000): Promise<void> {
174+
async waitForWorkbench(timeout: number = 30_000, waitForFn?: () => void | Promise<any>): Promise<void> {
154175
// Workaround/patch for https://github.com/redhat-developer/vscode-extension-tester/issues/466
155176
try {
156177
await this._driver.wait(until.elementLocated(By.className('monaco-workbench')), timeout, `Workbench was not loaded properly after ${timeout} ms.`);
157178
} catch (err) {
158179
if ((err as Error).name === 'WebDriverError') {
159-
await new Promise((res) => setTimeout(res, 3000));
180+
await new Promise((res) => setTimeout(res, 3_000));
160181
} else {
161182
throw err;
162183
}
163184
}
185+
if (waitForFn) {
186+
await waitForFn();
187+
}
164188
}
165189

166190
/**
@@ -199,19 +223,35 @@ export class VSBrowser {
199223
}
200224

201225
/**
202-
* Open folder(s) or file(s) in the current instance of vscode.
226+
* Opens one or more resources in the editor and optionally performs a follow-up action.
227+
*
228+
* This method accepts a variable number of arguments. All string arguments are interpreted
229+
* as resource paths to be opened. Optionally, a single callback function (synchronous or asynchronous)
230+
* can be provided as the last argument. This callback will be invoked after all resources have been opened.
231+
*
232+
* @param args - A list of file paths to open followed optionally by a callback function.
233+
* The callback can be either synchronous or asynchronous.
203234
*
204-
* @param paths path(s) of folder(s)/files(s) to open as varargs
205-
* @returns Promise resolving when all selected resources are opened and the workbench reloads
235+
* @example
236+
* // Open two files
237+
* await openResources('file1.ts', 'file2.ts');
238+
*
239+
* @example
240+
* // Open one file and then wait for a condition
241+
* await openResources('file1.ts', async () => {
242+
* await waitForElementToLoad();
243+
* });
206244
*/
207-
async openResources(...paths: string[]): Promise<void> {
245+
async openResources(...args: (string | (() => void | Promise<any>))[]): Promise<void> {
246+
const paths = args.filter((arg) => typeof arg === 'string');
247+
const waitForFn = args.find((arg) => typeof arg === 'function') as (() => void | Promise<any>) | undefined;
248+
208249
if (paths.length === 0) {
209250
return;
210251
}
211252

212253
const code = new CodeUtil(this.storagePath, this.releaseType, this.extensionsFolder);
213254
code.open(...paths);
214-
await new Promise((res) => setTimeout(res, 3000));
215-
await this.waitForWorkbench();
255+
await this.waitForWorkbench(undefined, waitForFn);
216256
}
217257
}

packages/extester/src/suite/runner.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,6 @@ export class VSRunner {
9494
await browser.start(binPath);
9595
await browser.openResources(...resources);
9696
await browser.waitForWorkbench();
97-
await new Promise((res) => {
98-
setTimeout(res, 3000);
99-
});
10097
console.log(`Browser ready in ${Date.now() - start} ms`);
10198
console.log('Launching tests...');
10299
});

packages/page-objects/src/components/sidebar/scm/NewScmView.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,7 @@ export class SingleScmProvider extends ScmProvider {
6868
const buttons: TitleActionButton[] = [];
6969

7070
if (satisfies(ScmProvider.versionInfo.version, '>=1.93.0')) {
71-
const header = await view.findElement(ScmView.locators.ScmView.sourceControlSection);
72-
actions = await header.findElements(ScmProvider.locators.ScmView.action);
71+
actions = await this.getProviderHeaderActions(view);
7372
names = await Promise.all(actions.map(async (action) => await action.getAttribute(ScmProvider.locators.ScmView.actionLabel)));
7473
} else {
7574
const titlePart = view.getTitlePart();
@@ -84,6 +83,7 @@ export class SingleScmProvider extends ScmProvider {
8483
const index = names.findIndex((item) => item === title);
8584
if (index > -1) {
8685
if (satisfies(ScmProvider.versionInfo.version, '>=1.93.0')) {
86+
actions = await this.getProviderHeaderActions(view);
8787
await actions[index].click();
8888
} else {
8989
await buttons[index].click();
@@ -93,6 +93,13 @@ export class SingleScmProvider extends ScmProvider {
9393
return false;
9494
}
9595

96+
private async getProviderHeaderActions(view: NewScmView): Promise<WebElement[]> {
97+
const header = await view.findElement(ScmView.locators.ScmView.sourceControlSection);
98+
await this.getDriver().actions().move({ origin: header }).perform();
99+
await this.getDriver().sleep(1_000);
100+
return await header.findElements(ScmProvider.locators.ScmView.action);
101+
}
102+
96103
async openMoreActions(): Promise<ContextMenu> {
97104
const view = this.enclosingItem as NewScmView;
98105
return await new MoreAction(view).openContextMenu();

tests/test-project/src/test/xsideBar/scmView.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@ import { satisfies } from 'compare-versions';
3030
await VSBrowser.instance.openResources(path.resolve('..', '..'));
3131
await VSBrowser.instance.waitForWorkbench();
3232
view = (await ((await new ActivityBar().getViewControl('Source Control')) as ViewControl).openView()) as ScmView;
33-
await new Promise((res) => {
34-
setTimeout(res, 2000);
35-
});
33+
await view.getDriver().sleep(5_000); // wait until scm changes are loaded
3634
});
3735

3836
after(() => {
@@ -116,7 +114,7 @@ import { satisfies } from 'compare-versions';
116114
expect(label).has.string('testfile');
117115
});
118116

119-
it('getDescritption works', async () => {
117+
it('getDescription works', async () => {
120118
const desc = await change.getDescription();
121119
expect(desc).has.string('');
122120
});

0 commit comments

Comments
 (0)