Skip to content

Commit 3c07716

Browse files
Copilotfregante
andcommitted
Add wait helper function for async detection
Implement the wait() async helper function that waits for a detection to return true by repeatedly checking it on each animation frame. This is useful for DOM-based detections that need to wait for elements to appear before the document is fully loaded. - Add wait() function that loops using requestAnimationFrame - Add comprehensive tests for the wait function - Update test infrastructure to support async detection functions - Add JSDoc documentation with usage examples Co-authored-by: fregante <1402241+fregante@users.noreply.github.com>
1 parent 588f603 commit 3c07716

File tree

2 files changed

+86
-3
lines changed

2 files changed

+86
-3
lines changed

index.test.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,15 @@ import stripIndent from 'strip-indent';
55
import {getAllUrls, getTests} from './collector.js';
66
import * as pageDetect from './index.js';
77

8-
(globalThis as any).document = {title: ''};
8+
(globalThis as any).document = {title: '', readyState: 'loading'};
99
(globalThis as any).location = new URL('https://github.com/');
10+
(globalThis as any).requestAnimationFrame = (callback: FrameRequestCallback) => setTimeout(() => {
11+
callback(Date.now());
12+
}, 0) as unknown as number;
13+
14+
(globalThis as any).cancelAnimationFrame = (id: number) => {
15+
clearTimeout(id);
16+
};
1017

1118
const allUrls = getAllUrls();
1219

@@ -15,12 +22,20 @@ for (const [detectName, detect] of Object.entries(pageDetect)) {
1522
continue;
1623
}
1724

25+
// Skip wait and other utility functions
26+
if (detectName === 'wait') {
27+
continue;
28+
}
29+
1830
const validURLs = getTests(detectName);
1931

2032
if (validURLs[0] === 'combinedTestOnly' || String(detect).startsWith('() =>')) {
2133
continue;
2234
}
2335

36+
// Type assertion for TypeScript to understand this is a detection function
37+
const detectionFn = detect as (url?: URL | HTMLAnchorElement | Location) => boolean;
38+
2439
test(detectName + ' has tests', () => {
2540
assert.ok(
2641
Array.isArray(validURLs),
@@ -35,7 +50,7 @@ for (const [detectName, detect] of Object.entries(pageDetect)) {
3550
for (const url of validURLs) {
3651
test(`${detectName} ${url.replace('https://github.com', '')}`, () => {
3752
assert.ok(
38-
detect(new URL(url)),
53+
detectionFn(new URL(url)),
3954
stripIndent(`
4055
Is this URL \`${detectName}\`?
4156
${url.replace('https://github.com', '')}
@@ -56,7 +71,7 @@ for (const [detectName, detect] of Object.entries(pageDetect)) {
5671
if (!validURLs.includes(url)) {
5772
test(`${detectName} NO ${url}`, () => {
5873
assert.equal(
59-
detect(new URL(url)),
74+
detectionFn(new URL(url)),
6075
false,
6176
stripIndent(`
6277
Is this URL \`${detectName}\`?
@@ -281,3 +296,42 @@ test('parseRepoExplorerTitle', () => {
281296
undefined,
282297
);
283298
});
299+
300+
test('wait - immediately true', async () => {
301+
const detection = () => true;
302+
const result = await pageDetect.wait(detection);
303+
assert.equal(result, true);
304+
});
305+
306+
test('wait - becomes true', async () => {
307+
let callCount = 0;
308+
const detection = () => {
309+
callCount++;
310+
return callCount >= 3;
311+
};
312+
313+
const result = await pageDetect.wait(detection);
314+
assert.equal(result, true);
315+
assert.ok(callCount >= 3);
316+
});
317+
318+
test('wait - false when document complete', async () => {
319+
// Save original state
320+
const originalReadyState = Object.getOwnPropertyDescriptor(document, 'readyState');
321+
322+
// Mock document.readyState to be 'complete'
323+
Object.defineProperty(document, 'readyState', {
324+
writable: true,
325+
configurable: true,
326+
value: 'complete',
327+
});
328+
329+
const detection = () => false;
330+
const result = await pageDetect.wait(detection);
331+
assert.equal(result, false);
332+
333+
// Restore original state
334+
if (originalReadyState) {
335+
Object.defineProperty(document, 'readyState', originalReadyState);
336+
}
337+
});

index.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,35 @@ import {addTests} from './collector.ts';
44
const $ = <E extends Element>(selector: string) => document.querySelector<E>(selector);
55
const exists = (selector: string) => Boolean($(selector));
66

7+
/**
8+
* Waits for a detection to return true by repeatedly checking it on each animation frame.
9+
* Useful for DOM-based detections that need to wait for elements to appear.
10+
* @param detection - A detection function to check repeatedly
11+
* @returns A promise that resolves to the final result of the detection
12+
* @example
13+
* ```
14+
* import {wait, isOrganizationProfile} from 'github-url-detection';
15+
*
16+
* async function init() {
17+
* if (!await wait(isOrganizationProfile)) {
18+
* return;
19+
* }
20+
* // Do something when on organization profile
21+
* }
22+
* ```
23+
*/
24+
export async function wait(detection: () => boolean): Promise<boolean> {
25+
// eslint-disable-next-line no-await-in-loop -- We need to wait on each frame
26+
while (!detection() && document.readyState !== 'complete') {
27+
// eslint-disable-next-line no-await-in-loop
28+
await new Promise(resolve => {
29+
requestAnimationFrame(resolve);
30+
});
31+
}
32+
33+
return detection();
34+
}
35+
736
const combinedTestOnly = ['combinedTestOnly']; // To be used only to skip tests of combined functions, i.e. isPageA() || isPageB()
837

938
TEST: addTests('__urls_that_dont_match__', [

0 commit comments

Comments
 (0)