Skip to content

Commit 23b9bb0

Browse files
CopilotLipata
andcommitted
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 <[email protected]>
1 parent 6fc760b commit 23b9bb0

File tree

2 files changed

+97
-21
lines changed

2 files changed

+97
-21
lines changed

projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,42 @@ import {
1515
} from './components';
1616
import { defineComponents } from '../utils/register';
1717

18+
/**
19+
* Wait for all elements to be fully initialized and their query updates to complete
20+
* @param elements Array of IgcNgElement instances to wait for
21+
* @param timeoutMs Optional timeout in milliseconds (default: 5000)
22+
*/
23+
async function waitForElementsReady(elements: IgcNgElement[], timeoutMs: number = 5000): Promise<void> {
24+
const timeout = timer(timeoutMs).toPromise();
25+
const initPromises = elements.map(async (element) => {
26+
try {
27+
// Wait for component ref to be ready
28+
await element.ngElementStrategy[ComponentRefKey];
29+
// Wait for any pending query updates
30+
if (typeof (element.ngElementStrategy as any).waitForQueryUpdates === 'function') {
31+
await (element.ngElementStrategy as any).waitForQueryUpdates();
32+
}
33+
} catch (error) {
34+
// Element might not have the strategy or methods, just continue
35+
console.warn('Element initialization warning:', error);
36+
}
37+
});
38+
39+
await Promise.race([
40+
Promise.all(initPromises),
41+
timeout.then(() => { throw new Error(`Elements initialization timed out after ${timeoutMs}ms`); })
42+
]);
43+
}
44+
45+
/**
46+
* Wait for a single element to be fully initialized and its query updates to complete
47+
* @param element IgcNgElement instance to wait for
48+
* @param timeoutMs Optional timeout in milliseconds (default: 5000)
49+
*/
50+
async function waitForElementReady(element: IgcNgElement, timeoutMs: number = 5000): Promise<void> {
51+
await waitForElementsReady([element], timeoutMs);
52+
}
53+
1854
describe('Elements: ', () => {
1955
let testContainer: HTMLDivElement;
2056

@@ -48,15 +84,16 @@ describe('Elements: ', () => {
4884
const columnEl = document.createElement("igc-column") as IgcNgElement;
4985
gridEl.appendChild(columnEl);
5086

51-
// TODO: Better way to wait - potentially expose the queue or observable for update on the strategy
52-
await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2));
87+
// Wait for both elements to be fully initialized and query updates to complete
88+
await waitForElementsReady([gridEl, columnEl]);
5389

5490
const gridComponent = (await gridEl.ngElementStrategy[ComponentRefKey]).instance as IgxGridComponent;
5591
const columnComponent = (await columnEl.ngElementStrategy[ComponentRefKey]).instance as IgxColumnComponent;
5692
expect(gridComponent.columnList.toArray()).toContain(columnComponent);
5793

5894
columnEl.remove();
59-
await firstValueFrom(timer(10 /* SCHEDULE_DELAY: DESTROY + QUERY */ * 3));
95+
// Wait for the query update after removal
96+
await (gridEl.ngElementStrategy as any).waitForQueryUpdates();
6097
expect(gridComponent.columnList.toArray()).toEqual([]);
6198
});
6299

@@ -73,12 +110,12 @@ describe('Elements: ', () => {
73110
</div>`;
74111
}
75112

76-
// TODO: Better way to wait - potentially expose the queue or observable for update on the strategy
77-
await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2));
113+
// Wait for grid and column to be fully initialized
114+
await waitForElementsReady([gridEl as unknown as IgcNgElement, columnEl]);
78115

79116
// sigh (。﹏。*)
80117
(gridEl as any).toggleRow('1');
81-
await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2));
118+
await waitForElementReady(gridEl as unknown as IgcNgElement);
82119

83120
let detailGrid = document.querySelector<IgcNgElement>('#child1');
84121
expect(detailGrid).toBeDefined();
@@ -87,9 +124,9 @@ describe('Elements: ', () => {
87124

88125
// close and re-expand row detail:
89126
(gridEl as any).toggleRow('1');
90-
await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2));
127+
await waitForElementReady(gridEl as unknown as IgcNgElement);
91128
(gridEl as any).toggleRow('1');
92-
await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2));
129+
await waitForElementReady(gridEl as unknown as IgcNgElement);
93130

94131
detailGrid = document.querySelector<IgcNgElement>('#child1');
95132
expect(detailGrid).toBeDefined();
@@ -116,8 +153,8 @@ describe('Elements: ', () => {
116153
const hgridComponent = (await hgridEl.ngElementStrategy[ComponentRefKey]).instance as IgxHierarchicalGridComponent;
117154
hgridComponent.data = hgridData;
118155

119-
// TODO: Better way to wait - potentially expose the queue or observable for update on the strategy
120-
await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2));
156+
// Wait for all elements to be fully initialized and query updates to complete
157+
await waitForElementsReady([hgridEl, columnProjectId, columnName, columnStartDate]);
121158

122159
expect(hgridComponent.dataView.length).toBeGreaterThan(0);
123160
});
@@ -163,8 +200,8 @@ describe('Elements: ', () => {
163200
});
164201
testContainer.appendChild(gridEl);
165202

166-
// TODO: Better way to wait - potentially expose the queue or observable for update on the strategy
167-
await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2));
203+
// Wait for all elements to be fully initialized and query updates to complete
204+
await waitForElementsReady([gridEl as unknown as IgcNgElement, columnID as unknown as IgcNgElement, columnName as unknown as IgcNgElement]);
168205

169206
const header = document.getElementsByTagName("igx-grid-header").item(0) as HTMLElement;
170207
expect(header.innerText).toEqual('Templated ProductID');
@@ -179,8 +216,8 @@ describe('Elements: ', () => {
179216

180217
testContainer.appendChild(gridEl);
181218

182-
// TODO: Better way to wait - potentially expose the queue or observable for update on the strategy
183-
await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2));
219+
// Wait for all elements to be fully initialized and query updates to complete
220+
await waitForElementsReady([gridEl as unknown as IgcNgElement, stateComponent as unknown as IgcNgElement]);
184221
expect(() => stateComponent.getStateAsString()).not.toThrow();
185222
});
186223

@@ -201,19 +238,21 @@ describe('Elements: ', () => {
201238
</igc-grid>`;
202239
testContainer.innerHTML = innerHtml;
203240

204-
// TODO: Better way to wait - potentially expose the queue or observable for update on the strategy
205-
await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3));
206-
207241
const grid = document.querySelector<IgcNgElement & InstanceType<typeof IgcGridComponent>>('#testGrid');
208242
const thirdGroup = document.querySelector<IgcNgElement>('igc-column-layout[header="Product Stock"]');
209243
const secondGroup = document.querySelector<IgcNgElement>('igc-column-layout[header="Product Details"]');
244+
245+
// Wait for all elements to be fully initialized
246+
const allElements = [grid, thirdGroup, secondGroup, ...Array.from(document.querySelectorAll<IgcNgElement>('igc-column'))];
247+
await waitForElementsReady(allElements.filter(el => el) as IgcNgElement[]);
210248

211249
expect(grid.columns.length).toEqual(8);
212250
expect(grid.getColumnByName('ProductID')).toBeTruthy();
213251
expect(grid.getColumnByVisibleIndex(1).field).toEqual('ProductName');
214252

215253
grid.removeChild(secondGroup);
216-
await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3));
254+
// Wait for query updates after removal
255+
await (grid.ngElementStrategy as any).waitForQueryUpdates();
217256

218257
expect(grid.columns.length).toEqual(4);
219258
expect(grid.getColumnByName('ProductID')).toBeTruthy();
@@ -225,7 +264,8 @@ describe('Elements: ', () => {
225264
newColumn.setAttribute('field', 'ProductName');
226265
newGroup.appendChild(newColumn);
227266
grid.insertBefore(newGroup, thirdGroup);
228-
await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 3));
267+
// Wait for new elements to be initialized and query updates to complete
268+
await waitForElementsReady([newGroup as unknown as IgcNgElement, newColumn as unknown as IgcNgElement]);
229269

230270
expect(grid.columns.length).toEqual(6);
231271
expect(grid.getColumnByVisibleIndex(1).field).toEqual('ProductName');

projects/igniteui-angular-elements/src/app/custom-strategy.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy {
288288

289289
//#region schedule query update
290290
private schedule = new Map<string, () => void>();
291+
private queryUpdatePromises = new Map<string, Promise<void>>();
291292

292293
/**
293294
* Schedule an update for a content query for the component
@@ -298,12 +299,47 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy {
298299
this.schedule.get(queryName)();
299300
}
300301

301-
const id = setTimeout(() => this.updateQuery(queryName), SCHEDULE_DELAY);
302-
this.schedule.set(queryName, () => clearTimeout(id));
302+
// Create a promise that resolves when the query update completes
303+
const updatePromise = new Promise<void>((resolve) => {
304+
const id = setTimeout(() => {
305+
this.updateQuery(queryName);
306+
resolve();
307+
}, SCHEDULE_DELAY);
308+
this.schedule.set(queryName, () => {
309+
clearTimeout(id);
310+
resolve();
311+
});
312+
});
313+
314+
this.queryUpdatePromises.set(queryName, updatePromise);
315+
}
316+
317+
/**
318+
* Wait for all pending query updates to complete
319+
* @returns Promise that resolves when all scheduled query updates are finished
320+
*/
321+
public async waitForQueryUpdates(): Promise<void> {
322+
const pendingPromises = Array.from(this.queryUpdatePromises.values());
323+
if (pendingPromises.length > 0) {
324+
await Promise.all(pendingPromises);
325+
}
326+
}
327+
328+
/**
329+
* Wait for a specific query update to complete
330+
* @param queryName The name of the query to wait for
331+
* @returns Promise that resolves when the query update is finished
332+
*/
333+
public async waitForQueryUpdate(queryName: string): Promise<void> {
334+
const promise = this.queryUpdatePromises.get(queryName);
335+
if (promise) {
336+
await promise;
337+
}
303338
}
304339

305340
private updateQuery(queryName: string) {
306341
this.schedule.delete(queryName);
342+
this.queryUpdatePromises.delete(queryName);
307343
const componentRef = (this as any).componentRef as ComponentRef<any>;
308344
if (componentRef) {
309345
const componentConfig = this.config?.find(x => x.component === this._componentFactory.componentType);

0 commit comments

Comments
 (0)