From 6fc760b765530b34c2f440b7bab8ea533dbca182 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 Aug 2025 14:25:06 +0000 Subject: [PATCH 1/5] Initial plan From 23b9bb0de66676db1c1ae9cbbccd3e72c5b5239a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 Aug 2025 14:35:52 +0000 Subject: [PATCH 2/5] Implement robust waiting mechanisms for Element tests to fix sporadic failures - Add waitForQueryUpdates() and waitForQueryUpdate() methods to IgxCustomNgElementStrategy - Replace hard-coded timer delays with proper async waiting mechanisms - Update all failing tests to use new robust waiting patterns - Fix type compatibility issues in test code - Make tests deterministic instead of relying on timing assumptions Co-authored-by: Lipata <2621802+Lipata@users.noreply.github.com> --- .../src/app/custom-strategy.spec.ts | 78 ++++++++++++++----- .../src/app/custom-strategy.ts | 40 +++++++++- 2 files changed, 97 insertions(+), 21 deletions(-) diff --git a/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts index 1b5b3795f02..3fed25d4be9 100644 --- a/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts +++ b/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts @@ -15,6 +15,42 @@ import { } from './components'; import { defineComponents } from '../utils/register'; +/** + * Wait for all elements to be fully initialized and their query updates to complete + * @param elements Array of IgcNgElement instances to wait for + * @param timeoutMs Optional timeout in milliseconds (default: 5000) + */ +async function waitForElementsReady(elements: IgcNgElement[], timeoutMs: number = 5000): Promise { + const timeout = timer(timeoutMs).toPromise(); + const initPromises = elements.map(async (element) => { + try { + // Wait for component ref to be ready + await element.ngElementStrategy[ComponentRefKey]; + // Wait for any pending query updates + if (typeof (element.ngElementStrategy as any).waitForQueryUpdates === 'function') { + await (element.ngElementStrategy as any).waitForQueryUpdates(); + } + } catch (error) { + // Element might not have the strategy or methods, just continue + console.warn('Element initialization warning:', error); + } + }); + + await Promise.race([ + Promise.all(initPromises), + timeout.then(() => { throw new Error(`Elements initialization timed out after ${timeoutMs}ms`); }) + ]); +} + +/** + * Wait for a single element to be fully initialized and its query updates to complete + * @param element IgcNgElement instance to wait for + * @param timeoutMs Optional timeout in milliseconds (default: 5000) + */ +async function waitForElementReady(element: IgcNgElement, timeoutMs: number = 5000): Promise { + await waitForElementsReady([element], timeoutMs); +} + describe('Elements: ', () => { let testContainer: HTMLDivElement; @@ -48,15 +84,16 @@ describe('Elements: ', () => { const columnEl = document.createElement("igc-column") as IgcNgElement; gridEl.appendChild(columnEl); - // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + // Wait for both elements to be fully initialized and query updates to complete + await waitForElementsReady([gridEl, columnEl]); const gridComponent = (await gridEl.ngElementStrategy[ComponentRefKey]).instance as IgxGridComponent; const columnComponent = (await columnEl.ngElementStrategy[ComponentRefKey]).instance as IgxColumnComponent; expect(gridComponent.columnList.toArray()).toContain(columnComponent); columnEl.remove(); - await firstValueFrom(timer(10 /* SCHEDULE_DELAY: DESTROY + QUERY */ * 3)); + // Wait for the query update after removal + await (gridEl.ngElementStrategy as any).waitForQueryUpdates(); expect(gridComponent.columnList.toArray()).toEqual([]); }); @@ -73,12 +110,12 @@ describe('Elements: ', () => { `; } - // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + // Wait for grid and column to be fully initialized + await waitForElementsReady([gridEl as unknown as IgcNgElement, columnEl]); // sigh (。﹏。*) (gridEl as any).toggleRow('1'); - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + await waitForElementReady(gridEl as unknown as IgcNgElement); let detailGrid = document.querySelector('#child1'); expect(detailGrid).toBeDefined(); @@ -87,9 +124,9 @@ describe('Elements: ', () => { // close and re-expand row detail: (gridEl as any).toggleRow('1'); - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + await waitForElementReady(gridEl as unknown as IgcNgElement); (gridEl as any).toggleRow('1'); - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + await waitForElementReady(gridEl as unknown as IgcNgElement); detailGrid = document.querySelector('#child1'); expect(detailGrid).toBeDefined(); @@ -116,8 +153,8 @@ describe('Elements: ', () => { const hgridComponent = (await hgridEl.ngElementStrategy[ComponentRefKey]).instance as IgxHierarchicalGridComponent; hgridComponent.data = hgridData; - // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + // Wait for all elements to be fully initialized and query updates to complete + await waitForElementsReady([hgridEl, columnProjectId, columnName, columnStartDate]); expect(hgridComponent.dataView.length).toBeGreaterThan(0); }); @@ -163,8 +200,8 @@ describe('Elements: ', () => { }); testContainer.appendChild(gridEl); - // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + // Wait for all elements to be fully initialized and query updates to complete + await waitForElementsReady([gridEl as unknown as IgcNgElement, columnID as unknown as IgcNgElement, columnName as unknown as IgcNgElement]); const header = document.getElementsByTagName("igx-grid-header").item(0) as HTMLElement; expect(header.innerText).toEqual('Templated ProductID'); @@ -179,8 +216,8 @@ describe('Elements: ', () => { testContainer.appendChild(gridEl); - // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + // Wait for all elements to be fully initialized and query updates to complete + await waitForElementsReady([gridEl as unknown as IgcNgElement, stateComponent as unknown as IgcNgElement]); expect(() => stateComponent.getStateAsString()).not.toThrow(); }); @@ -201,19 +238,21 @@ describe('Elements: ', () => { `; testContainer.innerHTML = innerHtml; - // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3)); - const grid = document.querySelector>('#testGrid'); const thirdGroup = document.querySelector('igc-column-layout[header="Product Stock"]'); const secondGroup = document.querySelector('igc-column-layout[header="Product Details"]'); + + // Wait for all elements to be fully initialized + const allElements = [grid, thirdGroup, secondGroup, ...Array.from(document.querySelectorAll('igc-column'))]; + await waitForElementsReady(allElements.filter(el => el) as IgcNgElement[]); expect(grid.columns.length).toEqual(8); expect(grid.getColumnByName('ProductID')).toBeTruthy(); expect(grid.getColumnByVisibleIndex(1).field).toEqual('ProductName'); grid.removeChild(secondGroup); - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3)); + // Wait for query updates after removal + await (grid.ngElementStrategy as any).waitForQueryUpdates(); expect(grid.columns.length).toEqual(4); expect(grid.getColumnByName('ProductID')).toBeTruthy(); @@ -225,7 +264,8 @@ describe('Elements: ', () => { newColumn.setAttribute('field', 'ProductName'); newGroup.appendChild(newColumn); grid.insertBefore(newGroup, thirdGroup); - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3)); + // Wait for new elements to be initialized and query updates to complete + await waitForElementsReady([newGroup as unknown as IgcNgElement, newColumn as unknown as IgcNgElement]); expect(grid.columns.length).toEqual(6); expect(grid.getColumnByVisibleIndex(1).field).toEqual('ProductName'); diff --git a/projects/igniteui-angular-elements/src/app/custom-strategy.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.ts index 1907cba1fe7..720d6c37eac 100644 --- a/projects/igniteui-angular-elements/src/app/custom-strategy.ts +++ b/projects/igniteui-angular-elements/src/app/custom-strategy.ts @@ -288,6 +288,7 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy { //#region schedule query update private schedule = new Map void>(); + private queryUpdatePromises = new Map>(); /** * Schedule an update for a content query for the component @@ -298,12 +299,47 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy { this.schedule.get(queryName)(); } - const id = setTimeout(() => this.updateQuery(queryName), SCHEDULE_DELAY); - this.schedule.set(queryName, () => clearTimeout(id)); + // Create a promise that resolves when the query update completes + const updatePromise = new Promise((resolve) => { + const id = setTimeout(() => { + this.updateQuery(queryName); + resolve(); + }, SCHEDULE_DELAY); + this.schedule.set(queryName, () => { + clearTimeout(id); + resolve(); + }); + }); + + this.queryUpdatePromises.set(queryName, updatePromise); + } + + /** + * Wait for all pending query updates to complete + * @returns Promise that resolves when all scheduled query updates are finished + */ + public async waitForQueryUpdates(): Promise { + const pendingPromises = Array.from(this.queryUpdatePromises.values()); + if (pendingPromises.length > 0) { + await Promise.all(pendingPromises); + } + } + + /** + * Wait for a specific query update to complete + * @param queryName The name of the query to wait for + * @returns Promise that resolves when the query update is finished + */ + public async waitForQueryUpdate(queryName: string): Promise { + const promise = this.queryUpdatePromises.get(queryName); + if (promise) { + await promise; + } } private updateQuery(queryName: string) { this.schedule.delete(queryName); + this.queryUpdatePromises.delete(queryName); const componentRef = (this as any).componentRef as ComponentRef; if (componentRef) { const componentConfig = this.config?.find(x => x.component === this._componentFactory.componentType); From 67b79f3fe3eef6f943132e5488601014e617e7d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 Aug 2025 14:44:24 +0000 Subject: [PATCH 3/5] Revert retry mechanism changes and restore original fixed timer delays Co-authored-by: Lipata <2621802+Lipata@users.noreply.github.com> --- .../src/app/custom-strategy.spec.ts | 78 +++++-------------- .../src/app/custom-strategy.ts | 40 +--------- 2 files changed, 21 insertions(+), 97 deletions(-) diff --git a/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts index 3fed25d4be9..1b5b3795f02 100644 --- a/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts +++ b/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts @@ -15,42 +15,6 @@ import { } from './components'; import { defineComponents } from '../utils/register'; -/** - * Wait for all elements to be fully initialized and their query updates to complete - * @param elements Array of IgcNgElement instances to wait for - * @param timeoutMs Optional timeout in milliseconds (default: 5000) - */ -async function waitForElementsReady(elements: IgcNgElement[], timeoutMs: number = 5000): Promise { - const timeout = timer(timeoutMs).toPromise(); - const initPromises = elements.map(async (element) => { - try { - // Wait for component ref to be ready - await element.ngElementStrategy[ComponentRefKey]; - // Wait for any pending query updates - if (typeof (element.ngElementStrategy as any).waitForQueryUpdates === 'function') { - await (element.ngElementStrategy as any).waitForQueryUpdates(); - } - } catch (error) { - // Element might not have the strategy or methods, just continue - console.warn('Element initialization warning:', error); - } - }); - - await Promise.race([ - Promise.all(initPromises), - timeout.then(() => { throw new Error(`Elements initialization timed out after ${timeoutMs}ms`); }) - ]); -} - -/** - * Wait for a single element to be fully initialized and its query updates to complete - * @param element IgcNgElement instance to wait for - * @param timeoutMs Optional timeout in milliseconds (default: 5000) - */ -async function waitForElementReady(element: IgcNgElement, timeoutMs: number = 5000): Promise { - await waitForElementsReady([element], timeoutMs); -} - describe('Elements: ', () => { let testContainer: HTMLDivElement; @@ -84,16 +48,15 @@ describe('Elements: ', () => { const columnEl = document.createElement("igc-column") as IgcNgElement; gridEl.appendChild(columnEl); - // Wait for both elements to be fully initialized and query updates to complete - await waitForElementsReady([gridEl, columnEl]); + // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); const gridComponent = (await gridEl.ngElementStrategy[ComponentRefKey]).instance as IgxGridComponent; const columnComponent = (await columnEl.ngElementStrategy[ComponentRefKey]).instance as IgxColumnComponent; expect(gridComponent.columnList.toArray()).toContain(columnComponent); columnEl.remove(); - // Wait for the query update after removal - await (gridEl.ngElementStrategy as any).waitForQueryUpdates(); + await firstValueFrom(timer(10 /* SCHEDULE_DELAY: DESTROY + QUERY */ * 3)); expect(gridComponent.columnList.toArray()).toEqual([]); }); @@ -110,12 +73,12 @@ describe('Elements: ', () => { `; } - // Wait for grid and column to be fully initialized - await waitForElementsReady([gridEl as unknown as IgcNgElement, columnEl]); + // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); // sigh (。﹏。*) (gridEl as any).toggleRow('1'); - await waitForElementReady(gridEl as unknown as IgcNgElement); + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); let detailGrid = document.querySelector('#child1'); expect(detailGrid).toBeDefined(); @@ -124,9 +87,9 @@ describe('Elements: ', () => { // close and re-expand row detail: (gridEl as any).toggleRow('1'); - await waitForElementReady(gridEl as unknown as IgcNgElement); + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); (gridEl as any).toggleRow('1'); - await waitForElementReady(gridEl as unknown as IgcNgElement); + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); detailGrid = document.querySelector('#child1'); expect(detailGrid).toBeDefined(); @@ -153,8 +116,8 @@ describe('Elements: ', () => { const hgridComponent = (await hgridEl.ngElementStrategy[ComponentRefKey]).instance as IgxHierarchicalGridComponent; hgridComponent.data = hgridData; - // Wait for all elements to be fully initialized and query updates to complete - await waitForElementsReady([hgridEl, columnProjectId, columnName, columnStartDate]); + // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); expect(hgridComponent.dataView.length).toBeGreaterThan(0); }); @@ -200,8 +163,8 @@ describe('Elements: ', () => { }); testContainer.appendChild(gridEl); - // Wait for all elements to be fully initialized and query updates to complete - await waitForElementsReady([gridEl as unknown as IgcNgElement, columnID as unknown as IgcNgElement, columnName as unknown as IgcNgElement]); + // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); const header = document.getElementsByTagName("igx-grid-header").item(0) as HTMLElement; expect(header.innerText).toEqual('Templated ProductID'); @@ -216,8 +179,8 @@ describe('Elements: ', () => { testContainer.appendChild(gridEl); - // Wait for all elements to be fully initialized and query updates to complete - await waitForElementsReady([gridEl as unknown as IgcNgElement, stateComponent as unknown as IgcNgElement]); + // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); expect(() => stateComponent.getStateAsString()).not.toThrow(); }); @@ -238,21 +201,19 @@ describe('Elements: ', () => { `; testContainer.innerHTML = innerHtml; + // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3)); + const grid = document.querySelector>('#testGrid'); const thirdGroup = document.querySelector('igc-column-layout[header="Product Stock"]'); const secondGroup = document.querySelector('igc-column-layout[header="Product Details"]'); - - // Wait for all elements to be fully initialized - const allElements = [grid, thirdGroup, secondGroup, ...Array.from(document.querySelectorAll('igc-column'))]; - await waitForElementsReady(allElements.filter(el => el) as IgcNgElement[]); expect(grid.columns.length).toEqual(8); expect(grid.getColumnByName('ProductID')).toBeTruthy(); expect(grid.getColumnByVisibleIndex(1).field).toEqual('ProductName'); grid.removeChild(secondGroup); - // Wait for query updates after removal - await (grid.ngElementStrategy as any).waitForQueryUpdates(); + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3)); expect(grid.columns.length).toEqual(4); expect(grid.getColumnByName('ProductID')).toBeTruthy(); @@ -264,8 +225,7 @@ describe('Elements: ', () => { newColumn.setAttribute('field', 'ProductName'); newGroup.appendChild(newColumn); grid.insertBefore(newGroup, thirdGroup); - // Wait for new elements to be initialized and query updates to complete - await waitForElementsReady([newGroup as unknown as IgcNgElement, newColumn as unknown as IgcNgElement]); + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3)); expect(grid.columns.length).toEqual(6); expect(grid.getColumnByVisibleIndex(1).field).toEqual('ProductName'); diff --git a/projects/igniteui-angular-elements/src/app/custom-strategy.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.ts index 720d6c37eac..1907cba1fe7 100644 --- a/projects/igniteui-angular-elements/src/app/custom-strategy.ts +++ b/projects/igniteui-angular-elements/src/app/custom-strategy.ts @@ -288,7 +288,6 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy { //#region schedule query update private schedule = new Map void>(); - private queryUpdatePromises = new Map>(); /** * Schedule an update for a content query for the component @@ -299,47 +298,12 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy { this.schedule.get(queryName)(); } - // Create a promise that resolves when the query update completes - const updatePromise = new Promise((resolve) => { - const id = setTimeout(() => { - this.updateQuery(queryName); - resolve(); - }, SCHEDULE_DELAY); - this.schedule.set(queryName, () => { - clearTimeout(id); - resolve(); - }); - }); - - this.queryUpdatePromises.set(queryName, updatePromise); - } - - /** - * Wait for all pending query updates to complete - * @returns Promise that resolves when all scheduled query updates are finished - */ - public async waitForQueryUpdates(): Promise { - const pendingPromises = Array.from(this.queryUpdatePromises.values()); - if (pendingPromises.length > 0) { - await Promise.all(pendingPromises); - } - } - - /** - * Wait for a specific query update to complete - * @param queryName The name of the query to wait for - * @returns Promise that resolves when the query update is finished - */ - public async waitForQueryUpdate(queryName: string): Promise { - const promise = this.queryUpdatePromises.get(queryName); - if (promise) { - await promise; - } + const id = setTimeout(() => this.updateQuery(queryName), SCHEDULE_DELAY); + this.schedule.set(queryName, () => clearTimeout(id)); } private updateQuery(queryName: string) { this.schedule.delete(queryName); - this.queryUpdatePromises.delete(queryName); const componentRef = (this as any).componentRef as ComponentRef; if (componentRef) { const componentConfig = this.config?.find(x => x.component === this._componentFactory.componentType); From c08b14fcf026b958ba47269d22a4181bed62a823 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 Aug 2025 16:03:09 +0000 Subject: [PATCH 4/5] Restore async waiting mechanisms for Element tests to fix sporadic failures Co-authored-by: Lipata <2621802+Lipata@users.noreply.github.com> --- .../src/app/custom-strategy.spec.ts | 78 ++++++++++++++----- .../src/app/custom-strategy.ts | 40 +++++++++- 2 files changed, 97 insertions(+), 21 deletions(-) diff --git a/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts index 1b5b3795f02..3fed25d4be9 100644 --- a/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts +++ b/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts @@ -15,6 +15,42 @@ import { } from './components'; import { defineComponents } from '../utils/register'; +/** + * Wait for all elements to be fully initialized and their query updates to complete + * @param elements Array of IgcNgElement instances to wait for + * @param timeoutMs Optional timeout in milliseconds (default: 5000) + */ +async function waitForElementsReady(elements: IgcNgElement[], timeoutMs: number = 5000): Promise { + const timeout = timer(timeoutMs).toPromise(); + const initPromises = elements.map(async (element) => { + try { + // Wait for component ref to be ready + await element.ngElementStrategy[ComponentRefKey]; + // Wait for any pending query updates + if (typeof (element.ngElementStrategy as any).waitForQueryUpdates === 'function') { + await (element.ngElementStrategy as any).waitForQueryUpdates(); + } + } catch (error) { + // Element might not have the strategy or methods, just continue + console.warn('Element initialization warning:', error); + } + }); + + await Promise.race([ + Promise.all(initPromises), + timeout.then(() => { throw new Error(`Elements initialization timed out after ${timeoutMs}ms`); }) + ]); +} + +/** + * Wait for a single element to be fully initialized and its query updates to complete + * @param element IgcNgElement instance to wait for + * @param timeoutMs Optional timeout in milliseconds (default: 5000) + */ +async function waitForElementReady(element: IgcNgElement, timeoutMs: number = 5000): Promise { + await waitForElementsReady([element], timeoutMs); +} + describe('Elements: ', () => { let testContainer: HTMLDivElement; @@ -48,15 +84,16 @@ describe('Elements: ', () => { const columnEl = document.createElement("igc-column") as IgcNgElement; gridEl.appendChild(columnEl); - // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + // Wait for both elements to be fully initialized and query updates to complete + await waitForElementsReady([gridEl, columnEl]); const gridComponent = (await gridEl.ngElementStrategy[ComponentRefKey]).instance as IgxGridComponent; const columnComponent = (await columnEl.ngElementStrategy[ComponentRefKey]).instance as IgxColumnComponent; expect(gridComponent.columnList.toArray()).toContain(columnComponent); columnEl.remove(); - await firstValueFrom(timer(10 /* SCHEDULE_DELAY: DESTROY + QUERY */ * 3)); + // Wait for the query update after removal + await (gridEl.ngElementStrategy as any).waitForQueryUpdates(); expect(gridComponent.columnList.toArray()).toEqual([]); }); @@ -73,12 +110,12 @@ describe('Elements: ', () => { `; } - // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + // Wait for grid and column to be fully initialized + await waitForElementsReady([gridEl as unknown as IgcNgElement, columnEl]); // sigh (。﹏。*) (gridEl as any).toggleRow('1'); - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + await waitForElementReady(gridEl as unknown as IgcNgElement); let detailGrid = document.querySelector('#child1'); expect(detailGrid).toBeDefined(); @@ -87,9 +124,9 @@ describe('Elements: ', () => { // close and re-expand row detail: (gridEl as any).toggleRow('1'); - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + await waitForElementReady(gridEl as unknown as IgcNgElement); (gridEl as any).toggleRow('1'); - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + await waitForElementReady(gridEl as unknown as IgcNgElement); detailGrid = document.querySelector('#child1'); expect(detailGrid).toBeDefined(); @@ -116,8 +153,8 @@ describe('Elements: ', () => { const hgridComponent = (await hgridEl.ngElementStrategy[ComponentRefKey]).instance as IgxHierarchicalGridComponent; hgridComponent.data = hgridData; - // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + // Wait for all elements to be fully initialized and query updates to complete + await waitForElementsReady([hgridEl, columnProjectId, columnName, columnStartDate]); expect(hgridComponent.dataView.length).toBeGreaterThan(0); }); @@ -163,8 +200,8 @@ describe('Elements: ', () => { }); testContainer.appendChild(gridEl); - // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + // Wait for all elements to be fully initialized and query updates to complete + await waitForElementsReady([gridEl as unknown as IgcNgElement, columnID as unknown as IgcNgElement, columnName as unknown as IgcNgElement]); const header = document.getElementsByTagName("igx-grid-header").item(0) as HTMLElement; expect(header.innerText).toEqual('Templated ProductID'); @@ -179,8 +216,8 @@ describe('Elements: ', () => { testContainer.appendChild(gridEl); - // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + // Wait for all elements to be fully initialized and query updates to complete + await waitForElementsReady([gridEl as unknown as IgcNgElement, stateComponent as unknown as IgcNgElement]); expect(() => stateComponent.getStateAsString()).not.toThrow(); }); @@ -201,19 +238,21 @@ describe('Elements: ', () => { `; testContainer.innerHTML = innerHtml; - // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3)); - const grid = document.querySelector>('#testGrid'); const thirdGroup = document.querySelector('igc-column-layout[header="Product Stock"]'); const secondGroup = document.querySelector('igc-column-layout[header="Product Details"]'); + + // Wait for all elements to be fully initialized + const allElements = [grid, thirdGroup, secondGroup, ...Array.from(document.querySelectorAll('igc-column'))]; + await waitForElementsReady(allElements.filter(el => el) as IgcNgElement[]); expect(grid.columns.length).toEqual(8); expect(grid.getColumnByName('ProductID')).toBeTruthy(); expect(grid.getColumnByVisibleIndex(1).field).toEqual('ProductName'); grid.removeChild(secondGroup); - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3)); + // Wait for query updates after removal + await (grid.ngElementStrategy as any).waitForQueryUpdates(); expect(grid.columns.length).toEqual(4); expect(grid.getColumnByName('ProductID')).toBeTruthy(); @@ -225,7 +264,8 @@ describe('Elements: ', () => { newColumn.setAttribute('field', 'ProductName'); newGroup.appendChild(newColumn); grid.insertBefore(newGroup, thirdGroup); - await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3)); + // Wait for new elements to be initialized and query updates to complete + await waitForElementsReady([newGroup as unknown as IgcNgElement, newColumn as unknown as IgcNgElement]); expect(grid.columns.length).toEqual(6); expect(grid.getColumnByVisibleIndex(1).field).toEqual('ProductName'); diff --git a/projects/igniteui-angular-elements/src/app/custom-strategy.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.ts index 1907cba1fe7..720d6c37eac 100644 --- a/projects/igniteui-angular-elements/src/app/custom-strategy.ts +++ b/projects/igniteui-angular-elements/src/app/custom-strategy.ts @@ -288,6 +288,7 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy { //#region schedule query update private schedule = new Map void>(); + private queryUpdatePromises = new Map>(); /** * Schedule an update for a content query for the component @@ -298,12 +299,47 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy { this.schedule.get(queryName)(); } - const id = setTimeout(() => this.updateQuery(queryName), SCHEDULE_DELAY); - this.schedule.set(queryName, () => clearTimeout(id)); + // Create a promise that resolves when the query update completes + const updatePromise = new Promise((resolve) => { + const id = setTimeout(() => { + this.updateQuery(queryName); + resolve(); + }, SCHEDULE_DELAY); + this.schedule.set(queryName, () => { + clearTimeout(id); + resolve(); + }); + }); + + this.queryUpdatePromises.set(queryName, updatePromise); + } + + /** + * Wait for all pending query updates to complete + * @returns Promise that resolves when all scheduled query updates are finished + */ + public async waitForQueryUpdates(): Promise { + const pendingPromises = Array.from(this.queryUpdatePromises.values()); + if (pendingPromises.length > 0) { + await Promise.all(pendingPromises); + } + } + + /** + * Wait for a specific query update to complete + * @param queryName The name of the query to wait for + * @returns Promise that resolves when the query update is finished + */ + public async waitForQueryUpdate(queryName: string): Promise { + const promise = this.queryUpdatePromises.get(queryName); + if (promise) { + await promise; + } } private updateQuery(queryName: string) { this.schedule.delete(queryName); + this.queryUpdatePromises.delete(queryName); const componentRef = (this as any).componentRef as ComponentRef; if (componentRef) { const componentConfig = this.config?.find(x => x.component === this._componentFactory.componentType); From 6a271125c1ce378f804c0c1aa4e32a0754c4b9d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 Aug 2025 17:22:09 +0000 Subject: [PATCH 5/5] Revert async waiting mechanisms and restore reliable fixed timer delays Co-authored-by: Lipata <2621802+Lipata@users.noreply.github.com> --- .../src/app/custom-strategy.spec.ts | 78 +++++-------------- .../src/app/custom-strategy.ts | 40 +--------- 2 files changed, 21 insertions(+), 97 deletions(-) diff --git a/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts index 3fed25d4be9..1b5b3795f02 100644 --- a/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts +++ b/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts @@ -15,42 +15,6 @@ import { } from './components'; import { defineComponents } from '../utils/register'; -/** - * Wait for all elements to be fully initialized and their query updates to complete - * @param elements Array of IgcNgElement instances to wait for - * @param timeoutMs Optional timeout in milliseconds (default: 5000) - */ -async function waitForElementsReady(elements: IgcNgElement[], timeoutMs: number = 5000): Promise { - const timeout = timer(timeoutMs).toPromise(); - const initPromises = elements.map(async (element) => { - try { - // Wait for component ref to be ready - await element.ngElementStrategy[ComponentRefKey]; - // Wait for any pending query updates - if (typeof (element.ngElementStrategy as any).waitForQueryUpdates === 'function') { - await (element.ngElementStrategy as any).waitForQueryUpdates(); - } - } catch (error) { - // Element might not have the strategy or methods, just continue - console.warn('Element initialization warning:', error); - } - }); - - await Promise.race([ - Promise.all(initPromises), - timeout.then(() => { throw new Error(`Elements initialization timed out after ${timeoutMs}ms`); }) - ]); -} - -/** - * Wait for a single element to be fully initialized and its query updates to complete - * @param element IgcNgElement instance to wait for - * @param timeoutMs Optional timeout in milliseconds (default: 5000) - */ -async function waitForElementReady(element: IgcNgElement, timeoutMs: number = 5000): Promise { - await waitForElementsReady([element], timeoutMs); -} - describe('Elements: ', () => { let testContainer: HTMLDivElement; @@ -84,16 +48,15 @@ describe('Elements: ', () => { const columnEl = document.createElement("igc-column") as IgcNgElement; gridEl.appendChild(columnEl); - // Wait for both elements to be fully initialized and query updates to complete - await waitForElementsReady([gridEl, columnEl]); + // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); const gridComponent = (await gridEl.ngElementStrategy[ComponentRefKey]).instance as IgxGridComponent; const columnComponent = (await columnEl.ngElementStrategy[ComponentRefKey]).instance as IgxColumnComponent; expect(gridComponent.columnList.toArray()).toContain(columnComponent); columnEl.remove(); - // Wait for the query update after removal - await (gridEl.ngElementStrategy as any).waitForQueryUpdates(); + await firstValueFrom(timer(10 /* SCHEDULE_DELAY: DESTROY + QUERY */ * 3)); expect(gridComponent.columnList.toArray()).toEqual([]); }); @@ -110,12 +73,12 @@ describe('Elements: ', () => { `; } - // Wait for grid and column to be fully initialized - await waitForElementsReady([gridEl as unknown as IgcNgElement, columnEl]); + // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); // sigh (。﹏。*) (gridEl as any).toggleRow('1'); - await waitForElementReady(gridEl as unknown as IgcNgElement); + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); let detailGrid = document.querySelector('#child1'); expect(detailGrid).toBeDefined(); @@ -124,9 +87,9 @@ describe('Elements: ', () => { // close and re-expand row detail: (gridEl as any).toggleRow('1'); - await waitForElementReady(gridEl as unknown as IgcNgElement); + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); (gridEl as any).toggleRow('1'); - await waitForElementReady(gridEl as unknown as IgcNgElement); + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); detailGrid = document.querySelector('#child1'); expect(detailGrid).toBeDefined(); @@ -153,8 +116,8 @@ describe('Elements: ', () => { const hgridComponent = (await hgridEl.ngElementStrategy[ComponentRefKey]).instance as IgxHierarchicalGridComponent; hgridComponent.data = hgridData; - // Wait for all elements to be fully initialized and query updates to complete - await waitForElementsReady([hgridEl, columnProjectId, columnName, columnStartDate]); + // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); expect(hgridComponent.dataView.length).toBeGreaterThan(0); }); @@ -200,8 +163,8 @@ describe('Elements: ', () => { }); testContainer.appendChild(gridEl); - // Wait for all elements to be fully initialized and query updates to complete - await waitForElementsReady([gridEl as unknown as IgcNgElement, columnID as unknown as IgcNgElement, columnName as unknown as IgcNgElement]); + // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); const header = document.getElementsByTagName("igx-grid-header").item(0) as HTMLElement; expect(header.innerText).toEqual('Templated ProductID'); @@ -216,8 +179,8 @@ describe('Elements: ', () => { testContainer.appendChild(gridEl); - // Wait for all elements to be fully initialized and query updates to complete - await waitForElementsReady([gridEl as unknown as IgcNgElement, stateComponent as unknown as IgcNgElement]); + // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); expect(() => stateComponent.getStateAsString()).not.toThrow(); }); @@ -238,21 +201,19 @@ describe('Elements: ', () => { `; testContainer.innerHTML = innerHtml; + // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3)); + const grid = document.querySelector>('#testGrid'); const thirdGroup = document.querySelector('igc-column-layout[header="Product Stock"]'); const secondGroup = document.querySelector('igc-column-layout[header="Product Details"]'); - - // Wait for all elements to be fully initialized - const allElements = [grid, thirdGroup, secondGroup, ...Array.from(document.querySelectorAll('igc-column'))]; - await waitForElementsReady(allElements.filter(el => el) as IgcNgElement[]); expect(grid.columns.length).toEqual(8); expect(grid.getColumnByName('ProductID')).toBeTruthy(); expect(grid.getColumnByVisibleIndex(1).field).toEqual('ProductName'); grid.removeChild(secondGroup); - // Wait for query updates after removal - await (grid.ngElementStrategy as any).waitForQueryUpdates(); + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3)); expect(grid.columns.length).toEqual(4); expect(grid.getColumnByName('ProductID')).toBeTruthy(); @@ -264,8 +225,7 @@ describe('Elements: ', () => { newColumn.setAttribute('field', 'ProductName'); newGroup.appendChild(newColumn); grid.insertBefore(newGroup, thirdGroup); - // Wait for new elements to be initialized and query updates to complete - await waitForElementsReady([newGroup as unknown as IgcNgElement, newColumn as unknown as IgcNgElement]); + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3)); expect(grid.columns.length).toEqual(6); expect(grid.getColumnByVisibleIndex(1).field).toEqual('ProductName'); diff --git a/projects/igniteui-angular-elements/src/app/custom-strategy.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.ts index 720d6c37eac..1907cba1fe7 100644 --- a/projects/igniteui-angular-elements/src/app/custom-strategy.ts +++ b/projects/igniteui-angular-elements/src/app/custom-strategy.ts @@ -288,7 +288,6 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy { //#region schedule query update private schedule = new Map void>(); - private queryUpdatePromises = new Map>(); /** * Schedule an update for a content query for the component @@ -299,47 +298,12 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy { this.schedule.get(queryName)(); } - // Create a promise that resolves when the query update completes - const updatePromise = new Promise((resolve) => { - const id = setTimeout(() => { - this.updateQuery(queryName); - resolve(); - }, SCHEDULE_DELAY); - this.schedule.set(queryName, () => { - clearTimeout(id); - resolve(); - }); - }); - - this.queryUpdatePromises.set(queryName, updatePromise); - } - - /** - * Wait for all pending query updates to complete - * @returns Promise that resolves when all scheduled query updates are finished - */ - public async waitForQueryUpdates(): Promise { - const pendingPromises = Array.from(this.queryUpdatePromises.values()); - if (pendingPromises.length > 0) { - await Promise.all(pendingPromises); - } - } - - /** - * Wait for a specific query update to complete - * @param queryName The name of the query to wait for - * @returns Promise that resolves when the query update is finished - */ - public async waitForQueryUpdate(queryName: string): Promise { - const promise = this.queryUpdatePromises.get(queryName); - if (promise) { - await promise; - } + const id = setTimeout(() => this.updateQuery(queryName), SCHEDULE_DELAY); + this.schedule.set(queryName, () => clearTimeout(id)); } private updateQuery(queryName: string) { this.schedule.delete(queryName); - this.queryUpdatePromises.delete(queryName); const componentRef = (this as any).componentRef as ComponentRef; if (componentRef) { const componentConfig = this.config?.find(x => x.component === this._componentFactory.componentType);