Skip to content

Commit cc8ef31

Browse files
committed
[tests] Stabilizing Public UI Tests
- Updated Kind: - kind v0.31.0 go1.25.5 linux/amd64 - Client Version: v1.35.0 - Tune up for Code Coverage v6.0.0 - Public UI Tests targeted: - Project test - Create Component test - Component Context Menu test - Operator Backed Service test - Log files included into the artifacts to be uploaded Signed-off-by: Victor Rubezhny <vrubezhny@redhat.com>
1 parent 4108edc commit cc8ef31

File tree

9 files changed

+662
-150
lines changed

9 files changed

+662
-150
lines changed

.github/workflows/continuous-integration-workflow.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ jobs:
3030
steps:
3131
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
3232
- uses: actions/checkout@v6
33+
with:
34+
fetch-depth: 1
3335

3436
# Set up Node
3537
- name: Use Node.js ${{ matrix.node }}
@@ -59,7 +61,8 @@ jobs:
5961
if: (success() || failure()) && runner.os == 'Linux'
6062
uses: helm/kind-action@v1.14.0
6163
with:
62-
version: v0.24.0
64+
version: v0.31.0
65+
node_image: kindest/node:v1.34.0
6366

6467
- name: Configure cluster
6568
if: (success() || failure()) && runner.os == 'Linux'
@@ -105,6 +108,7 @@ jobs:
105108
with:
106109
name: artifacts-${{ matrix.os }}
107110
path: |
111+
/tmp/test-resources/settings/logs/**/*.log
108112
/tmp/test-resources/screenshots/**/*.png
109113
retention-days: 2
110114

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/explorer.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,26 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
183183
.then(result => (allTrue(result) ? `${contextValue}.can-delete` : contextValue));
184184
}
185185

186+
186187
// eslint-disable-next-line class-methods-use-this
187188
async getTreeItem(element: ExplorerItem): Promise<TreeItem> {
189+
try {
190+
return await this._getTreeItem(element);
191+
} catch (err) {
192+
// eslint-disable-next-line no-console
193+
console.error('getTreeItem failed:', err, element);
194+
195+
return {
196+
label: '⚠️ Broken item',
197+
tooltip: `${err}`,
198+
iconPath: new ThemeIcon('error'),
199+
collapsibleState: TreeItemCollapsibleState.None
200+
};
201+
}
202+
}
203+
204+
// eslint-disable-next-line class-methods-use-this
205+
async _getTreeItem(element: ExplorerItem): Promise<TreeItem> {
188206
if ('command' in element || ('label' in element && 'iconPath' in element)) {
189207
return element;
190208
}
@@ -474,7 +492,23 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
474492
}
475493

476494
// eslint-disable-next-line class-methods-use-this
495+
477496
async getChildren(element?: ExplorerItem): Promise<ExplorerItem[]> {
497+
try {
498+
return await this._getChildren(element);
499+
} catch (err) {
500+
// eslint-disable-next-line no-console
501+
console.error('getChildren failed:', err);
502+
503+
return [{
504+
label: '⚠️ Error loading tree',
505+
tooltip: String(err),
506+
iconPath: new ThemeIcon('error')
507+
}];
508+
}
509+
}
510+
511+
async _getChildren(element?: ExplorerItem): Promise<ExplorerItem[]> {
478512
let result: ExplorerItem[] = [];
479513
// don't show Open In Developer Dashboard if not openshift cluster
480514
const isOpenshiftCluster = await isOpenShiftCluster(this.executionContext);

test/ui/common/conditions.ts

Lines changed: 203 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import {
1111
NotificationType,
1212
ViewItem,
1313
ViewSection,
14+
VSBrowser,
1415
WebDriver,
1516
WelcomeContentSection,
16-
Workbench,
17+
Workbench
1718
} from 'vscode-extension-tester';
1819

1920
export async function waitForInputUpdate(
@@ -97,6 +98,110 @@ export async function itemExists(
9798
);
9899
}
99100

101+
export async function stabilizeComponentsView(getSection: () => Promise<ViewSection>, timeout = 20000): Promise<ViewSection> {
102+
const result = await VSBrowser.instance.driver.wait(async () => {
103+
try {
104+
let section = await getSection();
105+
106+
await section.expand();
107+
await new Promise(res => setTimeout(res, 300));
108+
109+
section = await getSection();
110+
111+
// ---- try tree mode ----
112+
try {
113+
const items = await section.getVisibleItems();
114+
await Promise.all(items.map(i => i.getText()));
115+
return section;
116+
} catch {
117+
// ---- fallback: welcome content mode ----
118+
try {
119+
const welcome = await section.findWelcomeContent();
120+
const buttons = await welcome.getButtons();
121+
if (buttons) {
122+
return section;
123+
}
124+
} catch {
125+
return false;
126+
}
127+
}
128+
129+
return false;
130+
131+
} catch {
132+
return false;
133+
}
134+
}, timeout, 'Components view did not stabilize');
135+
136+
return result as ViewSection;
137+
}
138+
139+
export async function findItemFuzzy(section: ViewSection, label: string): Promise<ViewItem | undefined> {
140+
const items = await section.getVisibleItems();
141+
for (const item of items) {
142+
try {
143+
const text = await item.getText();
144+
if (text.includes(label)) {
145+
return item;
146+
}
147+
} catch {
148+
// ignore
149+
}
150+
}
151+
152+
return undefined;
153+
}
154+
155+
export async function waitForItem(getSection: () => Promise<ViewSection>, label: string, strict: boolean = true, timeout = 15000) {
156+
return VSBrowser.instance.driver.wait(async () => {
157+
try {
158+
const section = await getSection();
159+
const item = strict ? await section.findItem(label) : await findItemFuzzy(section, label);
160+
return item ?? false;
161+
} catch {
162+
return false;
163+
}
164+
}, timeout, `Item "${label}" not found within ${timeout}ms`) as Promise<ViewItem>;
165+
}
166+
167+
export async function waitForItemStable(getSection: () => Promise<ViewSection>, label: string, strict: boolean = true, timeout = 15000) {
168+
await stabilizeComponentsView(getSection);
169+
return waitForItem(getSection, label, strict, timeout);
170+
}
171+
172+
173+
export async function waitForItemToDisappear(getSection: () => Promise<ViewSection>, label: string, timeout = 15000): Promise<boolean> {
174+
return VSBrowser.instance.driver.wait(async () => {
175+
try {
176+
const section = await getSection();
177+
const item = await section.findItem(label);
178+
return !item;
179+
} catch {
180+
return true;
181+
}
182+
}, timeout, `Item "${label}" still exists after ${timeout}ms`);
183+
}
184+
185+
export async function waitForItemToDisappearStable(getSection: () => Promise<ViewSection>, label: string, timeout = 15000): Promise<boolean> {
186+
await stabilizeComponentsView(getSection);
187+
return waitForItemToDisappear(getSection, label, timeout);
188+
}
189+
190+
export async function withStableItem(getSection: () => Promise<ViewSection>, label: string, action: (item: ViewItem) => Promise<void>, timeout = 10_000) {
191+
await stabilizeComponentsView(getSection);
192+
return VSBrowser.instance.driver.wait(async () => {
193+
try {
194+
const section = await getSection();
195+
const item = await section.findItem(label);
196+
if (!item) return false;
197+
await action(item);
198+
return true;
199+
} catch {
200+
return false;
201+
}
202+
}, timeout, `Action on "${label}" could not be performed`);
203+
}
204+
100205
export async function itemDoesNotExist(
101206
title: string,
102207
view: ViewSection,
@@ -191,3 +296,100 @@ export async function webViewIsOpened(name: string, driver: WebDriver, timeout =
191296
}
192297
}, timeout);
193298
}
299+
300+
export async function step<T>(name: string, fn: () => Promise<T>): Promise<T> {
301+
const callSiteStack = new Error(`Step: ${name}`).stack;
302+
try {
303+
return await fn();
304+
} catch (err) {
305+
const error = err as Error;
306+
error.message = `Step failed: ${name}\n${error.message}`;
307+
if (error.stack && callSiteStack) {
308+
error.stack =
309+
`${error.stack}\n\n--- Step call site ---\n${callSiteStack}`;
310+
}
311+
throw error;
312+
}
313+
}
314+
315+
export async function debugClick(element, name: string) {
316+
try {
317+
await element.click();
318+
} catch (e) {
319+
/* eslint-disable no-console */
320+
error(`Failed to click: ${name}`);
321+
error(await element.getAttribute('outerHTML'));
322+
/* eslint-enable no-console */
323+
throw e;
324+
}
325+
}
326+
327+
/* eslint-disable no-console */
328+
export async function listItems(getSection: () => Promise<ViewSection>, title = undefined): Promise<void> {
329+
const t = title ? `[${title}]: ` : '';
330+
log(`${t}Current components in view: >>>`);
331+
332+
try {
333+
await VSBrowser.instance.driver.wait(async () => {
334+
try {
335+
const section = await getSection();
336+
337+
// Expand the section
338+
await section.expand();
339+
340+
// First try: list actual components
341+
try {
342+
const items = await VSBrowser.instance.driver.wait(async () => {
343+
try {
344+
return await section.getVisibleItems();
345+
} catch {
346+
return null;
347+
}
348+
}, 10000);
349+
350+
if (items && items.length) {
351+
const labels = [];
352+
for (const item of items) {
353+
try {
354+
labels.push(await item.getText());
355+
} catch {
356+
labels.push('<error>');
357+
}
358+
}
359+
log(`[${labels.join(', ')}]`);
360+
return true;
361+
}
362+
} catch {
363+
// fallback to welcome content
364+
}
365+
366+
// Second try: welcome content (empty section)
367+
try {
368+
const welcome = await section.findWelcomeContent();
369+
const buttons = await welcome.getButtons();
370+
if (buttons && buttons.length) {
371+
log('[<empty: welcome content>]');
372+
return true;
373+
}
374+
} catch {
375+
// still nothing visible, retry
376+
}
377+
378+
return false;
379+
380+
} catch {
381+
return false;
382+
}
383+
}, 10000, 'Could not list items');
384+
385+
} catch (err) {
386+
log('Error while getting the current components: ', err);
387+
} finally {
388+
log('<<<');
389+
}
390+
}
391+
392+
export const log = console.log;
393+
export const warn = console.warn;
394+
export const error = console.error;
395+
/* eslint-enable no-console */

0 commit comments

Comments
 (0)