Skip to content

Commit 7425970

Browse files
authored
fix: reuse opened GUI tab when possible (#748)
1 parent 1c63170 commit 7425970

5 files changed

Lines changed: 206 additions & 39 deletions

File tree

lib/gui/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type {CommanderStatic} from '@gemini-testing/commander';
2-
import opener from 'opener';
32

43
import * as server from './server';
54
import * as utils from '../server-utils';
5+
import {openBrowser} from './open-browser';
66

77
import type {ToolAdapter} from '../adapters/tool';
88

@@ -26,8 +26,10 @@ export interface ServerArgs {
2626

2727
export default (args: ServerArgs): void => {
2828
server.start(args)
29-
.then(({url}: { url: string }) => {
30-
args.cli.options.open && opener(url);
29+
.then(async ({url}: { url: string }) => {
30+
if (args.cli.options.open) {
31+
await openBrowser(url);
32+
}
3133
})
3234
.catch((err: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
3335
logError(err);

lib/gui/open-browser.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* The following is modified based on source found in
3+
* https://github.com/vitejs/vite and https://github.com/facebook/create-react-app
4+
*
5+
* MIT Licensed
6+
* Copyright (c) 2015-present, Facebook, Inc.
7+
*
8+
* Modified for use in html-reporter.
9+
*/
10+
11+
import path from 'node:path';
12+
import {exec} from 'node:child_process';
13+
import type {ExecOptions} from 'node:child_process';
14+
import open from 'open';
15+
16+
const supportedChromiumBrowsers = [
17+
'Google Chrome Canary',
18+
'Google Chrome Dev',
19+
'Google Chrome Beta',
20+
'Google Chrome',
21+
'Microsoft Edge',
22+
'Brave Browser',
23+
'Vivaldi',
24+
'Chromium',
25+
'Yandex'
26+
];
27+
28+
export async function openBrowser(url: string): Promise<boolean> {
29+
// If we're on macOS, we can try opening a Chromium browser with JXA.
30+
// This lets us reuse an existing tab when possible instead of creating a new one.
31+
if (process.platform === 'darwin') {
32+
try {
33+
const ps = await execAsync('ps cax');
34+
const openedBrowser = supportedChromiumBrowsers.find((b) => ps.includes(b));
35+
36+
if (openedBrowser) {
37+
// Try our best to reuse existing tab with JXA
38+
await execAsync(`osascript openChrome.js "${url}" "${openedBrowser}"`, {
39+
cwd: path.join(__dirname)
40+
});
41+
return true;
42+
}
43+
} catch {
44+
// Ignore errors, fall through to regular open
45+
}
46+
}
47+
48+
// Fallback to open (will always open new tab)
49+
try {
50+
await open(url);
51+
return true;
52+
} catch {
53+
return false;
54+
}
55+
}
56+
57+
function execAsync(command: string, options?: ExecOptions): Promise<string> {
58+
return new Promise((resolve, reject) => {
59+
exec(command, options, (error, stdout) => {
60+
if (error) {
61+
reject(error);
62+
} else {
63+
resolve(stdout.toString());
64+
}
65+
});
66+
});
67+
}

lib/gui/openChrome.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
Copyright (c) 2015-present, Facebook, Inc.
3+
4+
This source code is licensed under the MIT license found in the
5+
LICENSE file at
6+
https://github.com/facebook/create-react-app/blob/main/LICENSE
7+
8+
Modified for use in html-reporter, based on Vite's implementation.
9+
*/
10+
11+
/* global Application */
12+
13+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
14+
function run(argv) {
15+
const urlToOpen = argv[0];
16+
// Allow requested program to be optional, default to Google Chrome
17+
const programName = argv[1] ?? 'Google Chrome';
18+
19+
const app = Application(programName);
20+
21+
if (app.windows.length === 0) {
22+
app.Window().make();
23+
}
24+
25+
// 1: Looking for tab running debugger then,
26+
// Reload debugging tab if found, then return
27+
const found = lookupTabWithUrl(urlToOpen, app);
28+
if (found) {
29+
found.targetWindow.activeTabIndex = found.targetTabIndex;
30+
found.targetTab.reload();
31+
found.targetWindow.index = 1;
32+
app.activate();
33+
return;
34+
}
35+
36+
// 2: Looking for Empty tab
37+
// In case debugging tab was not found
38+
// We try to find an empty tab instead
39+
const emptyTabFound = lookupTabWithUrl('chrome://newtab/', app);
40+
if (emptyTabFound) {
41+
emptyTabFound.targetWindow.activeTabIndex = emptyTabFound.targetTabIndex;
42+
emptyTabFound.targetTab.url = urlToOpen;
43+
app.activate();
44+
return;
45+
}
46+
47+
// 3: Create new tab
48+
// both debugging and empty tab were not found make a new tab with url
49+
const firstWindow = app.windows[0];
50+
firstWindow.tabs.push(app.Tab({url: urlToOpen}));
51+
app.activate();
52+
}
53+
54+
/**
55+
* Lookup tab with given url
56+
*/
57+
function lookupTabWithUrl(lookupUrl, app) {
58+
const windows = app.windows();
59+
for (const window of windows) {
60+
for (const [tabIndex, tab] of window.tabs().entries()) {
61+
if (tab.url().includes(lookupUrl)) {
62+
return {
63+
targetTab: tab,
64+
targetTabIndex: tabIndex + 1,
65+
targetWindow: window
66+
};
67+
}
68+
}
69+
}
70+
}

package-lock.json

Lines changed: 63 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@
132132
"looks-same": "^10.0.1",
133133
"nested-error-stacks": "^2.1.0",
134134
"npm-which": "^3.0.1",
135-
"opener": "^1.4.3",
135+
"open": "^8.4.2",
136136
"ora": "^5.4.1",
137137
"p-queue": "^5.0.0",
138138
"qs": "^6.9.1",
@@ -180,7 +180,6 @@
180180
"@types/lodash": "^4.14.195",
181181
"@types/nested-error-stacks": "^2.1.0",
182182
"@types/npm-which": "^3.0.3",
183-
"@types/opener": "^1.4.0",
184183
"@types/proxyquire": "^1.3.28",
185184
"@types/react-dom": "^18.3.0",
186185
"@types/react-virtualized": "^9.21.30",

0 commit comments

Comments
 (0)