1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
/**
* Unit tests for openProfileWindow's failure reporting — no browser required.
*
* spawn() reports a missing or non-executable binary asynchronously, as an
* 'error' event, not by throwing. When that event was ignored, a bad executable
* path returned `ok` and the caller's 15s sentinel poll blamed the browser:
* "couldn't open a window in <profile> automatically". These tests pin the
* failure to its real cause instead.
*
* Run: npm test
*/
import { test, before, after, describe } from "node:test";
import assert from "node:assert/strict";
import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { openProfileWindow } from "../../src/profile/launch";
describe("openProfileWindow's failure reporting", () => {
let root: string;
before(() => {
root = mkdtempSync(join(tmpdir(), "pi-launch-"));
});
after(() => {
rmSync(root, { recursive: true, force: true });
});
// L1: a nonexistent binary — the shape of an in-place browser upgrade,
// where /proc/<pid>/exe still names the unlinked file.
describe("L1", () => {
const missing = (): string => join(root, "chrome (deleted)");
test("L1: a missing executable fails the launch", async () => {
const r = await openProfileWindow({ exePath: missing(), profileDir: "Default" });
assert.ok(!r.success);
});
test("L1: the error names the path that could not be launched", async () => {
const r = await openProfileWindow({ exePath: missing(), profileDir: "Default" });
assert.ok(!r.success);
assert.ok(r.error.includes(missing()));
});
test("L1: the error carries spawn's own cause", async () => {
const r = await openProfileWindow({ exePath: missing(), profileDir: "Default" });
assert.ok(!r.success);
assert.ok(r.error.includes("ENOENT"));
});
});
// L2: a file that exists but is not executable — same async 'error' path,
// different errno (EACCES). Skipped where the check is meaningless.
test(
"L2: a non-executable file fails the launch",
{ skip: process.platform === "win32" || process.getuid?.() === 0 },
async () => {
const notExec = join(root, "not-executable");
writeFileSync(notExec, "#!/bin/sh\n", { mode: 0o644 });
const r = await openProfileWindow({ exePath: notExec, profileDir: "Default" });
assert.ok(!r.success);
},
);
// L3: a failed launch leaves no sentinel file behind. The caller only calls
// cleanup() on the success path, so the failure path owns its own tidying.
describe("L3", () => {
let before_: string | undefined;
before(() => {
before_ = process.env["TMPDIR"];
process.env["TMPDIR"] = root;
});
after(() => {
if (before_ === undefined) delete process.env["TMPDIR"];
else process.env["TMPDIR"] = before_;
});
test("L3: launch failed as set up", async () => {
const r = await openProfileWindow({ exePath: join(root, "absent"), profileDir: "Default" });
assert.ok(!r.success);
});
test("L3: no handshake page is left on disk after a failed launch", async () => {
await openProfileWindow({ exePath: join(root, "absent"), profileDir: "Default" });
const leftovers = readdirSync(root).filter((n) => n.startsWith("pi-harness-"));
assert.equal(leftovers.length, 0);
});
});
// L4: a launcher that starts succeeds, and the sentinel it advertises is a
// real, readable file — the page the browser is asked to open.
describe("L4", () => {
let r: Awaited<ReturnType<typeof openProfileWindow>>;
before(async () => {
const stub = join(root, "stub-browser");
writeFileSync(stub, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
r = await openProfileWindow({ exePath: stub, profileDir: "Default" });
});
test("L4: a startable binary yields a handle", () => {
assert.ok(r.success);
});
test("L4: the sentinel page exists before the browser is polled", () => {
assert.ok(r.success);
if (r.success) {
const sentinel = fileURLToPath(r.data.sentinelUrl);
assert.ok(existsSync(sentinel));
}
});
test("L4: cleanup removes the sentinel page", async () => {
assert.ok(r.success);
if (r.success) {
const sentinel = fileURLToPath(r.data.sentinelUrl);
await r.data.cleanup();
assert.ok(!existsSync(sentinel));
}
});
test("L4: kill is safe to call twice", () => {
assert.ok(r.success);
if (r.success) {
r.data.kill();
r.data.kill();
}
});
});
});
|