browser-commander 0.30.0 → 0.30.1
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.30.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- aee418c: Upgrade Command Stream to ^2.0.2 to remove the vulnerable ShellJS/braces dependency chain from fresh consumer installations while preserving subprocess and detached browser launch behavior.
|
|
8
|
+
|
|
3
9
|
## 0.30.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browser-commander",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.1",
|
|
4
4
|
"description": "Universal browser automation library that supports Playwright, Puppeteer, and Selenium with a unified API",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
83
|
"bplist-parser": "^0.5.0",
|
|
84
|
-
"command-stream": "^
|
|
84
|
+
"command-stream": "^2.0.2",
|
|
85
85
|
"csv-parse": "^7.0.3",
|
|
86
86
|
"gifenc": "^1.0.3",
|
|
87
87
|
"links-notation": "^0.23.0",
|
|
@@ -9,7 +9,7 @@ export async function startDetachedProcess(file, args = [], options = {}) {
|
|
|
9
9
|
runner.child.once('error', reject);
|
|
10
10
|
});
|
|
11
11
|
const child = runner.child;
|
|
12
|
-
// command-stream
|
|
12
|
+
// command-stream starts noninteractive children in their own process
|
|
13
13
|
// group. Release its controller-owned cancellation registry while retaining
|
|
14
14
|
// the child's exit listeners. Keep this adapter isolated until it exposes a
|
|
15
15
|
// public detach operation; normal subprocesses retain managed cancellation.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Real launch and capture contracts for the Command Stream upgrade (#164). */
|
|
2
|
+
import { it } from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import UPNG from 'upng-js';
|
|
5
|
+
import {
|
|
6
|
+
screenshot,
|
|
7
|
+
startRecording,
|
|
8
|
+
encodeAnimation,
|
|
9
|
+
encodeVideo,
|
|
10
|
+
launchBrowser,
|
|
11
|
+
} from '../../src/index.js';
|
|
12
|
+
import { CHROME_LAUNCH_OPTIONS } from '../helpers/e2e-browser.js';
|
|
13
|
+
|
|
14
|
+
for (const engine of ['playwright', 'puppeteer', 'selenium']) {
|
|
15
|
+
it(
|
|
16
|
+
`${engine}: screenshots and bounded animation/video capture after a real launch`,
|
|
17
|
+
{ skip: !process.env.RUN_E2E, timeout: 60000 },
|
|
18
|
+
async () => {
|
|
19
|
+
const session = await launchBrowser({
|
|
20
|
+
engine,
|
|
21
|
+
headless: process.env.HEADLESS !== 'false',
|
|
22
|
+
...CHROME_LAUNCH_OPTIONS,
|
|
23
|
+
...(engine === 'selenium' ? { bidi: true } : {}),
|
|
24
|
+
});
|
|
25
|
+
let recording;
|
|
26
|
+
try {
|
|
27
|
+
const { page } = session;
|
|
28
|
+
await page.goto(
|
|
29
|
+
'data:text/html,<input id="focus"><div style="height:2000px;background:teal">Capture fixture</div>'
|
|
30
|
+
);
|
|
31
|
+
const state = () =>
|
|
32
|
+
page.evaluate(() => ({
|
|
33
|
+
scroll: window.scrollY,
|
|
34
|
+
active: document.activeElement.id,
|
|
35
|
+
}));
|
|
36
|
+
await page.evaluate(() => {
|
|
37
|
+
document.querySelector('input').focus();
|
|
38
|
+
window.scrollTo(0, 200);
|
|
39
|
+
});
|
|
40
|
+
const before = await state();
|
|
41
|
+
const png = await screenshot({ page, engine, stableViewport: true });
|
|
42
|
+
assert.equal(png.subarray(1, 4).toString(), 'PNG');
|
|
43
|
+
assert.deepEqual(await state(), before);
|
|
44
|
+
const jpeg = await screenshot({ page, engine, format: 'jpeg' });
|
|
45
|
+
assert.equal(jpeg.readUInt16BE(0), 0xffd8);
|
|
46
|
+
const webp = await screenshot({ page, engine, format: 'webp' });
|
|
47
|
+
assert.equal(webp.subarray(8, 12).toString(), 'WEBP');
|
|
48
|
+
|
|
49
|
+
recording = await startRecording({
|
|
50
|
+
page,
|
|
51
|
+
engine,
|
|
52
|
+
format: 'frames',
|
|
53
|
+
fps: 2,
|
|
54
|
+
maxFrames: 2,
|
|
55
|
+
maxBytes: 1024 * 1024,
|
|
56
|
+
maxDurationMs: 3000,
|
|
57
|
+
size: { width: 120, height: 100 },
|
|
58
|
+
});
|
|
59
|
+
await page.evaluate(() => {
|
|
60
|
+
document.querySelector('div').style.background = 'red';
|
|
61
|
+
});
|
|
62
|
+
const deadline = Date.now() + 3000;
|
|
63
|
+
while (recording.frames.length < 2 && Date.now() < deadline) {
|
|
64
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
65
|
+
}
|
|
66
|
+
const { frames } = await recording.stop();
|
|
67
|
+
assert.equal(frames.length, 2);
|
|
68
|
+
const gif = await encodeAnimation(frames, { format: 'gif', fps: 2 });
|
|
69
|
+
assert.equal(gif.subarray(0, 6).toString(), 'GIF89a');
|
|
70
|
+
const apng = await encodeAnimation(frames, { format: 'apng', fps: 2 });
|
|
71
|
+
assert.equal(UPNG.decode(apng).frames.length, 2);
|
|
72
|
+
const animatedWebp = await encodeAnimation(frames, {
|
|
73
|
+
format: 'webp',
|
|
74
|
+
fps: 2,
|
|
75
|
+
});
|
|
76
|
+
assert.equal(
|
|
77
|
+
animatedWebp.toString('latin1').split('ANMF').length - 1,
|
|
78
|
+
2
|
|
79
|
+
);
|
|
80
|
+
const video = await encodeVideo(frames, {
|
|
81
|
+
page,
|
|
82
|
+
engine,
|
|
83
|
+
format: 'webm',
|
|
84
|
+
fps: 2,
|
|
85
|
+
});
|
|
86
|
+
assert.equal(video.readUInt32BE(0), 0x1a45dfa3);
|
|
87
|
+
} finally {
|
|
88
|
+
try {
|
|
89
|
+
await recording?.stop();
|
|
90
|
+
} finally {
|
|
91
|
+
await session.close();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
3
6
|
import {
|
|
4
7
|
CommandError,
|
|
5
8
|
runCommand,
|
|
6
9
|
startProcess,
|
|
7
10
|
} from '../../../src/utilities/subprocess.js';
|
|
11
|
+
import { repoPath } from '../../helpers/repo.js';
|
|
8
12
|
|
|
9
13
|
const node = process.execPath;
|
|
10
14
|
|
|
11
|
-
describe('runCommand', () => {
|
|
15
|
+
describe('runCommand', { timeout: 10000 }, () => {
|
|
12
16
|
it('passes arguments without a shell and captures output', async () => {
|
|
13
17
|
const result = await runCommand(node, [
|
|
14
18
|
'-e',
|
|
@@ -46,9 +50,25 @@ describe('runCommand', () => {
|
|
|
46
50
|
const result = await runCommand(node, args, { check: false });
|
|
47
51
|
assert.equal(result.code, 3);
|
|
48
52
|
});
|
|
53
|
+
|
|
54
|
+
it('reports a missing executable as exit 127 with ENOENT diagnostics', async () => {
|
|
55
|
+
const file = path.join(tmpdir(), 'bc-missing-executable-164', 'missing');
|
|
56
|
+
await assert.rejects(
|
|
57
|
+
() => runCommand(file),
|
|
58
|
+
(error) => {
|
|
59
|
+
assert.ok(error instanceof CommandError);
|
|
60
|
+
assert.equal(error.code, 127);
|
|
61
|
+
assert.match(error.stderr, /ENOENT/);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
const result = await runCommand(file, [], { check: false });
|
|
66
|
+
assert.equal(result.code, 127);
|
|
67
|
+
assert.match(result.stderr, /ENOENT/);
|
|
68
|
+
});
|
|
49
69
|
});
|
|
50
70
|
|
|
51
|
-
describe('startProcess', () => {
|
|
71
|
+
describe('startProcess', { timeout: 10000 }, () => {
|
|
52
72
|
it('streams output and reports the exit code', async () => {
|
|
53
73
|
const child = startProcess(node, [
|
|
54
74
|
'-e',
|
|
@@ -78,3 +98,61 @@ describe('startProcess', () => {
|
|
|
78
98
|
assert.notEqual(exitCode, 0);
|
|
79
99
|
});
|
|
80
100
|
});
|
|
101
|
+
|
|
102
|
+
describe(
|
|
103
|
+
'startDetachedProcess',
|
|
104
|
+
{
|
|
105
|
+
timeout: 10000,
|
|
106
|
+
// Both the original 1.4.0 and 2.0.2 runner disable Windows detachment.
|
|
107
|
+
skip:
|
|
108
|
+
process.platform === 'win32' &&
|
|
109
|
+
'Command Stream only creates detached process groups on Unix',
|
|
110
|
+
},
|
|
111
|
+
() => {
|
|
112
|
+
it('lets the launcher exit while the detached child survives', async (t) => {
|
|
113
|
+
const directory = await fs.mkdtemp(
|
|
114
|
+
path.join(tmpdir(), 'bc-detached-164-')
|
|
115
|
+
);
|
|
116
|
+
const marker = path.join(directory, 'survived');
|
|
117
|
+
let pid;
|
|
118
|
+
const launcher = startProcess(node, [
|
|
119
|
+
repoPath('experiments', 'issue-164', 'detached-launcher.mjs'),
|
|
120
|
+
marker,
|
|
121
|
+
]);
|
|
122
|
+
t.after(async () => {
|
|
123
|
+
launcher.kill();
|
|
124
|
+
if (pid) {
|
|
125
|
+
try {
|
|
126
|
+
process.kill(pid, 'SIGKILL');
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error.code !== 'ESRCH') {
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
134
|
+
});
|
|
135
|
+
let output = '';
|
|
136
|
+
launcher.stdout.on('data', (chunk) => {
|
|
137
|
+
output += chunk;
|
|
138
|
+
pid = Number(output.trim());
|
|
139
|
+
});
|
|
140
|
+
assert.equal(await launcher.exited, 0, launcher.stderrTail);
|
|
141
|
+
assert.ok(pid > 0);
|
|
142
|
+
await assert.rejects(fs.readFile(marker), { code: 'ENOENT' });
|
|
143
|
+
const deadline = Date.now() + 5000;
|
|
144
|
+
while (Date.now() < deadline) {
|
|
145
|
+
try {
|
|
146
|
+
assert.equal(await fs.readFile(marker, 'utf8'), 'survived');
|
|
147
|
+
return;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error.code !== 'ENOENT') {
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
assert.fail('detached child did not survive the launcher exit');
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
);
|