Skip to content

Commit 6576488

Browse files
committed
fix(open): derive the dry-run URL via resolvePortalUrl and handle async spawn ENOENT
1 parent fc44d5b commit 6576488

3 files changed

Lines changed: 41 additions & 2 deletions

File tree

src/commands/test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3880,8 +3880,14 @@ export async function runOpen(
38803880

38813881
if (opts.dryRun) {
38823882
emitDryRunBanner(stderrFn);
3883+
// Derive the sample through the SAME resolver as the live path (against
3884+
// the canonical prod endpoint and the dry-run project id) so the two can
3885+
// never drift; the ?? arm is unreachable for the prod mapping but keeps
3886+
// the type total.
38833887
const sample = {
3884-
dashboardUrl: `https://www.testsprite.com/dashboard/tests/p_dryrun_2026/test/${opts.testId}`,
3888+
dashboardUrl:
3889+
resolvePortalUrl('https://api.testsprite.com', 'p_dryrun_2026', opts.testId) ??
3890+
`https://www.testsprite.com/dashboard/tests/p_dryrun_2026/test/${encodeURIComponent(opts.testId)}`,
38853891
};
38863892
out.print(sample, data => (data as { dashboardUrl: string }).dashboardUrl);
38873893
return sample;

src/lib/browser.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,34 @@ describe('openInBrowser', () => {
6161

6262
it('spawns detached, ignores stdio, and unrefs the child', () => {
6363
const unref = vi.fn();
64-
spawnMock.mockReturnValue({ unref });
64+
spawnMock.mockReturnValue({ unref, on: vi.fn() });
6565
openInBrowser(url, { platform: 'darwin' });
6666
expect(spawnMock).toHaveBeenCalledWith('open', [url], { detached: true, stdio: 'ignore' });
6767
expect(unref).toHaveBeenCalledTimes(1);
6868
});
69+
70+
it("handles the child's async 'error' (missing binary) with a stderr hint, not a crash", () => {
71+
// spawn() reports ENOENT asynchronously on the child; an unhandled
72+
// 'error' event would crash the CLI. The default spawner must register
73+
// a listener that degrades to the manual-open hint.
74+
const listeners = new Map<string, (err: Error) => void>();
75+
spawnMock.mockReturnValue({
76+
unref: vi.fn(),
77+
on: (event: string, listener: (err: Error) => void) => {
78+
listeners.set(event, listener);
79+
},
80+
});
81+
const stderrSpy = vi.spyOn(process.stderr, 'write').mockReturnValue(true);
82+
try {
83+
openInBrowser(url, { platform: 'linux' });
84+
const onError = listeners.get('error');
85+
expect(onError).toBeDefined();
86+
// Firing the listener must not throw and must print the hint.
87+
expect(() => onError!(new Error('spawn xdg-open ENOENT'))).not.toThrow();
88+
expect(String(stderrSpy.mock.calls.at(-1)?.[0])).toContain('could not launch a browser');
89+
} finally {
90+
stderrSpy.mockRestore();
91+
}
92+
});
6993
});
7094
});

src/lib/browser.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ export function openInBrowser(url: string, deps: OpenInBrowserDeps = {}): void {
3131
deps.exec ??
3232
((command: string, args: readonly string[]) => {
3333
const child = spawn(command, [...args], { detached: true, stdio: 'ignore' });
34+
// spawn() reports a missing binary (ENOENT) ASYNCHRONOUSLY on the child,
35+
// so the caller's try/catch cannot see it — and an unhandled 'error'
36+
// event would crash the whole CLI. The URL is already on stdout, so
37+
// degrade to the same manual-open hint the sync failure path prints.
38+
child.on('error', () => {
39+
process.stderr.write(
40+
'could not launch a browser; open the URL above manually (or use --no-browser)\n',
41+
);
42+
});
3443
child.unref();
3544
});
3645

0 commit comments

Comments
 (0)