Skip to content

Commit 60e98a9

Browse files
Merge branch 'main' into decode
2 parents 616c373 + c1e5e3f commit 60e98a9

File tree

5 files changed

+174
-90
lines changed

5 files changed

+174
-90
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

@@ -309,3 +316,45 @@ test('parseRepoExplorerTitle', () => {
309316
undefined,
310317
);
311318
});
319+
320+
test('waitFor - immediately true', async () => {
321+
const detection = () => true;
322+
const result = await pageDetect.utils.waitFor(detection);
323+
assert.equal(result, true);
324+
});
325+
326+
test('waitFor - becomes true', async () => {
327+
let callCount = 0;
328+
const detection = () => {
329+
callCount++;
330+
return callCount >= 3;
331+
};
332+
333+
const result = await pageDetect.utils.waitFor(detection);
334+
assert.equal(result, true);
335+
assert.ok(callCount >= 3);
336+
});
337+
338+
test('waitFor - false when document complete', async () => {
339+
// Save original state
340+
const originalReadyState = Object.getOwnPropertyDescriptor(document, 'readyState');
341+
342+
// Mock document.readyState to be 'complete'
343+
Object.defineProperty(document, 'readyState', {
344+
writable: true,
345+
configurable: true,
346+
value: 'complete',
347+
});
348+
349+
const detection = () => false;
350+
const result = await pageDetect.utils.waitFor(detection);
351+
assert.equal(result, false);
352+
353+
// Restore original state
354+
if (originalReadyState) {
355+
Object.defineProperty(document, 'readyState', originalReadyState);
356+
} else {
357+
// If readyState wasn't a property before, delete it
358+
delete (document as any).readyState;
359+
}
360+
});

index.ts

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,53 @@
1+
import type {StrictlyParseSelector} from 'typed-query-selector/parser.js';
12
import reservedNames from 'github-reserved-names/reserved-names.json' with {type: 'json'};
23
import {addTests} from './collector.ts';
34

4-
const $ = <E extends Element>(selector: string) => document.querySelector<E>(selector);
5-
const exists = (selector: string) => Boolean($(selector));
5+
function $<Selector extends string, Selected extends Element = StrictlyParseSelector<Selector>>(
6+
selector: Selector,
7+
): Selected | undefined;
8+
function $<Selected extends Element = HTMLElement>(
9+
selector: string,
10+
): Selected | undefined;
11+
function $<Selected extends Element>(selector: string): Selected | undefined {
12+
return document.querySelector<Selected>(selector) ?? undefined;
13+
}
14+
15+
function exists<Selector extends string, Selected extends Element = StrictlyParseSelector<Selector>>(
16+
selector: Selector,
17+
): Selected extends never ? never : boolean;
18+
function exists(selector: string): boolean;
19+
function exists(selector: string): boolean {
20+
return Boolean(document.querySelector(selector));
21+
}
22+
23+
/**
24+
* Waits for a detection to return true by repeatedly checking it on each animation frame.
25+
* Useful for DOM-based detections that need to wait for elements to appear.
26+
* @param detection - A detection function to check repeatedly
27+
* @returns A promise that resolves to the final result of the detection
28+
* @example
29+
* ```
30+
* import {utils} from 'github-url-detection';
31+
*
32+
* async function init() {
33+
* if (!await utils.waitFor(isOrganizationProfile)) {
34+
* return;
35+
* }
36+
* // Do something when on organization profile
37+
* }
38+
* ```
39+
*/
40+
async function waitFor(detection: () => boolean): Promise<boolean> {
41+
// eslint-disable-next-line no-await-in-loop -- We need to wait on each frame
42+
while (!detection() && document.readyState !== 'complete') {
43+
// eslint-disable-next-line no-await-in-loop
44+
await new Promise(resolve => {
45+
requestAnimationFrame(resolve);
46+
});
47+
}
48+
49+
return detection();
50+
}
651

752
const combinedTestOnly = ['combinedTestOnly']; // To be used only to skip tests of combined functions, i.e. isPageA() || isPageB()
853

@@ -61,17 +106,19 @@ TEST: addTests('isCompareWikiPage', [
61106
'https://github.com/brookhong/Surfingkeys/wiki/Color-Themes/_compare/8ebb46b1a12d16fc1af442b7df0ca13ca3bb34dc...80e51eeabe69b15a3f23880ecc36f800b71e6c6d',
62107
]);
63108

109+
/**
110+
* @deprecated Use `isHome` and/or `isFeed` instead
111+
*/
64112
export const isDashboard = (url: URL | HTMLAnchorElement | Location = location): boolean => !isGist(url) && /^$|^(orgs\/[^/]+\/)?dashboard(-feed)?(\/|$)/.test(processPathname(url));
65113
TEST: addTests('isDashboard', [
66114
'https://github.com///',
67115
'https://github.com//',
68116
'https://github.com/',
69117
'https://github.com',
70-
'https://github.com/orgs/test/dashboard',
118+
'https://github.com/orgs/refined-github/dashboard',
71119
'https://github.com/dashboard/index/2',
72120
'https://github.com//dashboard',
73121
'https://github.com/dashboard',
74-
'https://github.com/orgs/edit/dashboard',
75122
'https://github.big-corp.com/',
76123
'https://not-github.com/',
77124
'https://my-little-hub.com/',
@@ -84,6 +131,31 @@ TEST: addTests('isDashboard', [
84131
'https://github.com/dashboard-feed',
85132
]);
86133

134+
export const isHome = (url: URL | HTMLAnchorElement | Location = location): boolean => !isGist(url) && /^$|^dashboard\/?$/.test(getCleanPathname(url));
135+
TEST: addTests('isHome', [
136+
'https://github.com',
137+
'https://github.com//dashboard',
138+
'https://github.com///',
139+
'https://github.com//',
140+
'https://github.com/',
141+
'https://github.com/dashboard',
142+
'https://github.big-corp.com/',
143+
'https://not-github.com/',
144+
'https://my-little-hub.com/',
145+
'https://github.com/?tab=repositories', // Gotcha for `isUserProfileRepoTab`
146+
'https://github.com/?tab=stars', // Gotcha for `isUserProfileStarsTab`
147+
'https://github.com/?tab=followers', // Gotcha for `isUserProfileFollowersTab`
148+
'https://github.com/?tab=following', // Gotcha for `isUserProfileFollowingTab`
149+
'https://github.com/?tab=overview', // Gotcha for `isUserProfileMainTab`
150+
'https://github.com?search=1', // Gotcha for `isRepoTree`
151+
]);
152+
153+
export const isFeed = (url: URL | HTMLAnchorElement | Location = location): boolean => !isGist(url) && /^(feed|orgs\/[^/]+\/dashboard)\/?$/.test(getCleanPathname(url));
154+
TEST: addTests('isFeed', [
155+
'https://github.com/feed',
156+
'https://github.com/orgs/refined-github/dashboard',
157+
]);
158+
87159
export const isEnterprise = (url: URL | HTMLAnchorElement | Location = location): boolean => url.hostname !== 'github.com' && url.hostname !== 'gist.github.com';
88160
TEST: addTests('isEnterprise', [
89161
'https://github.big-corp.com/',
@@ -865,6 +937,9 @@ TEST: addTests('isRepositoryActions', [
865937

866938
export const isUserTheOrganizationOwner = (): boolean => isOrganizationProfile() && exists('[aria-label="Organization"] [data-tab-item="org-header-settings-tab"]');
867939

940+
/**
941+
* @deprecated Use canUserAccessRepoSettings or API instead.
942+
*/
868943
export const canUserAdminRepo = (): boolean => {
869944
const repo = getRepo();
870945
return Boolean(repo && exists(`:is(${[
@@ -874,6 +949,9 @@ export const canUserAdminRepo = (): boolean => {
874949
].join(',')}) a[href="/${repo.nameWithOwner}/settings"]`));
875950
};
876951

952+
// eslint-disable-next-line @typescript-eslint/no-deprecated
953+
export const canUserAccessRepoSettings = canUserAdminRepo;
954+
877955
export const isNewRepo = (url: URL | HTMLAnchorElement | Location = location): boolean => !isGist(url) && (url.pathname === '/new' || /^organizations\/[^/]+\/repositories\/new$/.test(processPathname(url)));
878956
TEST: addTests('isNewRepo', [
879957
'https://github.com/new',
@@ -988,4 +1066,5 @@ export const utils = {
9881066
getCleanGistPathname,
9891067
getRepositoryInfo: getRepo,
9901068
parseRepoExplorerTitle,
1069+
waitFor,
9911070
};

package-lock.json

Lines changed: 14 additions & 83 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "github-url-detection",
3-
"version": "11.1.3",
3+
"version": "11.2.0",
44
"description": "Which GitHub page are you on? Is it an issue? Is it a list? Perfect for your WebExtension or userscript.",
55
"keywords": [
66
"github",
@@ -42,7 +42,7 @@
4242
"xo": "xo"
4343
},
4444
"dependencies": {
45-
"github-reserved-names": "^2.1.1"
45+
"github-reserved-names": "^2.1.3"
4646
},
4747
"devDependencies": {
4848
"@sindresorhus/tsconfig": "^8.1.0",
@@ -56,6 +56,7 @@
5656
"svelte-check": "^4.3.5",
5757
"ts-morph": "^27.0.2",
5858
"tsx": "^4.21.0",
59+
"typed-query-selector": "^2.12.0",
5960
"typescript": "5.9.3",
6061
"vite": "^7.3.1",
6162
"vitest": "^4.0.17",

0 commit comments

Comments
 (0)