Skip to content

Commit aecaee8

Browse files
feat: add retryingTest helper for DOM testing (#517)
* feat: add retryingTest helper for DOM testing - Add retryingTest function to handle async DOM element detection - Useful for waiting for elements that appear after API calls or rendering - Configurable retry attempts (default 20) with 1ms delay between attempts - Returns Promise that resolves when test passes or rejects with error message - Add comprehensive test suite with 8 test cases covering all scenarios - Export as part of __helpers global object for use in curriculum tests * Apply suggestions from code review Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * Update packages/tests/javascript-helper.test.ts --------- Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
1 parent 0ee62c4 commit aecaee8

File tree

2 files changed

+152
-1
lines changed

2 files changed

+152
-1
lines changed

packages/helpers/lib/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,3 +669,30 @@ export function getFunctionParams(code: string) {
669669
// Return an empty array if no function parameters are found
670670
return [];
671671
}
672+
673+
/**
674+
* Retries a test function with a specified delay between attempts
675+
* Useful for testing DOM elements that may not be immediately available
676+
* @param test - Function that returns a truthy value when the condition is met
677+
* @param message - Error message to throw if all retry attempts fail
678+
* @param tries - Number of retry attempts (defaults to 20)
679+
* @returns Promise that resolves when test passes or rejects with message
680+
* @example
681+
* await retryingTest(() => document.querySelector('img'), "'img' element not found");
682+
*/
683+
export function retryingTest(
684+
test: () => unknown,
685+
message: string,
686+
tries = 20,
687+
): Promise<void> {
688+
if (tries < 1) return Promise.reject(new Error(message));
689+
if (test()) return Promise.resolve();
690+
691+
return new Promise((resolve, reject) => {
692+
setTimeout(() => {
693+
retryingTest(test, message, tries - 1)
694+
.then(resolve)
695+
.catch(reject);
696+
}, 1);
697+
});
698+
}

packages/tests/javascript-helper.test.ts

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import jsTestValues from "./__fixtures__/curriculum-helpers-javascript";
22

3-
import { getFunctionParams } from "./../helpers/lib/index";
3+
import { getFunctionParams, retryingTest } from "./../helpers/lib/index";
44

55
const {
66
functionDeclaration,
@@ -46,4 +46,128 @@ describe("js-help", () => {
4646
expect(parameters[3].name).toBe("...rest");
4747
});
4848
});
49+
50+
describe("retryingTest", () => {
51+
beforeEach(() => {
52+
vi.useFakeTimers();
53+
});
54+
55+
afterEach(() => {
56+
vi.useRealTimers();
57+
});
58+
59+
it("should resolve immediately when test passes on first try", async () => {
60+
const testFn = vi.fn().mockReturnValue(true);
61+
62+
const promise = retryingTest(testFn, "Test failed");
63+
64+
await expect(promise).resolves.toBeUndefined();
65+
expect(testFn).toHaveBeenCalledTimes(1);
66+
});
67+
68+
it("should resolve when test passes after retries", async () => {
69+
const testFn = vi
70+
.fn()
71+
.mockReturnValueOnce(false)
72+
.mockReturnValueOnce(false)
73+
.mockReturnValueOnce(true);
74+
75+
const promise = retryingTest(testFn, "Test failed");
76+
77+
// Run all timers and wait for the promise to settle
78+
vi.runAllTimers();
79+
80+
await expect(promise).resolves.toBeUndefined();
81+
expect(testFn).toHaveBeenCalledTimes(3);
82+
});
83+
84+
it("should reject with error message when all retries fail", async () => {
85+
const testFn = vi.fn().mockReturnValue(false);
86+
const errorMessage = "Element not found";
87+
88+
const promise = retryingTest(testFn, errorMessage, 3);
89+
90+
// Run all timers and wait for the promise to settle
91+
vi.runAllTimers();
92+
93+
await expect(promise).rejects.toThrow(errorMessage);
94+
expect(testFn).toHaveBeenCalledTimes(3);
95+
});
96+
97+
it("should reject immediately when tries is less than 1", async () => {
98+
const testFn = vi.fn();
99+
const errorMessage = "Invalid tries";
100+
101+
const promise = retryingTest(testFn, errorMessage, 0);
102+
103+
await expect(promise).rejects.toThrow(errorMessage);
104+
expect(testFn).not.toHaveBeenCalled();
105+
});
106+
107+
it("should use default tries value of 20", async () => {
108+
const testFn = vi.fn().mockReturnValue(false);
109+
110+
const promise = retryingTest(testFn, "Test failed");
111+
112+
// Run all timers and wait for the promise to settle
113+
vi.runAllTimers();
114+
115+
await expect(promise).rejects.toThrow("Test failed");
116+
expect(testFn).toHaveBeenCalledTimes(20);
117+
});
118+
119+
it("should work with truthy values (not just boolean true)", async () => {
120+
const testFn = vi
121+
.fn()
122+
.mockReturnValueOnce(null)
123+
.mockReturnValueOnce(undefined)
124+
.mockReturnValueOnce(0)
125+
.mockReturnValueOnce("")
126+
.mockReturnValueOnce("found element"); // Truthy
127+
128+
const promise = retryingTest(testFn, "Test failed");
129+
130+
vi.runAllTimers();
131+
132+
await expect(promise).resolves.toBeUndefined();
133+
expect(testFn).toHaveBeenCalledTimes(5);
134+
});
135+
136+
it("should handle DOM element testing scenario", async () => {
137+
// Mock DOM element that appears after delay
138+
let mockElement: Element | null = null;
139+
const testFn = vi.fn(() => mockElement);
140+
141+
// Simulate element appearing after 3 attempts
142+
setTimeout(() => {
143+
mockElement = { tagName: "IMG" } as Element;
144+
}, 3);
145+
146+
const promise = retryingTest(testFn, "'img' element not found");
147+
148+
vi.runAllTimers();
149+
150+
await expect(promise).resolves.toBeUndefined();
151+
expect(testFn).toHaveBeenCalledTimes(4);
152+
});
153+
154+
it("should wait 1ms between retries", async () => {
155+
const testFn = vi
156+
.fn()
157+
.mockReturnValueOnce(false)
158+
.mockReturnValueOnce(false)
159+
.mockReturnValueOnce(true);
160+
161+
const promise = retryingTest(testFn, "Test failed");
162+
163+
// Step through timers manually to verify 1ms delay
164+
expect(testFn).toHaveBeenCalledTimes(1);
165+
vi.advanceTimersByTime(1);
166+
expect(testFn).toHaveBeenCalledTimes(2);
167+
vi.advanceTimersByTime(1);
168+
169+
await expect(promise).resolves.toBeUndefined();
170+
expect(testFn).toHaveBeenCalledTimes(3);
171+
});
172+
});
49173
});

0 commit comments

Comments
 (0)