Skip to content

Commit 9d99d98

Browse files
committed
e2e: resolve extension id via CDP with chrome://extensions fallback
1 parent 52ab35a commit 9d99d98

1 file changed

Lines changed: 64 additions & 14 deletions

File tree

client/tests/e2e/utils/extension-path.ts

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,57 @@
22
* Returns the Chrome/Firefox extension base URL:
33
* - Chrome: chrome-extension://<id>
44
* - Firefox: moz-extension://<uuid>
5+
*
6+
* CI fix:
7+
* Prefer CDP Target.getTargets (works in headless CI) and fall back to chrome://extensions scraping.
58
*/
69

10+
import { URL } from 'node:url';
11+
712
let cachedChromeExtensionPath: string | null = null;
813

9-
export const getChromeExtensionPath = async (browser: WebdriverIO.Browser) => {
10-
if (cachedChromeExtensionPath) return cachedChromeExtensionPath;
14+
type TargetInfo = { type: string; url: string; title?: string };
15+
type GetTargetsResult = { targetInfos?: TargetInfo[] };
1116

17+
function hostFromUrl(raw: string): string | null {
18+
try {
19+
return new URL(raw).host || null;
20+
} catch {
21+
return null;
22+
}
23+
}
24+
25+
async function tryResolveChromeExtensionIdViaCdp(browser: WebdriverIO.Browser): Promise<string | null> {
26+
// Allow explicit override (useful if multiple extensions are loaded).
27+
const forced = (process.env.RC_EXTENSION_ID ?? process.env.EXTENSION_ID ?? '').trim();
28+
if (forced) return forced;
29+
30+
// browser.cdp may not be available depending on WDIO services/driver; treat as optional.
31+
const cdp = (browser as any)?.cdp;
32+
if (typeof cdp !== 'function') return null;
33+
34+
try {
35+
const res = (await (browser as any).cdp('Target', 'getTargets', {})) as GetTargetsResult;
36+
const infos = res.targetInfos ?? [];
37+
38+
const extTargets = infos
39+
.filter(t => typeof t.url === 'string' && t.url.startsWith('chrome-extension://'))
40+
.map(t => ({ ...t, id: hostFromUrl(t.url) }))
41+
.filter(t => !!t.id) as Array<TargetInfo & { id: string }>;
42+
43+
// Prefer MV3 service worker pointing to background.js
44+
const preferred =
45+
extTargets.find(t => t.type === 'service_worker' && /\/background\.js(\?|$)/.test(t.url)) ??
46+
extTargets.find(t => t.type === 'service_worker') ??
47+
extTargets[0];
48+
49+
return preferred?.id ?? null;
50+
} catch {
51+
return null;
52+
}
53+
}
54+
55+
async function resolveChromeExtensionIdViaExtensionsPage(browser: WebdriverIO.Browser): Promise<string> {
1256
const originalHandle = await browser.getWindowHandle();
1357

1458
// Open extensions manager in a TAB (not a new window), so it’s less disruptive.
@@ -17,14 +61,11 @@ export const getChromeExtensionPath = async (browser: WebdriverIO.Browser) => {
1761
const mgrHandle = await browser.getWindowHandle();
1862
await browser.switchToWindow(mgrHandle);
1963

20-
// Wait for the extensions UI to exist.
2164
await browser.waitUntil(async () => (await $('extensions-manager').isExisting()) === true, {
22-
timeout: 10000,
65+
timeout: 10_000,
2366
timeoutMsg: 'chrome://extensions did not load extensions-manager',
2467
});
2568

26-
// Find an extensions-item. If you have multiple extensions loaded in this profile,
27-
// you should filter by name; otherwise the first item is typically your unpacked extension.
2869
const extensionId = await (async () => {
2970
const extensionsManager = await $('extensions-manager').getElement();
3071

@@ -36,27 +77,36 @@ export const getChromeExtensionPath = async (browser: WebdriverIO.Browser) => {
3677
})();
3778

3879
if (!extensionId) {
39-
// Best-effort diagnostics
4080
const debug = await browser.execute(() => {
4181
const mgr = document.querySelector('extensions-manager') as any;
4282
if (!mgr?.shadowRoot) return { hasManager: !!mgr, hasShadowRoot: false };
4383
const itemList = mgr.shadowRoot.querySelector('#viewManager > extensions-item-list') as any;
44-
return {
45-
hasManager: true,
46-
hasShadowRoot: true,
47-
hasItemList: !!itemList,
48-
};
84+
return { hasManager: true, hasShadowRoot: true, hasItemList: !!itemList };
4985
});
5086

5187
throw new Error(`Extension ID not found on chrome://extensions. Debug: ${JSON.stringify(debug)}`);
5288
}
5389

54-
cachedChromeExtensionPath = `chrome-extension://${extensionId}`;
55-
5690
// Close chrome://extensions tab and go back
5791
await browser.closeWindow();
5892
await browser.switchToWindow(originalHandle);
5993

94+
return extensionId;
95+
}
96+
97+
export const getChromeExtensionPath = async (browser: WebdriverIO.Browser) => {
98+
if (cachedChromeExtensionPath) return cachedChromeExtensionPath;
99+
100+
// CI-safe path first
101+
const cdpId = await tryResolveChromeExtensionIdViaCdp(browser);
102+
if (cdpId) {
103+
cachedChromeExtensionPath = `chrome-extension://${cdpId}`;
104+
return cachedChromeExtensionPath;
105+
}
106+
107+
// Fallback for local environments where CDP might not be available
108+
const uiId = await resolveChromeExtensionIdViaExtensionsPage(browser);
109+
cachedChromeExtensionPath = `chrome-extension://${uiId}`;
60110
return cachedChromeExtensionPath;
61111
};
62112

0 commit comments

Comments
 (0)