@rightkit/qa 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chrome.d.ts +1 -99
- package/dist/chrome.js +1 -357
- package/package.json +2 -1
package/dist/chrome.d.ts
CHANGED
|
@@ -1,99 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Chrome for Testing for repository QA: one resolver, one launcher, one graceful close.
|
|
3
|
-
*
|
|
4
|
-
* Regular Google Chrome on macOS makes an APFS clone of its app bundle per instance
|
|
5
|
-
* (`<DARWIN_USER_TEMP_DIR>/../X/com.google.Chrome.code_sign_clone/`). A normal shutdown
|
|
6
|
-
* removes it; SIGKILL or a crash leaves it behind. Chrome for Testing does not create the
|
|
7
|
-
* clone, and a graceful close (CDP `Browser.close`, then SIGTERM, then SIGKILL last) lets
|
|
8
|
-
* any browser clean up. Harnesses never fall back to regular Chrome silently.
|
|
9
|
-
*
|
|
10
|
-
* This is for repo-owned web/CDP harnesses only. The native lane (`native.ts`) drives the
|
|
11
|
-
* app's own Tauri WebView and is unaffected.
|
|
12
|
-
*/
|
|
13
|
-
import { type ChildProcess } from 'node:child_process';
|
|
14
|
-
/** Environment variable naming an explicit Chrome for Testing executable. */
|
|
15
|
-
export declare const CHROME_FOR_TESTING_ENV = "RIGHTKIT_CHROME_FOR_TESTING";
|
|
16
|
-
export declare const CHROME_FOR_TESTING_INSTALL_HINT: string;
|
|
17
|
-
export declare class ChromeForTestingNotFoundError extends Error {
|
|
18
|
-
readonly searched: string[];
|
|
19
|
-
constructor(message: string, searched: string[]);
|
|
20
|
-
}
|
|
21
|
-
export interface ResolveChromeForTestingOptions {
|
|
22
|
-
/** Explicit executable; wins over the environment and known locations. */
|
|
23
|
-
executablePath?: string;
|
|
24
|
-
env?: NodeJS.ProcessEnv;
|
|
25
|
-
platform?: NodeJS.Platform;
|
|
26
|
-
arch?: string;
|
|
27
|
-
homedir?: string;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Known Chrome for Testing install locations in resolution order (after the explicit option
|
|
31
|
-
* and the environment variable): app installs, then Puppeteer's cache, then Playwright's
|
|
32
|
-
* cache, newest version first within each cache.
|
|
33
|
-
*/
|
|
34
|
-
export declare function chromeForTestingCandidates(options?: ResolveChromeForTestingOptions): string[];
|
|
35
|
-
/**
|
|
36
|
-
* Resolve the Chrome for Testing executable: `executablePath`, then `RIGHTKIT_CHROME_FOR_TESTING`,
|
|
37
|
-
* then the known install locations. Throws {@link ChromeForTestingNotFoundError} with install
|
|
38
|
-
* instructions when none exists; never falls back to regular Chrome.
|
|
39
|
-
*/
|
|
40
|
-
export declare function resolveChromeForTesting(options?: ResolveChromeForTestingOptions): string;
|
|
41
|
-
/** Non-throwing form for test skips: `{ path }` or `{ reason }`. */
|
|
42
|
-
export declare function findChromeForTesting(options?: ResolveChromeForTestingOptions): {
|
|
43
|
-
path?: string;
|
|
44
|
-
reason?: string;
|
|
45
|
-
};
|
|
46
|
-
export interface LaunchChromeOptions extends ResolveChromeForTestingOptions {
|
|
47
|
-
/** Default true (`--headless=new`). */
|
|
48
|
-
headless?: boolean;
|
|
49
|
-
/** Default true. */
|
|
50
|
-
muteAudio?: boolean;
|
|
51
|
-
/** Extra flags. The launcher owns `--user-data-dir` and `--remote-debugging-port`. */
|
|
52
|
-
args?: string[];
|
|
53
|
-
/** Temp profile directory prefix. */
|
|
54
|
-
profilePrefix?: string;
|
|
55
|
-
/** Initial URL. Default `about:blank`. */
|
|
56
|
-
url?: string;
|
|
57
|
-
/** DevTools endpoint wait. Default 20s. */
|
|
58
|
-
startTimeoutMs?: number;
|
|
59
|
-
}
|
|
60
|
-
export type ChromeCloseStep = 'cdp' | 'sigterm' | 'sigkill' | 'already-exited';
|
|
61
|
-
export interface ChromeCloseReport {
|
|
62
|
-
/** The step after which the whole browser process tree was gone. */
|
|
63
|
-
step: ChromeCloseStep;
|
|
64
|
-
exited: boolean;
|
|
65
|
-
profileRemoved: boolean;
|
|
66
|
-
durationMs: number;
|
|
67
|
-
}
|
|
68
|
-
export interface CloseChromeOptions {
|
|
69
|
-
/** Per-step grace before escalating. Default 5000 ms. */
|
|
70
|
-
graceMs?: number;
|
|
71
|
-
}
|
|
72
|
-
export interface ChromeForTestingBrowser {
|
|
73
|
-
executablePath: string;
|
|
74
|
-
process: ChildProcess;
|
|
75
|
-
pid: number;
|
|
76
|
-
/** Browser-level DevTools WebSocket URL. */
|
|
77
|
-
wsEndpoint: string;
|
|
78
|
-
port: number;
|
|
79
|
-
/** Harness-owned temp profile, removed by {@link ChromeForTestingBrowser.close}. */
|
|
80
|
-
profileDir: string;
|
|
81
|
-
/** Graceful close; idempotent. */
|
|
82
|
-
close(options?: CloseChromeOptions): Promise<ChromeCloseReport>;
|
|
83
|
-
}
|
|
84
|
-
/** Launch Chrome for Testing with a fresh temp profile and a DevTools endpoint. */
|
|
85
|
-
export declare function launchChromeForTesting(options?: LaunchChromeOptions): Promise<ChromeForTestingBrowser>;
|
|
86
|
-
/**
|
|
87
|
-
* Close a Chrome process tree gracefully: CDP `Browser.close`, wait; SIGTERM the process
|
|
88
|
-
* group, wait; SIGKILL only last. Then remove the harness-owned profile directory.
|
|
89
|
-
*/
|
|
90
|
-
export declare function closeChromeGracefully(options: {
|
|
91
|
-
child: ChildProcess;
|
|
92
|
-
wsEndpoint?: string;
|
|
93
|
-
profileDir?: string;
|
|
94
|
-
graceMs?: number;
|
|
95
|
-
}): Promise<ChromeCloseReport>;
|
|
96
|
-
/** macOS per-user `X` directories that hold browser code-sign clones (deduplicated). */
|
|
97
|
-
export declare function codeSignCloneRoots(): string[];
|
|
98
|
-
/** Every entry under every `*.code_sign_clone` directory (e.g. `com.google.Chrome.code_sign_clone`). */
|
|
99
|
-
export declare function codeSignCloneEntries(): string[];
|
|
1
|
+
export * from '@rightkit/qa-chrome';
|
package/dist/chrome.js
CHANGED
|
@@ -1,357 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Chrome for Testing for repository QA: one resolver, one launcher, one graceful close.
|
|
3
|
-
*
|
|
4
|
-
* Regular Google Chrome on macOS makes an APFS clone of its app bundle per instance
|
|
5
|
-
* (`<DARWIN_USER_TEMP_DIR>/../X/com.google.Chrome.code_sign_clone/`). A normal shutdown
|
|
6
|
-
* removes it; SIGKILL or a crash leaves it behind. Chrome for Testing does not create the
|
|
7
|
-
* clone, and a graceful close (CDP `Browser.close`, then SIGTERM, then SIGKILL last) lets
|
|
8
|
-
* any browser clean up. Harnesses never fall back to regular Chrome silently.
|
|
9
|
-
*
|
|
10
|
-
* This is for repo-owned web/CDP harnesses only. The native lane (`native.ts`) drives the
|
|
11
|
-
* app's own Tauri WebView and is unaffected.
|
|
12
|
-
*/
|
|
13
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
14
|
-
import { existsSync, mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
15
|
-
import { homedir, tmpdir } from 'node:os';
|
|
16
|
-
import path from 'node:path';
|
|
17
|
-
/** Environment variable naming an explicit Chrome for Testing executable. */
|
|
18
|
-
export const CHROME_FOR_TESTING_ENV = 'RIGHTKIT_CHROME_FOR_TESTING';
|
|
19
|
-
export const CHROME_FOR_TESTING_INSTALL_HINT = [
|
|
20
|
-
'Install Chrome for Testing (any one of):',
|
|
21
|
-
' pnpm dlx @puppeteer/browsers install chrome@stable --path ~/.cache/puppeteer',
|
|
22
|
-
' pnpm exec playwright install chromium',
|
|
23
|
-
`or point ${CHROME_FOR_TESTING_ENV} at its executable, e.g.`,
|
|
24
|
-
` ${CHROME_FOR_TESTING_ENV}="$HOME/.cache/puppeteer/chrome/mac_arm-<version>/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"`,
|
|
25
|
-
'Regular Google Chrome is never used as a fallback: on macOS each killed instance leaks a code-sign clone.',
|
|
26
|
-
].join('\n');
|
|
27
|
-
export class ChromeForTestingNotFoundError extends Error {
|
|
28
|
-
searched;
|
|
29
|
-
constructor(message, searched) {
|
|
30
|
-
super(`${message}\nSearched:\n${searched.map((p) => ` ${p}`).join('\n') || ' (nothing)'}\n${CHROME_FOR_TESTING_INSTALL_HINT}`);
|
|
31
|
-
this.name = 'ChromeForTestingNotFoundError';
|
|
32
|
-
this.searched = searched;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
const MAC_APP = 'Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing';
|
|
36
|
-
/** Numeric sort key from a versioned cache dir name (`mac_arm-152.0.7977.75`, `chromium-1243`). */
|
|
37
|
-
function versionKey(name) {
|
|
38
|
-
return (name.match(/\d+/g) ?? []).map(Number);
|
|
39
|
-
}
|
|
40
|
-
function newestFirst(a, b) {
|
|
41
|
-
const ka = versionKey(a);
|
|
42
|
-
const kb = versionKey(b);
|
|
43
|
-
for (let i = 0; i < Math.max(ka.length, kb.length); i++) {
|
|
44
|
-
const d = (kb[i] ?? -1) - (ka[i] ?? -1);
|
|
45
|
-
if (d !== 0)
|
|
46
|
-
return d;
|
|
47
|
-
}
|
|
48
|
-
return b.localeCompare(a);
|
|
49
|
-
}
|
|
50
|
-
function versionedChildren(root, prefix) {
|
|
51
|
-
try {
|
|
52
|
-
return readdirSync(root).filter((name) => prefix.test(name)).sort(newestFirst).map((name) => path.join(root, name));
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Known Chrome for Testing install locations in resolution order (after the explicit option
|
|
60
|
-
* and the environment variable): app installs, then Puppeteer's cache, then Playwright's
|
|
61
|
-
* cache, newest version first within each cache.
|
|
62
|
-
*/
|
|
63
|
-
export function chromeForTestingCandidates(options = {}) {
|
|
64
|
-
const env = options.env ?? process.env;
|
|
65
|
-
const platform = options.platform ?? process.platform;
|
|
66
|
-
const arch = options.arch ?? process.arch;
|
|
67
|
-
const home = options.homedir ?? homedir();
|
|
68
|
-
const puppeteerRoot = path.join(env.PUPPETEER_CACHE_DIR || path.join(home, '.cache', 'puppeteer'), 'chrome');
|
|
69
|
-
const out = [];
|
|
70
|
-
if (platform === 'darwin') {
|
|
71
|
-
out.push(path.join('/Applications', MAC_APP), path.join(home, 'Applications', MAC_APP));
|
|
72
|
-
const macDirs = arch === 'arm64' ? ['chrome-mac-arm64', 'chrome-mac-x64'] : ['chrome-mac-x64', 'chrome-mac-arm64'];
|
|
73
|
-
for (const dir of versionedChildren(puppeteerRoot, /^mac(_arm)?-/)) {
|
|
74
|
-
for (const sub of macDirs)
|
|
75
|
-
out.push(path.join(dir, sub, MAC_APP));
|
|
76
|
-
}
|
|
77
|
-
const playwrightRoot = env.PLAYWRIGHT_BROWSERS_PATH || path.join(home, 'Library', 'Caches', 'ms-playwright');
|
|
78
|
-
for (const dir of versionedChildren(playwrightRoot, /^chromium-\d/)) {
|
|
79
|
-
for (const sub of macDirs)
|
|
80
|
-
out.push(path.join(dir, sub, MAC_APP));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
else if (platform === 'win32') {
|
|
84
|
-
const local = env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
85
|
-
for (const dir of versionedChildren(puppeteerRoot, /^win(32|64)-/)) {
|
|
86
|
-
out.push(path.join(dir, 'chrome-win64', 'chrome.exe'), path.join(dir, 'chrome-win32', 'chrome.exe'));
|
|
87
|
-
}
|
|
88
|
-
const playwrightRoot = env.PLAYWRIGHT_BROWSERS_PATH || path.join(local, 'ms-playwright');
|
|
89
|
-
for (const dir of versionedChildren(playwrightRoot, /^chromium-\d/)) {
|
|
90
|
-
out.push(path.join(dir, 'chrome-win64', 'chrome.exe'));
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
for (const dir of versionedChildren(puppeteerRoot, /^linux(_arm)?-/)) {
|
|
95
|
-
out.push(path.join(dir, 'chrome-linux64', 'chrome'), path.join(dir, 'chrome-linux-arm64', 'chrome'));
|
|
96
|
-
}
|
|
97
|
-
const playwrightRoot = env.PLAYWRIGHT_BROWSERS_PATH || path.join(home, '.cache', 'ms-playwright');
|
|
98
|
-
for (const dir of versionedChildren(playwrightRoot, /^chromium-\d/)) {
|
|
99
|
-
out.push(path.join(dir, 'chrome-linux64', 'chrome'));
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return out;
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Resolve the Chrome for Testing executable: `executablePath`, then `RIGHTKIT_CHROME_FOR_TESTING`,
|
|
106
|
-
* then the known install locations. Throws {@link ChromeForTestingNotFoundError} with install
|
|
107
|
-
* instructions when none exists; never falls back to regular Chrome.
|
|
108
|
-
*/
|
|
109
|
-
export function resolveChromeForTesting(options = {}) {
|
|
110
|
-
const env = options.env ?? process.env;
|
|
111
|
-
if (options.executablePath) {
|
|
112
|
-
if (existsSync(options.executablePath))
|
|
113
|
-
return options.executablePath;
|
|
114
|
-
throw new ChromeForTestingNotFoundError(`explicit Chrome for Testing executable does not exist: ${options.executablePath}`, [options.executablePath]);
|
|
115
|
-
}
|
|
116
|
-
const fromEnv = env[CHROME_FOR_TESTING_ENV];
|
|
117
|
-
if (fromEnv) {
|
|
118
|
-
if (existsSync(fromEnv))
|
|
119
|
-
return fromEnv;
|
|
120
|
-
throw new ChromeForTestingNotFoundError(`${CHROME_FOR_TESTING_ENV} points at a missing file: ${fromEnv}`, [fromEnv]);
|
|
121
|
-
}
|
|
122
|
-
const candidates = chromeForTestingCandidates(options);
|
|
123
|
-
const found = candidates.find((candidate) => existsSync(candidate));
|
|
124
|
-
if (found)
|
|
125
|
-
return found;
|
|
126
|
-
throw new ChromeForTestingNotFoundError('Chrome for Testing is not installed', candidates);
|
|
127
|
-
}
|
|
128
|
-
/** Non-throwing form for test skips: `{ path }` or `{ reason }`. */
|
|
129
|
-
export function findChromeForTesting(options = {}) {
|
|
130
|
-
try {
|
|
131
|
-
return { path: resolveChromeForTesting(options) };
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
return { reason: error.message };
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
const MANAGED_FLAGS = /^--(user-data-dir|remote-debugging-port|remote-debugging-pipe)(=|$)/;
|
|
138
|
-
/** Launch Chrome for Testing with a fresh temp profile and a DevTools endpoint. */
|
|
139
|
-
export async function launchChromeForTesting(options = {}) {
|
|
140
|
-
const executablePath = resolveChromeForTesting(options);
|
|
141
|
-
const extra = options.args ?? [];
|
|
142
|
-
const managed = extra.find((arg) => MANAGED_FLAGS.test(arg));
|
|
143
|
-
if (managed)
|
|
144
|
-
throw new Error(`${managed} is managed by launchChromeForTesting`);
|
|
145
|
-
const profileDir = mkdtempSync(path.join(tmpdir(), options.profilePrefix ?? 'rk-qa-chrome-'));
|
|
146
|
-
const args = [
|
|
147
|
-
...(options.headless === false ? [] : ['--headless=new']),
|
|
148
|
-
...(options.muteAudio === false ? [] : ['--mute-audio']),
|
|
149
|
-
'--no-first-run',
|
|
150
|
-
'--no-default-browser-check',
|
|
151
|
-
'--disable-breakpad',
|
|
152
|
-
'--password-store=basic',
|
|
153
|
-
'--use-mock-keychain',
|
|
154
|
-
...extra,
|
|
155
|
-
`--user-data-dir=${profileDir}`,
|
|
156
|
-
'--remote-debugging-port=0',
|
|
157
|
-
options.url ?? 'about:blank',
|
|
158
|
-
];
|
|
159
|
-
// Own process group on Unix so the whole tree can be signalled together.
|
|
160
|
-
const child = spawn(executablePath, args, {
|
|
161
|
-
stdio: ['ignore', 'ignore', 'pipe'],
|
|
162
|
-
detached: process.platform !== 'win32',
|
|
163
|
-
windowsHide: true,
|
|
164
|
-
});
|
|
165
|
-
const pid = child.pid;
|
|
166
|
-
if (!pid) {
|
|
167
|
-
rmSync(profileDir, { recursive: true, force: true });
|
|
168
|
-
throw new Error(`failed to start Chrome for Testing: ${executablePath}`);
|
|
169
|
-
}
|
|
170
|
-
let closing;
|
|
171
|
-
// A harness that exits without closing still asks Chrome to shut down gracefully.
|
|
172
|
-
const onHostExit = () => signalTree(child, 'SIGTERM');
|
|
173
|
-
process.once('exit', onHostExit);
|
|
174
|
-
const close = (closeOptions = {}) => {
|
|
175
|
-
process.removeListener('exit', onHostExit);
|
|
176
|
-
closing ??= closeChromeGracefully({ child, wsEndpoint, profileDir, ...closeOptions });
|
|
177
|
-
return closing;
|
|
178
|
-
};
|
|
179
|
-
let wsEndpoint;
|
|
180
|
-
try {
|
|
181
|
-
wsEndpoint = await new Promise((resolve, reject) => {
|
|
182
|
-
const timer = setTimeout(() => reject(new Error('Chrome for Testing did not expose a DevTools endpoint')), options.startTimeoutMs ?? 20_000);
|
|
183
|
-
let buf = '';
|
|
184
|
-
child.stderr.setEncoding('utf8');
|
|
185
|
-
// Keep draining after the endpoint line so Chrome never blocks on a full pipe.
|
|
186
|
-
child.stderr.on('data', (chunk) => {
|
|
187
|
-
if (wsEndpoint)
|
|
188
|
-
return;
|
|
189
|
-
buf += chunk;
|
|
190
|
-
const match = buf.match(/DevTools listening on (ws:\/\/\S+)/);
|
|
191
|
-
if (match) {
|
|
192
|
-
clearTimeout(timer);
|
|
193
|
-
resolve(match[1]);
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
child.once('exit', (code, signal) => {
|
|
197
|
-
clearTimeout(timer);
|
|
198
|
-
reject(new Error(`Chrome for Testing exited early (code ${code}, signal ${signal})\n${buf.slice(-2000)}`));
|
|
199
|
-
});
|
|
200
|
-
child.once('error', (error) => {
|
|
201
|
-
clearTimeout(timer);
|
|
202
|
-
reject(error);
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
catch (error) {
|
|
207
|
-
await close();
|
|
208
|
-
throw error;
|
|
209
|
-
}
|
|
210
|
-
return {
|
|
211
|
-
executablePath,
|
|
212
|
-
process: child,
|
|
213
|
-
pid,
|
|
214
|
-
wsEndpoint,
|
|
215
|
-
port: Number(new URL(wsEndpoint).port),
|
|
216
|
-
profileDir,
|
|
217
|
-
close,
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
function treeAlive(child) {
|
|
221
|
-
const pid = child.pid;
|
|
222
|
-
if (!pid)
|
|
223
|
-
return false;
|
|
224
|
-
if (process.platform === 'win32')
|
|
225
|
-
return child.exitCode === null && child.signalCode === null;
|
|
226
|
-
try {
|
|
227
|
-
process.kill(-pid, 0);
|
|
228
|
-
return true;
|
|
229
|
-
}
|
|
230
|
-
catch (error) {
|
|
231
|
-
return error.code === 'EPERM';
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
async function waitGone(child, timeoutMs) {
|
|
235
|
-
const deadline = Date.now() + timeoutMs;
|
|
236
|
-
while (treeAlive(child)) {
|
|
237
|
-
if (Date.now() >= deadline)
|
|
238
|
-
return false;
|
|
239
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
240
|
-
}
|
|
241
|
-
return true;
|
|
242
|
-
}
|
|
243
|
-
function signalTree(child, signal) {
|
|
244
|
-
const pid = child.pid;
|
|
245
|
-
if (!pid)
|
|
246
|
-
return;
|
|
247
|
-
if (process.platform === 'win32') {
|
|
248
|
-
spawnSync('taskkill.exe', ['/PID', String(pid), '/T', ...(signal === 'SIGKILL' ? ['/F'] : [])], { stdio: 'ignore', windowsHide: true });
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
try {
|
|
252
|
-
process.kill(-pid, signal);
|
|
253
|
-
}
|
|
254
|
-
catch {
|
|
255
|
-
try {
|
|
256
|
-
child.kill(signal);
|
|
257
|
-
}
|
|
258
|
-
catch { /* gone */ }
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
async function sendBrowserClose(wsEndpoint, timeoutMs) {
|
|
262
|
-
await new Promise((resolve) => {
|
|
263
|
-
let ws;
|
|
264
|
-
const done = () => {
|
|
265
|
-
clearTimeout(timer);
|
|
266
|
-
try {
|
|
267
|
-
ws.close();
|
|
268
|
-
}
|
|
269
|
-
catch { /* closed */ }
|
|
270
|
-
resolve();
|
|
271
|
-
};
|
|
272
|
-
const timer = setTimeout(done, timeoutMs);
|
|
273
|
-
try {
|
|
274
|
-
ws = new WebSocket(wsEndpoint);
|
|
275
|
-
}
|
|
276
|
-
catch {
|
|
277
|
-
clearTimeout(timer);
|
|
278
|
-
resolve();
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
ws.onopen = () => ws.send(JSON.stringify({ id: 1, method: 'Browser.close' }));
|
|
282
|
-
ws.onmessage = done;
|
|
283
|
-
ws.onclose = done;
|
|
284
|
-
ws.onerror = done;
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Close a Chrome process tree gracefully: CDP `Browser.close`, wait; SIGTERM the process
|
|
289
|
-
* group, wait; SIGKILL only last. Then remove the harness-owned profile directory.
|
|
290
|
-
*/
|
|
291
|
-
export async function closeChromeGracefully(options) {
|
|
292
|
-
const started = Date.now();
|
|
293
|
-
const grace = options.graceMs ?? 5_000;
|
|
294
|
-
let step = 'already-exited';
|
|
295
|
-
let exited = !treeAlive(options.child);
|
|
296
|
-
if (!exited && options.wsEndpoint) {
|
|
297
|
-
step = 'cdp';
|
|
298
|
-
await sendBrowserClose(options.wsEndpoint, Math.min(grace, 2_000));
|
|
299
|
-
exited = await waitGone(options.child, grace);
|
|
300
|
-
}
|
|
301
|
-
if (!exited) {
|
|
302
|
-
step = 'sigterm';
|
|
303
|
-
signalTree(options.child, 'SIGTERM');
|
|
304
|
-
exited = await waitGone(options.child, grace);
|
|
305
|
-
}
|
|
306
|
-
if (!exited) {
|
|
307
|
-
step = 'sigkill';
|
|
308
|
-
signalTree(options.child, 'SIGKILL');
|
|
309
|
-
exited = await waitGone(options.child, 2_000);
|
|
310
|
-
}
|
|
311
|
-
let profileRemoved = true;
|
|
312
|
-
if (options.profileDir) {
|
|
313
|
-
rmSync(options.profileDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
314
|
-
profileRemoved = !existsSync(options.profileDir);
|
|
315
|
-
}
|
|
316
|
-
return { step, exited, profileRemoved, durationMs: Date.now() - started };
|
|
317
|
-
}
|
|
318
|
-
/** macOS per-user `X` directories that hold browser code-sign clones (deduplicated). */
|
|
319
|
-
export function codeSignCloneRoots() {
|
|
320
|
-
if (process.platform !== 'darwin')
|
|
321
|
-
return [];
|
|
322
|
-
const temps = new Set();
|
|
323
|
-
const conf = spawnSync('getconf', ['DARWIN_USER_TEMP_DIR'], { encoding: 'utf8' });
|
|
324
|
-
if (conf.status === 0 && conf.stdout.trim())
|
|
325
|
-
temps.add(path.resolve(conf.stdout.trim()));
|
|
326
|
-
if (process.env.TMPDIR)
|
|
327
|
-
temps.add(path.resolve(process.env.TMPDIR));
|
|
328
|
-
return [...new Set([...temps].map((t) => path.join(path.dirname(t), 'X')))].filter((x) => {
|
|
329
|
-
try {
|
|
330
|
-
return statSync(x).isDirectory();
|
|
331
|
-
}
|
|
332
|
-
catch {
|
|
333
|
-
return false;
|
|
334
|
-
}
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
/** Every entry under every `*.code_sign_clone` directory (e.g. `com.google.Chrome.code_sign_clone`). */
|
|
338
|
-
export function codeSignCloneEntries() {
|
|
339
|
-
const out = [];
|
|
340
|
-
for (const root of codeSignCloneRoots()) {
|
|
341
|
-
let names = [];
|
|
342
|
-
try {
|
|
343
|
-
names = readdirSync(root);
|
|
344
|
-
}
|
|
345
|
-
catch {
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
for (const dir of names.filter((n) => n.endsWith('code_sign_clone'))) {
|
|
349
|
-
try {
|
|
350
|
-
for (const entry of readdirSync(path.join(root, dir)))
|
|
351
|
-
out.push(path.join(root, dir, entry));
|
|
352
|
-
}
|
|
353
|
-
catch { /* unreadable */ }
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
return out.sort();
|
|
357
|
-
}
|
|
1
|
+
export * from '@rightkit/qa-chrome';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/qa",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Shared native QA runner, evidence, ownership, and release-backdoor guards for Right Suite apps.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
],
|
|
28
28
|
"sideEffects": false,
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"@rightkit/qa-chrome": "0.1.0",
|
|
30
31
|
"@wdio/cli": "9.27.1",
|
|
31
32
|
"@wdio/local-runner": "9.27.1",
|
|
32
33
|
"@wdio/mocha-framework": "9.27.1",
|