From e4da51573e2a61724f62af7a0fd2ad7d6edb6a26 Mon Sep 17 00:00:00 2001 From: "@rugpanov" Date: Wed, 1 Jul 2026 20:16:03 +0200 Subject: [PATCH] Fix flaky UC explorer e2e test (lazy-load race) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *Why* The `unity_catalog.ucws.e2e.ts` e2e suite flakes on the Windows shard with `AssertionError: Schema 'access' not found. Got: __internal_data_quality_monitoring`, cascading into 5 failing specs (all the `system.access` assertions). UC tree nodes lazy-load their children over the network and stream them in incrementally. `openUCPath` accepted the child list on the *first* poll where `getChildren().length > 0`, so it captured only the first schema that arrived. `__internal_data_quality_monitoring` sorts before `access` (`_` < `a`), so it streamed in first and the assertion ran before `access` had loaded. *What* - Extract the expand-and-read logic from `openUCPath` into a `getStableChildren` helper that requires the child count to be non-zero AND unchanged across 3 consecutive polls before returning, so the whole lazy-loaded batch has landed rather than just the first child. - `openUCPath` now delegates to `getStableChildren` for every path segment; descent and below-the-fold re-scroll behaviour are unchanged. *Verification* - `npx tsc --noEmit` — no new errors in the changed file. - `npx eslint src/test/e2e/utils/unityCatalogUtils.ts` — clean (exit 0). - e2e runs in databricks-eng/eng-dev-ecosystem; will validate on the isolated PR run. Co-authored-by: Isaac --- .../src/test/e2e/utils/unityCatalogUtils.ts | 103 ++++++++++++------ 1 file changed, 68 insertions(+), 35 deletions(-) diff --git a/packages/databricks-vscode/src/test/e2e/utils/unityCatalogUtils.ts b/packages/databricks-vscode/src/test/e2e/utils/unityCatalogUtils.ts index e8776b697..48d6c96e4 100644 --- a/packages/databricks-vscode/src/test/e2e/utils/unityCatalogUtils.ts +++ b/packages/databricks-vscode/src/test/e2e/utils/unityCatalogUtils.ts @@ -112,6 +112,72 @@ export async function getUCActionButton( return undefined; } +/** + * Expands an already-located tree item and returns its children once the list + * has fully loaded and stabilised. + * + * UC nodes lazy-load their children over the network and stream them into the + * tree incrementally. A naive `waitUntil(() => getChildren().length > 0)` + * returns on the *first* child that arrives, which is why the "system" catalog + * intermittently reported only `__internal_data_quality_monitoring` (it sorts + * before `access`, so it streams in first) and the `access` assertion flaked. + * + * To avoid that race we require the child count to be non-zero AND unchanged for + * several consecutive polls before accepting the result, so the whole batch has + * landed. The parent is re-scrolled to the top of the viewport on every poll: + * VS Code virtual lists only render the ~25 rows inside the scroll window, so + * keeping the parent pinned to the top keeps its immediate children rendered. + */ +async function getStableChildren( + current: TreeItem, + pathLabel: string +): Promise { + // Bring item into view before interacting with it + await current.elem.scrollIntoView({block: "start"}); + await browser.pause(200); + + // Trigger expansion once before the retry loop. If aria-expanded + // lags behind the click during lazy loading, the expand() call + // inside getChildren() would otherwise toggle the item closed on + // every retry. + await (current as any).expand(); + await browser.pause(500); + + // Number of consecutive polls the child count must stay unchanged before we + // treat the lazy-loaded list as fully settled. + const requiredStableReads = 3; + let children: TreeItem[] = []; + let lastCount = -1; + let stableReads = 0; + + await browser.waitUntil( + async () => { + // Re-scroll the item to the top of the viewport on every retry so + // its immediate children stay within the rendered scroll window. + await current.elem.scrollIntoView({block: "start"}); + await browser.pause(100); + // getChildren() calls expand() internally then queries DOM rows + children = await (current as any).getChildren(); + + if (children.length > 0 && children.length === lastCount) { + stableReads += 1; + } else { + stableReads = 0; + } + lastCount = children.length; + + return stableReads >= requiredStableReads; + }, + { + timeout: 30_000, + interval: 1000, + timeoutMsg: `Children of "${pathLabel}" did not load and stabilise`, + } + ); + + return children; +} + /** * Expands items along `path` one level at a time and returns the children of * the final item. This replaces `section.openItem(...)` which internally calls @@ -128,41 +194,8 @@ export async function openUCPath( let current: TreeItem = await findUCItem(section, path[0]); for (let i = 0; i < path.length; i++) { - // Bring item into view before interacting with it - await current.elem.scrollIntoView({block: "start"}); - await browser.pause(200); - - // Trigger expansion once before the retry loop. If aria-expanded - // lags behind the click during lazy loading, the expand() call - // inside getChildren() would otherwise toggle the item closed on - // every retry. - await (current as any).expand(); - await browser.pause(500); - - let children: TreeItem[] = []; - await browser.waitUntil( - async () => { - // Re-scroll the item to the top of the viewport on every - // retry. VS Code virtual lists only render rows that are - // inside the current scroll window; after expansion the list - // may have scrolled so that the item's children are below the - // rendered region and therefore absent from the DOM. Keeping - // the parent item at the top ensures its immediate children - // are rendered. - await current.elem.scrollIntoView({block: "start"}); - await browser.pause(100); - // getChildren() calls expand() internally then queries DOM rows - children = await (current as any).getChildren(); - return children.length > 0; - }, - { - timeout: 30_000, - interval: 1000, - timeoutMsg: `No children found under "${path - .slice(0, i + 1) - .join(".")}"`, - } - ); + const pathLabel = path.slice(0, i + 1).join("."); + const children = await getStableChildren(current, pathLabel); if (i === path.length - 1) { return children;