Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ci: DH-18428: vscode-extension-tester e2e tests #223

Merged
merged 71 commits into from
Mar 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
d01421f
Initial config of e2e tests using vscode-extension-tester (DH-18428-2)
bmingles Feb 28, 2025
2e6514a
Removed some debugging stuff. Should be minimal working Docker config…
bmingles Feb 28, 2025
5f4d0d4
Start dh server with e2 tests (DH-18428-2)
bmingles Mar 3, 2025
12c434b
Verifying os is what I think it is (DH-18428-2)
bmingles Mar 3, 2025
60ebb17
Fixed paths (DH-18428-2)
bmingles Mar 3, 2025
dcfa63e
Removed console log (DH-18428-2)
bmingles Mar 3, 2025
8c86a39
webview switching (DH-18428-2)
bmingles Mar 5, 2025
2759072
Cleanup (DH-18428-2)
bmingles Mar 5, 2025
3a94209
Cleanup (DH-18428-2)
bmingles Mar 5, 2025
40d2afd
Panel lazy load tests (DH-18428-2)
bmingles Mar 5, 2025
32ccaf9
Change first test to not select tab (DH-18428-2)
bmingles Mar 6, 2025
ff9c320
Split out page object classes (DH-18428-2)
bmingles Mar 6, 2025
ff54200
Removed custom codelens util (DH-18428-2)
bmingles Mar 6, 2025
229ff65
Added step blocks (DH-18428-2)
bmingles Mar 6, 2025
512cd45
restored custom codelens function (DH-18428-2)
bmingles Mar 6, 2025
d2fdb72
Checking code lens availability (DH-18428-2)
bmingles Mar 6, 2025
04d4021
Simplifying codelens waiting (DH-18428-2)
bmingles Mar 6, 2025
e86ab2d
Removed console log (DH-18428-2)
bmingles Mar 6, 2025
aee28f2
Maded tests more declarative in getting panel state (DH-18428-2)
bmingles Mar 6, 2025
f9b38d8
Cleanup (DH-18428-2)
bmingles Mar 6, 2025
b5dd7a9
Improved iframe switching (DH-18428-2)
bmingles Mar 7, 2025
853519a
Cleanup (DH-18428-2)
bmingles Mar 7, 2025
af08ec2
Fixed build (DH-18428-2)
bmingles Mar 7, 2025
b8fbff9
Error handling (DH-18428-2)
bmingles Mar 7, 2025
d3f9924
Additional test (DH-18428-2)
bmingles Mar 7, 2025
a4303a1
Removed unnecessary docker config (DH-18428-2)
bmingles Mar 7, 2025
342868e
Locator cleanup (DH-18428-2)
bmingles Mar 7, 2025
bab5154
Re-enabling GH action steps (DH-18428-2)
bmingles Mar 10, 2025
e5cd5b3
Cleaned up file locations (DH-18428-2)
bmingles Mar 10, 2025
57ee34e
Mocha ctrf report (DH-18428-2)
bmingles Mar 10, 2025
450b7a3
trying summary (DH-18428-2)
bmingles Mar 10, 2025
9f43b7c
Include failed tests report (DH-18428-2)
bmingles Mar 10, 2025
711423f
Status bar item tests (DH-18428-2)
bmingles Mar 10, 2025
48670d2
Docs / comments (DH-18428-2)
bmingles Mar 10, 2025
6b35465
Cleanup (DH-18428-2)
bmingles Mar 10, 2025
2d81971
Rename variable (DH-18428-2)
bmingles Mar 10, 2025
5dc8eff
Comments (DH-18428-2)
bmingles Mar 10, 2025
c8c1457
Cleanup (DH-18428-2)
bmingles Mar 10, 2025
347194c
Removed unused codepath (DH-18428-2)
bmingles Mar 10, 2025
33eefc2
Removed unused method (DH-18428-2)
bmingles Mar 10, 2025
8fa7091
Test lifecycle + consts (DH-18428-2)
bmingles Mar 10, 2025
1500d5c
Consolidated steps (DH-18428-2)
bmingles Mar 10, 2025
8397b6f
Split out runDhFileCodeLens (DH-18428-2)
bmingles Mar 10, 2025
3e740e0
Consolidated lifecycle hooks (DH-18428-2)
bmingles Mar 10, 2025
e7b1e90
Removed unused interface (DH-18428-2)
bmingles Mar 10, 2025
b69fa35
Removed unused utils (DH-18428-2)
bmingles Mar 10, 2025
6f80478
Removed unused import (DH-18428-2)
bmingles Mar 10, 2025
567fb8d
Split out types (DH-18428-2)
bmingles Mar 10, 2025
855b605
Fixed issue in setup (DH-18428-2)
bmingles Mar 10, 2025
a2f2066
Fixed typo (DH-18428-2)
bmingles Mar 10, 2025
9cf7bfc
Added missing setup (DH-18428-2)
bmingles Mar 10, 2025
a53809c
Debugging (DH-18428-2)
bmingles Mar 10, 2025
b96ddff
Trying sleep (DH-18428-2)
bmingles Mar 10, 2025
8147d2d
attempt to fix CI (DH-18428-2)
bmingles Mar 10, 2025
806bf2f
comment (DH-18428-2)
bmingles Mar 10, 2025
6f5b137
Debug file opening (DH-18428-2)
bmingles Mar 10, 2025
568e811
screenshots (DH-18428-2)
bmingles Mar 10, 2025
3225026
screenshots (DH-18428-2)
bmingles Mar 10, 2025
cbfa4ac
Trying focusing input (DH-18428-2)
bmingles Mar 10, 2025
63a225d
Checking if linux works now (DH-18428-2)
bmingles Mar 11, 2025
f42a20a
Trying pinning to 1.97.2 (DH-18428-2)
bmingles Mar 11, 2025
1c5ba01
Attempt fixing 1.98.x by setting "window.titleBarStyle": "native" (DH…
bmingles Mar 11, 2025
6e3675c
Debugging again (DH-18428-2)
bmingles Mar 11, 2025
50306f6
Attempt sleep (DH-18428-2)
bmingles Mar 11, 2025
93cbf57
Try clearing file input (DH-18428-2)
bmingles Mar 11, 2025
9f93368
Trying Windows run (DH-18428-2)
bmingles Mar 11, 2025
537bc94
Trying quick open (DH-18428-2)
bmingles Mar 11, 2025
3f20c5a
Removed Windows config (DH-18428-2)
bmingles Mar 11, 2025
4eaa803
Removed screenshot (DH-18428-2)
bmingles Mar 11, 2025
d451ed4
Added issue comment link (DH-18428-2)
bmingles Mar 11, 2025
bb921f0
Apply suggestions from code review
bmingles Mar 12, 2025
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
20 changes: 20 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Hidden file / directories
**/.*/

# Add back specific hidden files
!.eslintrc.json
!.nvmrc
!.vscodeignore
!e2e-testing/.mocharc*.js

vitest.config.mts

*.tsbuildinfo
*.vsix

**/dist
**/node_modules/
**/out
docs
releases
test-reports
9 changes: 8 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"project": [
"./tsconfig.json",
"./e2e/tsconfig.json",
"./e2e-testing/tsconfig.json",
"./tsconfig.unit.json",
"./packages/*/tsconfig.json"
]
Expand All @@ -44,5 +45,11 @@
}
}
],
"ignorePatterns": ["out", "dist", "**/*.d.ts"]
"ignorePatterns": [
"out",
"dist",
"**/*.d.ts",
".resources",
".test-extensions"
]
}
8 changes: 7 additions & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,16 @@ jobs:
- run: npm ci
- name: Run end-to-end tests
run: xvfb-run npm run test:e2e
- name: Upload Screenshots as Artifact
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: e2e-screenshots
path: e2e-testing/.resources/screenshots
retention-days: 1
- name: Publish Test Summary Results
if: ${{ always() }}
run: |
npm run report:ctrfmerge
npm run report:ctrfsummary
sed -i 's/<h3>Test Summary<\/h3>/<h3>End-to-end Test Summary<\/h3>/' $GITHUB_STEP_SUMMARY
npm run report:prcomment
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
out
dist
node_modules
/*.vsix
.vscode-test/
.DS_Store
.wdio-vscode-service
e2e/reports/
e2e-testing/.resources
e2e-testing/.test-extensions
test-reports/
tsconfig.tsbuildinfo
10 changes: 10 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "${defaultBuildTask}"
},
{
"name": "e2e Tests",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/e2e-testing/out/runner.mjs",
"args": ["--debug"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"preLaunchTask": "${defaultBuildTask}"
}
]
}
23 changes: 5 additions & 18 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ npm run test

### End-to-end Testing

End-to-end tests are configured to run via `wdio-vscode-service`. This allows
End-to-end tests are configured to run via `vscode-extension-tester. This allows
testing workflows and has better abstractions for ui testing than `@vscode/test-electron`.

See [wdio-vscode-service](https://www.npmjs.com/package/wdio-vscode-service) for more details.
See [vscode-extension-tester](https://github.com/redhat-developer/vscode-extension-tester) for more details.

To run end-to-end tests:

Expand All @@ -26,23 +26,10 @@ npm run test:e2e
To run using `VS Code` debugger:

1. Set a breakpoint in a test
2. Either
2. Run the `e2e Tests` launch config in VS Code
3. You should see the tests start and `VS Code` stop at the breakpoint

Enable auto attach with flag (`Command palette -> Debug: Toggle Auto Attach -> Only With Flag`).

> Note you may need to open a new terminal for this to take effect.

or

Open `Javascript Debug Terminal` (Click the dropdown arrow beside the + button for adding a new terminal)

3. Run the script:

```sh
npm run test:e2e
```

4. You should see the tests start and `VS Code` stop at the breakpoint.
The `vscode-extension-tester` library uses `Mocha` to run tests. If you need to tweak debugging settings such as test timeout, you can do so in [`e2e-testing/src/mocharcDebug.ts`](./e2e-testing/src/mocharcDebug.ts).

## VSCE
[vsce](https://github.com/microsoft/vscode-vsce), short for "Visual Studio Code Extensions", is a command-line tool for packaging, publishing and managing `VS Code` extensions. The Deephaven extension calls `vsce` via npm scripts. Note that `vsce package` and `vsce publish` both call the `vscode:prepublish` script.
Expand Down
9 changes: 9 additions & 0 deletions e2e-testing/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
services:
dhc-server:
container_name: dhc-server
image: ghcr.io/deephaven/server:${DHC_VERSION:-edge}
pull_policy: always
ports:
- 10000:10000
environment:
- START_OPTS=-Xmx4g -DAuthHandlers=io.deephaven.auth.AnonymousAuthenticationHandler -Ddeephaven.console.type=python
11 changes: 11 additions & 0 deletions e2e-testing/src/mocharc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { MochaOptions } from 'vscode-extension-tester';

const options: MochaOptions = {
timeout: 30000,
reporter: 'mocha-ctrf-json-reporter',
reporterOptions: {
outputDir: 'test-reports',
},
};

module.exports = options;
13 changes: 13 additions & 0 deletions e2e-testing/src/mocharcDebug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MochaOptions } from 'vscode-extension-tester';

import * as baseOptions from './mocharc';

const options: MochaOptions = {
...baseOptions,
// When debugging tests in VS Code, we want to be able to set breakpoints and
// inspect DOM, so we increase Mocha tests timeout. An arbitrary 3 minutes
// seems sufficient for most cases but can be tweaked as necessary.
timeout: 60000 * 3,
};

module.exports = options;
200 changes: 200 additions & 0 deletions e2e-testing/src/pageObjects/EditorViewExtended.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import {
By,
EditorView,
TextEditor,
VSBrowser,
type EditorGroup,
type WebElement,
} from 'vscode-extension-tester';
import {
extractErrorType,
locators,
switchToFrame,
type EditorGroupData,
type TabData,
type WebViewData,
} from '../util';
import { WebViewExtended } from './WebViewExtended';

export class EditorViewExtended extends EditorView {
/**
* Get a serializable data representation for all editor groups, their tabs,
* and associated webviews.
* @returns Promise resolving to an array of EditorGroupData objects
*/
async getEditorGroupsData(): Promise<EditorGroupData[]> {
const groupDataList: EditorGroupData[] = [];
let groupIndex = 0;
for (const group of await this.getEditorGroups()) {
const tabs: TabData[] = [];

// An editor group may be associated with 0-n webviews
let webViewCount = 0;

for (const tab of await group.getOpenTabs()) {
const title = await tab.getTitle();
const isSelected = await tab.isSelected();
const resourceName = await tab.getAttribute('data-resource-name');
const isWebView = resourceName.startsWith('webview-');

if (isWebView) {
webViewCount += 1;
}

tabs.push({
title,
isSelected,
isWebView,
});
}

let webViews: WebViewData[] | undefined;

if (webViewCount > 0) {
const driver = this.getDriver();
webViews = [];

const webView = await new WebViewExtended(group).wait();
const parentFlowToElementId = await webView.getParentFlowToElementId();

// Each webview consists of a container div containing an iframe. The
// container divs all get appended as siblings near the end of the
// document body and are associated with an editor instance via
// `parentFlowToElementId`. Only one of the containers will be visible
// for each editor group, corresponding to the selected tab / active
// editor instance within the group.
const iframeContainers = await driver.findElements(
locators.webViewContainer(parentFlowToElementId)
);

for (const container of iframeContainers) {
const style = await container.getAttribute('style');
const isVisible = style.includes('visibility: visible');

// Grab current context so we can switch back to it
const windowHandle = await driver.getWindowHandle();

const iframeAccessor = async (): Promise<WebElement> =>
container.findElement(By.xpath('.//iframe'));

let hasContent = false;
try {
// Attempt to switch to nested content frame to see if content has
// been loaded. Nested iframes won't exist if not, and this will fail.
// WebView.switchToFrame() only switches to the webview container
// that is currently visible. We want to inspect all of them, even
// the hidden ones, so we use the `switchToFrame` utility function
// instead.
await switchToFrame(
[
iframeAccessor,
WebViewExtended.activeFrameSelector,
WebViewExtended.contentFrameSelector,
],
// If tab is visible, we could possibly be loading for the first time,
// so wait a little longer. If tab is invisible, the only scenario
// where content would be there is if it has already loaded, so we
// don't need to wait as long
isVisible ? 3000 : 250
);
hasContent = true;
} catch (err) {
// We expect TimeoutErrors in standard case where there is no loaded
// content. Other errors indicate something unexpected.
if (extractErrorType(err) !== 'TimeoutError') {
// eslint-disable-next-line no-console
console.log('Err:', err);
}
} finally {
// Reset context
await driver.switchTo().window(windowHandle);
}

const webViewData: WebViewData = {};

// Only set isVisible & hasContent if true to make test comparisons
// easier to write / read
if (isVisible) {
webViewData.isVisible = true;
}
if (hasContent) {
webViewData.hasContent = true;
}
webViews.push(webViewData);
}
}

const groupData: EditorGroupData = {
groupIndex: groupIndex++,
tabs,
};

if (webViews) {
groupData.webViews = webViews;
}

groupDataList.push(groupData);
}

return groupDataList;
}

/**
* Switch to a TextEditor tab with the given title + optional group index. Throws
* if a matching Editor is found that isn't an instance of a TextEditor.
* @param title title of the tab
* @param groupIndex zero based index for the editor group (0 for the left most group)
* @returns Promise resolving to TextEditor object
*/
async openTextEditor(
title: string,
groupIndex?: number
): Promise<TextEditor> {
const editor = await this.openEditor(title, groupIndex);

if (!(editor instanceof TextEditor)) {
throw new Error('Editor is not a text editor');
}

return editor;
}

/**
* Switch to a TextEditor tab with the given title + optional group index.
* Throws if a matching Editor is found that isn't associated with a WebView.
* @param groupIndex zero based index for the editor group
* @param title title of the tab
* @returns Promise resolving to `WebViewExtended` object
*/
async openWebView(
title: string,
groupIndex?: number
): Promise<WebViewExtended> {
const group = await this.getEditorGroup(groupIndex ?? 0);

const tab = await group.getTabByTitle(title);
const resourceName = await tab.getAttribute('data-resource-name');
const isWebView = resourceName.startsWith('webview-');

if (!isWebView) {
throw new Error('Tab is not associated with a WebView');
}

await tab.select();

return new WebViewExtended(group).wait();
}

/**
* Wait for an editor group to be available
* @param groupIndex zero based index for the editor group
* @returns Promise resolving to EditorGroup object
*/
async waitForEditorGroup(groupIndex: number): Promise<EditorGroup> {
await VSBrowser.instance.driver.wait(
async () => (await this.getEditorGroups()).length > groupIndex
);

return this.getEditorGroup(groupIndex);
}
}
Loading
Loading