diff --git a/index.test.ts b/index.test.ts index 944d520..d18d2a4 100644 --- a/index.test.ts +++ b/index.test.ts @@ -5,8 +5,15 @@ import stripIndent from 'strip-indent'; import {getAllUrls, getTests} from './collector.js'; import * as pageDetect from './index.js'; -(globalThis as any).document = {title: ''}; +(globalThis as any).document = {title: '', readyState: 'loading'}; (globalThis as any).location = new URL('https://github.com/'); +(globalThis as any).requestAnimationFrame = (callback: FrameRequestCallback) => setTimeout(() => { + callback(Date.now()); +}, 0) as unknown as number; + +(globalThis as any).cancelAnimationFrame = (id: number) => { + clearTimeout(id); +}; const allUrls = getAllUrls(); @@ -281,3 +288,45 @@ test('parseRepoExplorerTitle', () => { undefined, ); }); + +test('waitFor - immediately true', async () => { + const detection = () => true; + const result = await pageDetect.utils.waitFor(detection); + assert.equal(result, true); +}); + +test('waitFor - becomes true', async () => { + let callCount = 0; + const detection = () => { + callCount++; + return callCount >= 3; + }; + + const result = await pageDetect.utils.waitFor(detection); + assert.equal(result, true); + assert.ok(callCount >= 3); +}); + +test('waitFor - false when document complete', async () => { + // Save original state + const originalReadyState = Object.getOwnPropertyDescriptor(document, 'readyState'); + + // Mock document.readyState to be 'complete' + Object.defineProperty(document, 'readyState', { + writable: true, + configurable: true, + value: 'complete', + }); + + const detection = () => false; + const result = await pageDetect.utils.waitFor(detection); + assert.equal(result, false); + + // Restore original state + if (originalReadyState) { + Object.defineProperty(document, 'readyState', originalReadyState); + } else { + // If readyState wasn't a property before, delete it + delete (document as any).readyState; + } +}); diff --git a/index.ts b/index.ts index 3e9988b..a14492c 100644 --- a/index.ts +++ b/index.ts @@ -4,6 +4,35 @@ import {addTests} from './collector.ts'; const $ = (selector: string) => document.querySelector(selector); const exists = (selector: string) => Boolean($(selector)); +/** + * Waits for a detection to return true by repeatedly checking it on each animation frame. + * Useful for DOM-based detections that need to wait for elements to appear. + * @param detection - A detection function to check repeatedly + * @returns A promise that resolves to the final result of the detection + * @example + * ``` + * import {utils} from 'github-url-detection'; + * + * async function init() { + * if (!await utils.waitFor(isOrganizationProfile)) { + * return; + * } + * // Do something when on organization profile + * } + * ``` + */ +async function waitFor(detection: () => boolean): Promise { + // eslint-disable-next-line no-await-in-loop -- We need to wait on each frame + while (!detection() && document.readyState !== 'complete') { + // eslint-disable-next-line no-await-in-loop + await new Promise(resolve => { + requestAnimationFrame(resolve); + }); + } + + return detection(); +} + const combinedTestOnly = ['combinedTestOnly']; // To be used only to skip tests of combined functions, i.e. isPageA() || isPageB() TEST: addTests('__urls_that_dont_match__', [ @@ -962,4 +991,5 @@ export const utils = { getCleanGistPathname, getRepositoryInfo: getRepo, parseRepoExplorerTitle, + waitFor, }; diff --git a/readme.md b/readme.md index f2fba7a..bd41b2c 100644 --- a/readme.md +++ b/readme.md @@ -67,6 +67,30 @@ if (pageDetect.isOrganizationProfile()) { } ``` +### Async detections with `waitFor` + +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. + +```js +import {utils, isOrganizationProfile} from 'github-url-detection'; + +async function init() { + // Wait for the detection to return true or for the document to be complete + if (!await utils.waitFor(isOrganizationProfile)) { + return; // Not an organization profile + } + + // The page is now confirmed to be an organization profile + console.log('On organization profile!'); +} +``` + +The `waitFor` function: +- Repeatedly calls the detection function on each animation frame +- Stops when the detection returns `true` or when `document.readyState` is `'complete'` +- Returns the final result of the detection +- Works with any detection function that returns a boolean + ## Related - [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.