Skip to content

Commit 41f9fe6

Browse files
Copilotfregante
andauthored
Add async waitFor() helper for DOM-based detections (#227)
Co-authored-by: fregante <me@fregante.com>
1 parent 73e3e49 commit 41f9fe6

File tree

3 files changed

+104
-1
lines changed

3 files changed

+104
-1
lines changed

index.test.ts

Lines changed: 50 additions & 1 deletion
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

@@ -281,3 +288,45 @@ test('parseRepoExplorerTitle', () => {
281288
undefined,
282289
);
283290
});
291+
292+
test('waitFor - immediately true', async () => {
293+
const detection = () => true;
294+
const result = await pageDetect.utils.waitFor(detection);
295+
assert.equal(result, true);
296+
});
297+
298+
test('waitFor - becomes true', async () => {
299+
let callCount = 0;
300+
const detection = () => {
301+
callCount++;
302+
return callCount >= 3;
303+
};
304+
305+
const result = await pageDetect.utils.waitFor(detection);
306+
assert.equal(result, true);
307+
assert.ok(callCount >= 3);
308+
});
309+
310+
test('waitFor - false when document complete', async () => {
311+
// Save original state
312+
const originalReadyState = Object.getOwnPropertyDescriptor(document, 'readyState');
313+
314+
// Mock document.readyState to be 'complete'
315+
Object.defineProperty(document, 'readyState', {
316+
writable: true,
317+
configurable: true,
318+
value: 'complete',
319+
});
320+
321+
const detection = () => false;
322+
const result = await pageDetect.utils.waitFor(detection);
323+
assert.equal(result, false);
324+
325+
// Restore original state
326+
if (originalReadyState) {
327+
Object.defineProperty(document, 'readyState', originalReadyState);
328+
} else {
329+
// If readyState wasn't a property before, delete it
330+
delete (document as any).readyState;
331+
}
332+
});

index.ts

Lines changed: 30 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 {utils} from 'github-url-detection';
15+
*
16+
* async function init() {
17+
* if (!await utils.waitFor(isOrganizationProfile)) {
18+
* return;
19+
* }
20+
* // Do something when on organization profile
21+
* }
22+
* ```
23+
*/
24+
async function waitFor(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__', [
@@ -1013,4 +1042,5 @@ export const utils = {
10131042
getCleanGistPathname,
10141043
getRepositoryInfo: getRepo,
10151044
parseRepoExplorerTitle,
1045+
waitFor,
10161046
};

readme.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,30 @@ if (pageDetect.isOrganizationProfile()) {
6767
}
6868
```
6969

70+
### Async detections with `waitFor`
71+
72+
The `waitFor` helper function allows you to wait for a detection to become 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.
73+
74+
```js
75+
import {utils, isOrganizationProfile} from 'github-url-detection';
76+
77+
async function init() {
78+
// Wait for the detection to return true or for the document to be complete
79+
if (!await utils.waitFor(isOrganizationProfile)) {
80+
return; // Not an organization profile
81+
}
82+
83+
// The page is now confirmed to be an organization profile
84+
console.log('On organization profile!');
85+
}
86+
```
87+
88+
The `waitFor` function:
89+
- Repeatedly calls the detection function on each animation frame
90+
- Stops when the detection returns `true` or when `document.readyState` is `'complete'`
91+
- Returns the final result of the detection
92+
- Works with any detection function that returns a boolean
93+
7094
## Related
7195

7296
- [github-reserved-names](https://github.com/Mottie/github-reserved-names) - Get a list, or check if a user or organization name is reserved by GitHub.

0 commit comments

Comments
 (0)