Skip to content

Commit 5d527df

Browse files
authored
test: DH-20440: Support for worker creation UI in e2e tests (#308)
DH-20440: Support for worker creation UI in e2e tests (aka. gplus servers) ### Testing Install dependencies `npm install` Run tests against a gplus server (I've been using dev-gplus due to current flakiness of qa gplus) `npm run test:e2e -- --coreplus https://dev-gplus.int.illumon.com:8123/` All tests should pass. There are occasions where there are flaky tests. Usually trying again will succeed.
1 parent 348e4cf commit 5d527df

5 files changed

Lines changed: 154 additions & 17 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ out
22
dist
33
node_modules
44
/*.vsix
5+
.claude
56
.vscode-test/
67
.DS_Store
78
e2e-testing/.resources

e2e-testing/src/specs/panels.spec.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
connectToServer,
44
executeCommandWithRetry,
55
expectDeepEqualArray,
6+
getIsCreateQueryIframeSupported,
67
runDhFileCodeLens,
78
setup,
89
SIMPLE_TICKING3_PY,
@@ -49,9 +50,17 @@ const expectedTabs = {
4950
};
5051

5152
describe('Panels Tests', () => {
52-
before(async () => {
53+
before(async function () {
54+
const isCreateQueryIframeSupported = await getIsCreateQueryIframeSupported(
55+
process.env.DH_SERVER_URL
56+
);
57+
58+
if (isCreateQueryIframeSupported) {
59+
this.timeout(120_000);
60+
}
61+
5362
await setup(SIMPLE_TICKING3_PY.path);
54-
await connectToServer();
63+
await connectToServer(isCreateQueryIframeSupported);
5564
await executeCommandWithRetry('View: Split Editor Down');
5665
});
5766

e2e-testing/src/specs/statusBar.spec.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { InputBox } from 'vscode-extension-tester';
22
import {
33
getDhStatusBarItem,
4+
getIsCreateQueryIframeSupported,
45
getServerItems,
56
getSidebarViewItem,
67
setup,
@@ -18,8 +19,13 @@ import { VIEW_NAME } from '../util/constants';
1819

1920
describe('Status Bar Tests', () => {
2021
let editorView: EditorViewExtended;
22+
let isCreateQueryIframeSupported: boolean;
23+
24+
before(async function () {
25+
isCreateQueryIframeSupported = await getIsCreateQueryIframeSupported(
26+
process.env.DH_SERVER_URL
27+
);
2128

22-
before(async () => {
2329
await setup(
2430
SIMPLE_TICKING_MD.path,
2531
SIMPLE_TICKING3_PY.path,
@@ -57,7 +63,10 @@ describe('Status Bar Tests', () => {
5763
}
5864
});
5965

60-
it('should connect to server on click', async () => {
66+
it('should connect to server on click', async function () {
67+
if (isCreateQueryIframeSupported) {
68+
this.timeout(120_000);
69+
}
6170
await editorView.openTextEditor(SIMPLE_TICKING3_PY.name);
6271

6372
await step(1, 'Click Deephaven status bar item', async stepLabel => {
@@ -70,7 +79,7 @@ describe('Status Bar Tests', () => {
7079
const input = await InputBox.create();
7180
await input.selectQuickPick(0);
7281

73-
await waitForServerConnection();
82+
await waitForServerConnection(isCreateQueryIframeSupported);
7483
});
7584

7685
await step(3, 'Verify server node', async stepLabel => {

e2e-testing/src/util/testUtils.ts

Lines changed: 128 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable no-console */
12
import {
23
ActivityBar,
34
By,
@@ -45,9 +46,12 @@ export async function closeActivityBarView(name: string): Promise<void> {
4546
* this more flexible in the future. Also, there needs to be an active editor
4647
* for the command to work, so ensure a file is opened before calling this
4748
* function.
49+
* @param isCreateQueryIframeSupported Whether the server requires the "Create
50+
* Connection" webview form. Pass the result of `isCreateQueryIframeServer()`.
4851
*/
49-
export async function connectToServer(): Promise<void> {
50-
// eslint-disable-next-line no-console
52+
export async function connectToServer(
53+
isCreateQueryIframeSupported = false
54+
): Promise<void> {
5155
console.log('Connecting to Deephaven server...');
5256

5357
await executeCommandWithRetry('Deephaven: Select Connection');
@@ -58,7 +62,7 @@ export async function connectToServer(): Promise<void> {
5862
const input = await InputBox.create();
5963
await input.selectQuickPick(0);
6064

61-
await waitForServerConnection();
65+
await waitForServerConnection(isCreateQueryIframeSupported);
6266
}
6367

6468
/**
@@ -169,15 +173,13 @@ export async function executeWithRetry<T>(
169173
);
170174

171175
if (!isRetryableError) {
172-
// eslint-disable-next-line no-console
173176
console.error('Non-retryable error encountered:', error);
174177
throw error;
175178
}
176179

177180
const hasRetryAttemptsRemaining = retryAttempt < maxRetries;
178181

179182
if (!hasRetryAttemptsRemaining) {
180-
// eslint-disable-next-line no-console
181183
console.error('Max retry attempts reached. Last error:', error);
182184
throw error;
183185
}
@@ -379,7 +381,6 @@ export async function openFileResources(
379381
return;
380382
}
381383

382-
// eslint-disable-next-line no-console
383384
console.log('Opening filePaths:', filePaths);
384385

385386
// In CI environment, openResources doesn't work on Linux. Using the quick open
@@ -399,8 +400,10 @@ export async function openFileResources(
399400
* @param editor Editor to execute code lens in
400401
*/
401402
export async function runDhFileCodeLens(editor: TextEditor): Promise<void> {
402-
const runDhFileCodeLens = await getCodeLens(editor, 'Run Deephaven File');
403-
await runDhFileCodeLens.click();
403+
await executeWithRetry(async () => {
404+
const runDhFileCodeLens = await getCodeLens(editor, 'Run Deephaven File');
405+
await runDhFileCodeLens.click();
406+
});
404407
}
405408

406409
/**
@@ -422,7 +425,6 @@ export async function step<TResult>(
422425
fn: (stepLabel: string) => Promise<TResult>
423426
): Promise<TResult> {
424427
const stepLabel = `Step ${n}: ${label}`;
425-
// eslint-disable-next-line no-console
426428
console.log(stepLabel);
427429
return fn(stepLabel);
428430
}
@@ -485,7 +487,6 @@ export async function switchToFrame(
485487
throw err;
486488
}
487489

488-
// eslint-disable-next-line no-console
489490
console.log(`Retrying after '${errorType}' error`);
490491

491492
// Try retrieving the iframe and switching again
@@ -497,21 +498,131 @@ export async function switchToFrame(
497498
try {
498499
await driver.switchTo().frame(iframe);
499500
} catch (err) {
500-
// eslint-disable-next-line no-console
501501
console.log(`Failed to switch to frame: ${iframeOrIdentifier}`, err);
502502
throw err;
503503
}
504504
}
505505
}
506506
}
507507

508+
/**
509+
* Check if the given server URL requires the "Create Connection" webview form
510+
* to create a worker. Mirrors the getDheFeatures() check in src/dh/dhe.ts using
511+
* the same endpoint and response validation.
512+
*/
513+
export async function getIsCreateQueryIframeSupported(
514+
serverUrl: string | undefined
515+
): Promise<boolean> {
516+
if (serverUrl == null) {
517+
return false;
518+
}
519+
520+
try {
521+
const res = await fetch(new URL('/iriside/features.json', serverUrl));
522+
if (!res.ok || res.headers.get('content-type') !== 'application/json') {
523+
console.log(
524+
`[getIsCreateQueryIframe] Unsupported. Status: ${res.status}, Content-Type: ${res.headers.get('content-type')}`
525+
);
526+
return false;
527+
}
528+
const json = await res.json();
529+
return json?.features?.createQueryIframe === true;
530+
} catch (err) {
531+
console.error(`[getIsCreateQueryIframe] Error at ${serverUrl}:`, err);
532+
return false;
533+
}
534+
}
535+
536+
/**
537+
* Fill in and submit the "Create Connection" webview form that appears for
538+
* servers with the `createQueryIframe` feature. Sets Heap Size to 0.5 GB and
539+
* Language to Python, then clicks Connect.
540+
*/
541+
async function handleCreateQueryForm(): Promise<void> {
542+
const { driver } = VSBrowser.instance;
543+
544+
// The Create Connection panel is a VS Code WebviewView rendered as an iframe
545+
// with src containing "purpose=webviewView".
546+
// Navigate: outer sidebar iframe → #active-frame → #content-iframe.
547+
await switchToFrame([
548+
'iframe[src*="purpose=webviewView"]',
549+
'#active-frame',
550+
'#content-iframe',
551+
]);
552+
553+
console.log('[handleCreateQueryForm] Waiting for form to load...');
554+
// Wait for the form to finish loading (blank panel / spinner may appear first)
555+
const heapInput = await driver.wait<WebElement>(async () => {
556+
const [el] = await driver.findElements(
557+
By.css('.form-control.inputHeapSize')
558+
);
559+
return el;
560+
});
561+
562+
console.log('[handleCreateQueryForm] Form loaded. Setting heap size...');
563+
await heapInput.clear();
564+
await heapInput.sendKeys('0.5');
565+
566+
const langSelect = await driver.findElement(
567+
By.css('.custom-select.inputLanguage')
568+
);
569+
for (const option of await langSelect.findElements(By.css('option'))) {
570+
if ((await option.getText()) === 'Python') {
571+
await option.click();
572+
break;
573+
}
574+
}
575+
576+
console.log('[handleCreateQueryForm] Language set. Clicking Connect...');
577+
const connectBtn = await driver.findElement(
578+
By.css('button[type="submit"].btn-primary')
579+
);
580+
await connectBtn.click();
581+
582+
console.log(
583+
'[handleCreateQueryForm] Connect clicked. Waiting for panel to finish...'
584+
);
585+
586+
console.log('[handleCreateQueryForm] Returning to default content.');
587+
await driver.switchTo().defaultContent();
588+
589+
// Wait for the outer sidebar iframe to become hidden. VS Code keeps the iframe
590+
// in the DOM when a WebviewView panel closes but hides it via CSS — so check
591+
// isDisplayed() rather than DOM presence. The panel hides once the worker is
592+
// fully created, which is the tightest signal before the notification fires.
593+
console.log('[handleCreateQueryForm] Waiting for panel to close...');
594+
try {
595+
await driver.wait(async () => {
596+
const [frame] = await driver.findElements(
597+
By.css('iframe[src*="purpose=webviewView"]')
598+
);
599+
600+
try {
601+
return !(await frame.isDisplayed());
602+
} catch {
603+
return true; // stale
604+
}
605+
});
606+
} catch (err) {
607+
throw new Error(
608+
`[handleCreateQueryForm] TIMEOUT waiting for panel to close: ${err}`
609+
);
610+
}
611+
612+
console.log('[handleCreateQueryForm] Panel closed.');
613+
}
614+
508615
/**
509616
* Wait for Deephaven server connection to complete, handling username/password
510617
* input if prompted. Also handles authentication method selection if the server
511618
* supports both SAML and Basic authentication. Closes the "Created Deephaven
512619
* session" notification when connection is established.
620+
* @param isCreateQueryIframeSupported Whether the server requires the "Create
621+
* Connection" webview form
513622
*/
514-
export async function waitForServerConnection(): Promise<void> {
623+
export async function waitForServerConnection(
624+
isCreateQueryIframeSupported: boolean
625+
): Promise<void> {
515626
let firstInputBox: InputBox | null = null;
516627
try {
517628
firstInputBox = await InputBox.create();
@@ -561,6 +672,11 @@ export async function waitForServerConnection(): Promise<void> {
561672
await passwordInputBox.confirm();
562673
}
563674

675+
// Handle "Create Connection" form for servers with the createQueryIframe feature
676+
if (isCreateQueryIframeSupported) {
677+
await handleCreateQueryForm();
678+
}
679+
564680
// Wait for connection to be active
565681
const notification = await VSBrowser.instance.driver.wait<Notification>(
566682
getServerConnectedNotification

scripts/e2e.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ else
8282
node scripts/generate-test-settings.mjs --coreplus "${SERVER_URL}"
8383
fi
8484

85+
export DH_SERVER_URL="${SERVER_URL}"
86+
8587
# Run e2e tests
8688
echo "Running E2E tests..."
8789
node e2e-testing/out/runner.mjs --setup

0 commit comments

Comments
 (0)