Skip to content

Commit 5f60403

Browse files
committed
fix(test-elements): correct test element markings on initial tree load and tree refresh
1 parent 12eebc1 commit 5f60403

File tree

1 file changed

+166
-81
lines changed

1 file changed

+166
-81
lines changed

src/treeViews/implementations/testElements/TestElementsTreeView.ts

Lines changed: 166 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ export class TestElementsTreeView extends TreeViewBase<TestElementsTreeItem> {
199199
try {
200200
await this.refreshResourceAvailabilityFromWorkspace();
201201
await this.updateAllParentMarkings();
202+
this._onDidChangeTreeData.fire(undefined);
202203
} catch (error) {
203204
this.logger.error(
204205
"[TestElementsTreeView] Error during debounced resource availability refresh:",
@@ -272,6 +273,124 @@ export class TestElementsTreeView extends TreeViewBase<TestElementsTreeItem> {
272273
}
273274
}
274275

276+
/**
277+
* Ensure Language Server readiness for availability/icon checks.
278+
*/
279+
private async ensureLanguageServerReadyForAvailabilityChecks(): Promise<void> {
280+
if (isLanguageServerRunning()) {
281+
return;
282+
}
283+
284+
const cfgExists = await hasLsConfig();
285+
if (!cfgExists) {
286+
this.logger.trace("[TestElementsTreeView] No LS config present; proceeding with availability checks.");
287+
return;
288+
}
289+
290+
try {
291+
await updateOrRestartLS();
292+
await waitForLanguageServerReady(5000, 100);
293+
} catch {
294+
this.logger.trace("[TestElementsTreeView] LS not ready, proceeding with availability checks.");
295+
}
296+
}
297+
298+
private isResourceSubdivision(item: TestElementsTreeItem): boolean {
299+
if (item.data.testElementType !== TestElementType.Subdivision || item.data.isVirtual) {
300+
return false;
301+
}
302+
return ResourceFileService.hasResourceMarker(item.data.hierarchicalName || item.data.displayName || "");
303+
}
304+
305+
private collectSubdivisionItems(
306+
items: TestElementsTreeItem[],
307+
options: {
308+
onlyVisible: boolean;
309+
filter?: (item: TestElementsTreeItem) => boolean;
310+
}
311+
): TestElementsTreeItem[] {
312+
const subdivisionItems: TestElementsTreeItem[] = [];
313+
const { onlyVisible, filter } = options;
314+
315+
const collect = (currentItems: TestElementsTreeItem[]) => {
316+
for (const item of currentItems) {
317+
const isExpanded = item.collapsibleState === vscode.TreeItemCollapsibleState.Expanded;
318+
const shouldRecurse = !onlyVisible || isExpanded;
319+
320+
if (item.data.testElementType === TestElementType.Subdivision) {
321+
const passesVisibility = !onlyVisible || isExpanded;
322+
const passesFilter = filter ? filter(item) : true;
323+
if (passesVisibility && passesFilter) {
324+
subdivisionItems.push(item);
325+
}
326+
}
327+
328+
if (item.children && shouldRecurse) {
329+
collect(item.children as TestElementsTreeItem[]);
330+
}
331+
}
332+
};
333+
collect(items);
334+
return subdivisionItems;
335+
}
336+
337+
private async updateSubdivisionAvailability(
338+
subdivisionItems: TestElementsTreeItem[],
339+
options: {
340+
updateParentMarkingOnAvailableResource: boolean;
341+
}
342+
): Promise<void> {
343+
await this.ensureLanguageServerReadyForAvailabilityChecks();
344+
345+
// Process file checks in batches to yield to UI thread
346+
const BATCH_SIZE = 20;
347+
for (let i = 0; i < subdivisionItems.length; i += BATCH_SIZE) {
348+
const batch = subdivisionItems.slice(i, i + BATCH_SIZE);
349+
await Promise.all(
350+
batch.map(async (subdivisionItem) => {
351+
try {
352+
if (subdivisionItem.data.isVirtual) {
353+
return;
354+
}
355+
356+
const hierarchicalName = subdivisionItem.data.hierarchicalName;
357+
if (!hierarchicalName) {
358+
return;
359+
}
360+
361+
const isResourceFile = ResourceFileService.hasResourceMarker(hierarchicalName);
362+
const cleanName = this.removeResourceMarkersFromHierarchicalName(hierarchicalName).trim();
363+
let resourcePath = await this.resourceFileService.constructAbsolutePath(cleanName);
364+
365+
if (!resourcePath) {
366+
return;
367+
}
368+
369+
if (isResourceFile && !resourcePath.endsWith(".resource")) {
370+
resourcePath += ".resource";
371+
}
372+
373+
const exists = await this.resourceFileService.pathExists(resourcePath);
374+
subdivisionItem.updateLocalAvailability(exists, resourcePath);
375+
376+
if (options.updateParentMarkingOnAvailableResource && exists && isResourceFile) {
377+
await this.updateParentSubdivisionMarking(subdivisionItem);
378+
}
379+
} catch (error) {
380+
this.logger.error(
381+
`[TestElementsTreeView] Error updating subdivision availability for tree item ${subdivisionItem.label}:`,
382+
error
383+
);
384+
}
385+
})
386+
);
387+
388+
if (i + BATCH_SIZE < subdivisionItems.length) {
389+
await new Promise((resolve) => setImmediate(resolve));
390+
}
391+
}
392+
}
393+
275394
/**
276395
* Handler for resource file related operations.
277396
* @param config The configuration object defining the operation to perform.
@@ -556,9 +675,9 @@ export class TestElementsTreeView extends TreeViewBase<TestElementsTreeItem> {
556675
this.stateManager.setLoading(false);
557676
(this as any).updateTreeViewMessage();
558677

678+
// Publish new data immediately, then update availability/marking in the background.
559679
this._onDidChangeTreeData.fire(undefined);
560-
// Only check visible items initially
561-
await this.updateSubdivisionIcons(newRootItems, true);
680+
void this.runPostFetchAvailabilityUpdates(newRootItems);
562681

563682
const loadTime = Date.now() - startTime;
564683
this.logger.debug(
@@ -646,6 +765,10 @@ export class TestElementsTreeView extends TreeViewBase<TestElementsTreeItem> {
646765
// Remaining items will be checked when expanded
647766
await this.updateSubdivisionIcons(this.rootItems, true);
648767

768+
// Compute availability for all resource subdivisions and recompute parent markings
769+
await this.updateResourceSubdivisionAvailability(this.rootItems);
770+
await this.updateAllParentMarkings();
771+
649772
// Set the last data fetch timestamp to prevent infinite loading
650773
// This is important even for empty results to prevent the tree from continuously trying to load data
651774
(this as any)._lastDataFetch = Date.now();
@@ -739,10 +862,8 @@ export class TestElementsTreeView extends TreeViewBase<TestElementsTreeItem> {
739862
this.rootItems = rootTestElementItems;
740863
(this as any)._lastDataFetch = Date.now();
741864

742-
// Async icon updates for visible items only
743-
this.updateSubdivisionIcons(rootTestElementItems, true).then(() => {
744-
this._onDidChangeTreeData.fire(undefined);
745-
});
865+
// Run availability/icon updates in the background
866+
void this.runPostFetchAvailabilityUpdates(rootTestElementItems);
746867

747868
return rootTestElementItems;
748869
} catch (error) {
@@ -751,88 +872,52 @@ export class TestElementsTreeView extends TreeViewBase<TestElementsTreeItem> {
751872
}
752873
}
753874

875+
/**
876+
* Post-fetch background updates:
877+
* - refresh visible subdivision availability
878+
* - compute availability for all resource subdivisions (even under collapsed branches)
879+
* - recompute parent markings
880+
* Always triggers a final tree refresh.
881+
*/
882+
private async runPostFetchAvailabilityUpdates(rootItems: TestElementsTreeItem[]): Promise<void> {
883+
try {
884+
await this.updateSubdivisionIcons(rootItems, true);
885+
await this.updateResourceSubdivisionAvailability(rootItems);
886+
await this.updateAllParentMarkings();
887+
} catch (error) {
888+
this.logger.error("[TestElementsTreeView] Error during post-fetch availability updates:", error);
889+
} finally {
890+
this._onDidChangeTreeData.fire(undefined);
891+
}
892+
}
893+
894+
/**
895+
* Makes sure local availability is computed for all resource subdivisions in the tree.
896+
* This is required so parent marking/icon state is correct even when resource subdivisions
897+
* are under collapsed branches (i.e., not "visible" yet).
898+
*/
899+
private async updateResourceSubdivisionAvailability(items: TestElementsTreeItem[]): Promise<void> {
900+
const resourceSubdivisionItems = this.collectSubdivisionItems(items, {
901+
onlyVisible: false,
902+
filter: (item) => this.isResourceSubdivision(item)
903+
});
904+
await this.updateSubdivisionAvailability(resourceSubdivisionItems, {
905+
// Parent marking is recomputed in a separate full pass (updateAllParentMarkings)
906+
updateParentMarkingOnAvailableResource: false
907+
});
908+
}
909+
754910
/**
755911
* Updates all subdivision icons by checking for their existence on the local file system
756912
* @param items Array of tree items to process
757913
* @param onlyVisible If true, only checks visible/expanded items to save performance
758914
* @returns Promise that resolves when all icon updates are complete
759915
*/
760916
private async updateSubdivisionIcons(items: TestElementsTreeItem[], onlyVisible: boolean = false): Promise<void> {
761-
// The python regex processing is done in language server via testbench_ls.get_resource_directory_subdivision_index command.
762-
// Language server initialization should be awaited here to prevent error logs caused by this command call.
763-
if (!isLanguageServerRunning()) {
764-
const cfgExists = await hasLsConfig();
765-
if (cfgExists) {
766-
try {
767-
await updateOrRestartLS();
768-
await waitForLanguageServerReady(5000, 100);
769-
} catch {
770-
this.logger.trace("[TestElementsTreeView] LS not ready, proceeding with icon updates.");
771-
}
772-
} else {
773-
this.logger.trace("[TestElementsTreeView] No LS config present; proceeding with icon updates.");
774-
}
775-
}
776-
777-
const subdivisionItems: TestElementsTreeItem[] = [];
778-
const collectSubdivisions = (currentItems: TestElementsTreeItem[], checkExpanded: boolean) => {
779-
for (const item of currentItems) {
780-
if (item.data.testElementType === TestElementType.Subdivision) {
781-
if (!checkExpanded || item.collapsibleState === vscode.TreeItemCollapsibleState.Expanded) {
782-
subdivisionItems.push(item);
783-
}
784-
}
785-
if (
786-
item.children &&
787-
(!checkExpanded || item.collapsibleState === vscode.TreeItemCollapsibleState.Expanded)
788-
) {
789-
collectSubdivisions(item.children as TestElementsTreeItem[], checkExpanded);
790-
}
791-
}
792-
};
793-
collectSubdivisions(items, onlyVisible);
794-
795-
// Process file checks in batches to yield to UI thread
796-
const BATCH_SIZE = 20;
797-
for (let i = 0; i < subdivisionItems.length; i += BATCH_SIZE) {
798-
const batch = subdivisionItems.slice(i, i + BATCH_SIZE);
799-
await Promise.all(
800-
batch.map(async (subdivisionItem) => {
801-
try {
802-
if (subdivisionItem.data.isVirtual) {
803-
return;
804-
}
805-
const hierarchicalName = subdivisionItem.data.hierarchicalName;
806-
if (hierarchicalName) {
807-
const isResourceFile = ResourceFileService.hasResourceMarker(hierarchicalName);
808-
const cleanName = this.removeResourceMarkersFromHierarchicalName(hierarchicalName).trim();
809-
let resourcePath = await this.resourceFileService.constructAbsolutePath(cleanName);
810-
811-
if (resourcePath) {
812-
if (isResourceFile && !resourcePath.endsWith(".resource")) {
813-
resourcePath += ".resource";
814-
}
815-
const resourcePathExists = await this.resourceFileService.pathExists(resourcePath);
816-
subdivisionItem.updateLocalAvailability(resourcePathExists, resourcePath);
817-
818-
if (resourcePathExists) {
819-
await this.updateParentSubdivisionMarking(subdivisionItem);
820-
}
821-
}
822-
}
823-
} catch (error) {
824-
this.logger.error(
825-
`[TestElementsTreeView] Error updating subdivision icon for tree item ${subdivisionItem.label}:`,
826-
error
827-
);
828-
}
829-
})
830-
);
831-
// Yield to UI thread between batches to keep UI responsive
832-
if (i + BATCH_SIZE < subdivisionItems.length) {
833-
await new Promise((resolve) => setImmediate(resolve));
834-
}
835-
}
917+
const subdivisionItems = this.collectSubdivisionItems(items, { onlyVisible });
918+
await this.updateSubdivisionAvailability(subdivisionItems, {
919+
updateParentMarkingOnAvailableResource: true
920+
});
836921
}
837922

838923
/**

0 commit comments

Comments
 (0)