Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 60 additions & 3 deletions index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

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

// Skip wait and other utility functions
if (detectName === 'wait') {
continue;
}

const validURLs = getTests(detectName);

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

// Type assertion for TypeScript to understand this is a detection function
const detectionFn = detect as (url?: URL | HTMLAnchorElement | Location) => boolean;

test(detectName + ' has tests', () => {
assert.ok(
Array.isArray(validURLs),
Expand All @@ -35,7 +50,7 @@ for (const [detectName, detect] of Object.entries(pageDetect)) {
for (const url of validURLs) {
test(`${detectName} ${url.replace('https://github.com', '')}`, () => {
assert.ok(
detect(new URL(url)),
detectionFn(new URL(url)),
stripIndent(`
Is this URL \`${detectName}\`?
${url.replace('https://github.com', '')}
Expand All @@ -56,7 +71,7 @@ for (const [detectName, detect] of Object.entries(pageDetect)) {
if (!validURLs.includes(url)) {
test(`${detectName} NO ${url}`, () => {
assert.equal(
detect(new URL(url)),
detectionFn(new URL(url)),
false,
stripIndent(`
Is this URL \`${detectName}\`?
Expand Down Expand Up @@ -281,3 +296,45 @@ test('parseRepoExplorerTitle', () => {
undefined,
);
});

test('wait - immediately true', async () => {
const detection = () => true;
const result = await pageDetect.wait(detection);
assert.equal(result, true);
});

test('wait - becomes true', async () => {
let callCount = 0;
const detection = () => {
callCount++;
return callCount >= 3;
};

const result = await pageDetect.wait(detection);
assert.equal(result, true);
assert.ok(callCount >= 3);
});

test('wait - 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.wait(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;
}
});
29 changes: 29 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,35 @@ import {addTests} from './collector.ts';
const $ = <E extends Element>(selector: string) => document.querySelector<E>(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 {wait, isOrganizationProfile} from 'github-url-detection';
*
* async function init() {
* if (!await wait(isOrganizationProfile)) {
* return;
* }
* // Do something when on organization profile
* }
* ```
*/
export async function wait(detection: () => boolean): Promise<boolean> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot

  • Export via utils only.
  • Revert type changes (detectionFn = detect as) in index.test.ts
  • Rename to waitFor

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit b177e73:

  • Renamed wait to waitFor
  • Moved to utils export only (removed direct export)
  • Reverted type assertion changes in index.test.ts

// 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__', [
Expand Down
24 changes: 24 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,30 @@ if (pageDetect.isOrganizationProfile()) {
}
```

### Async detections with `wait`

The `wait` 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 {wait, isOrganizationProfile} from 'github-url-detection';

async function init() {
// Wait for the detection to return true or for the document to be complete
if (!await wait(isOrganizationProfile)) {
return; // Not an organization profile
}

// The page is now confirmed to be an organization profile
console.log('On organization profile!');
}
```

The `wait` 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.
Expand Down