From 63cc4e7d6b1f95188a7cc12052fdf6bb53731e9c Mon Sep 17 00:00:00 2001 From: shadowusr Date: Tue, 13 Jan 2026 18:58:36 +0300 Subject: [PATCH] fix: reuse opened GUI tab when possible --- lib/gui/index.ts | 8 ++-- lib/gui/open-browser.ts | 67 ++++++++++++++++++++++++++++ lib/gui/openChrome.js | 70 +++++++++++++++++++++++++++++ package-lock.json | 97 ++++++++++++++++++++++++++--------------- package.json | 3 +- 5 files changed, 206 insertions(+), 39 deletions(-) create mode 100644 lib/gui/open-browser.ts create mode 100644 lib/gui/openChrome.js diff --git a/lib/gui/index.ts b/lib/gui/index.ts index 90239c104..548c6716e 100644 --- a/lib/gui/index.ts +++ b/lib/gui/index.ts @@ -1,8 +1,8 @@ import type {CommanderStatic} from '@gemini-testing/commander'; -import opener from 'opener'; import * as server from './server'; import * as utils from '../server-utils'; +import {openBrowser} from './open-browser'; import type {ToolAdapter} from '../adapters/tool'; @@ -26,8 +26,10 @@ export interface ServerArgs { export default (args: ServerArgs): void => { server.start(args) - .then(({url}: { url: string }) => { - args.cli.options.open && opener(url); + .then(async ({url}: { url: string }) => { + if (args.cli.options.open) { + await openBrowser(url); + } }) .catch((err: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any logError(err); diff --git a/lib/gui/open-browser.ts b/lib/gui/open-browser.ts new file mode 100644 index 000000000..00479255b --- /dev/null +++ b/lib/gui/open-browser.ts @@ -0,0 +1,67 @@ +/** + * The following is modified based on source found in + * https://github.com/vitejs/vite and https://github.com/facebook/create-react-app + * + * MIT Licensed + * Copyright (c) 2015-present, Facebook, Inc. + * + * Modified for use in html-reporter. + */ + +import path from 'node:path'; +import {exec} from 'node:child_process'; +import type {ExecOptions} from 'node:child_process'; +import open from 'open'; + +const supportedChromiumBrowsers = [ + 'Google Chrome Canary', + 'Google Chrome Dev', + 'Google Chrome Beta', + 'Google Chrome', + 'Microsoft Edge', + 'Brave Browser', + 'Vivaldi', + 'Chromium', + 'Yandex' +]; + +export async function openBrowser(url: string): Promise { + // If we're on macOS, we can try opening a Chromium browser with JXA. + // This lets us reuse an existing tab when possible instead of creating a new one. + if (process.platform === 'darwin') { + try { + const ps = await execAsync('ps cax'); + const openedBrowser = supportedChromiumBrowsers.find((b) => ps.includes(b)); + + if (openedBrowser) { + // Try our best to reuse existing tab with JXA + await execAsync(`osascript openChrome.js "${url}" "${openedBrowser}"`, { + cwd: path.join(__dirname) + }); + return true; + } + } catch { + // Ignore errors, fall through to regular open + } + } + + // Fallback to open (will always open new tab) + try { + await open(url); + return true; + } catch { + return false; + } +} + +function execAsync(command: string, options?: ExecOptions): Promise { + return new Promise((resolve, reject) => { + exec(command, options, (error, stdout) => { + if (error) { + reject(error); + } else { + resolve(stdout.toString()); + } + }); + }); +} diff --git a/lib/gui/openChrome.js b/lib/gui/openChrome.js new file mode 100644 index 000000000..0d691a901 --- /dev/null +++ b/lib/gui/openChrome.js @@ -0,0 +1,70 @@ +/* +Copyright (c) 2015-present, Facebook, Inc. + +This source code is licensed under the MIT license found in the +LICENSE file at +https://github.com/facebook/create-react-app/blob/main/LICENSE + +Modified for use in html-reporter, based on Vite's implementation. +*/ + +/* global Application */ + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function run(argv) { + const urlToOpen = argv[0]; + // Allow requested program to be optional, default to Google Chrome + const programName = argv[1] ?? 'Google Chrome'; + + const app = Application(programName); + + if (app.windows.length === 0) { + app.Window().make(); + } + + // 1: Looking for tab running debugger then, + // Reload debugging tab if found, then return + const found = lookupTabWithUrl(urlToOpen, app); + if (found) { + found.targetWindow.activeTabIndex = found.targetTabIndex; + found.targetTab.reload(); + found.targetWindow.index = 1; + app.activate(); + return; + } + + // 2: Looking for Empty tab + // In case debugging tab was not found + // We try to find an empty tab instead + const emptyTabFound = lookupTabWithUrl('chrome://newtab/', app); + if (emptyTabFound) { + emptyTabFound.targetWindow.activeTabIndex = emptyTabFound.targetTabIndex; + emptyTabFound.targetTab.url = urlToOpen; + app.activate(); + return; + } + + // 3: Create new tab + // both debugging and empty tab were not found make a new tab with url + const firstWindow = app.windows[0]; + firstWindow.tabs.push(app.Tab({url: urlToOpen})); + app.activate(); +} + +/** + * Lookup tab with given url + */ +function lookupTabWithUrl(lookupUrl, app) { + const windows = app.windows(); + for (const window of windows) { + for (const [tabIndex, tab] of window.tabs().entries()) { + if (tab.url().includes(lookupUrl)) { + return { + targetTab: tab, + targetTabIndex: tabIndex + 1, + targetWindow: window + }; + } + } + } +} diff --git a/package-lock.json b/package-lock.json index 18ef6083b..7c76b303a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "html-reporter", - "version": "11.6.1", + "version": "11.8.3", "license": "MIT", "workspaces": [ "test/func/fixtures/*", @@ -38,7 +38,7 @@ "looks-same": "^10.0.1", "nested-error-stacks": "^2.1.0", "npm-which": "^3.0.1", - "opener": "^1.4.3", + "open": "^8.4.2", "ora": "^5.4.1", "p-queue": "^5.0.0", "qs": "^6.9.1", @@ -89,7 +89,6 @@ "@types/lodash": "^4.14.195", "@types/nested-error-stacks": "^2.1.0", "@types/npm-which": "^3.0.3", - "@types/opener": "^1.4.0", "@types/proxyquire": "^1.3.28", "@types/react-dom": "^18.3.0", "@types/react-virtualized": "^9.21.30", @@ -6987,15 +6986,6 @@ "@types/node": "*" } }, - "node_modules/@types/opener": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@types/opener/-/opener-1.4.0.tgz", - "integrity": "sha512-6KEABBcWAD6PychSyUUXuyCOIy64c5V6KJczTK4hpR5JKSBdK/s/LFQgRDnfy0F6lQWf9UvjE5Z2PbU7mhU3Tw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/parse-json": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.1.tgz", @@ -13197,6 +13187,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -18186,7 +18185,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, "license": "MIT", "bin": { "is-docker": "cli.js" @@ -24027,12 +24025,33 @@ "node": ">=6" } }, - "node_modules/opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", - "bin": { - "opener": "bin/opener-bin.js" + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/opn": { @@ -38847,15 +38866,6 @@ "@types/node": "*" } }, - "@types/opener": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@types/opener/-/opener-1.4.0.tgz", - "integrity": "sha512-6KEABBcWAD6PychSyUUXuyCOIy64c5V6KJczTK4hpR5JKSBdK/s/LFQgRDnfy0F6lQWf9UvjE5Z2PbU7mhU3Tw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/parse-json": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.1.tgz", @@ -43558,6 +43568,11 @@ "gopd": "^1.0.1" } }, + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==" + }, "define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -47310,8 +47325,7 @@ "is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==" }, "is-extendable": { "version": "1.0.1", @@ -51688,10 +51702,25 @@ } } }, - "opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==" + "open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "dependencies": { + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "requires": { + "is-docker": "^2.0.0" + } + } + } }, "opn": { "version": "5.5.0", diff --git a/package.json b/package.json index 2c9863238..391dda4ff 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,7 @@ "looks-same": "^10.0.1", "nested-error-stacks": "^2.1.0", "npm-which": "^3.0.1", - "opener": "^1.4.3", + "open": "^8.4.2", "ora": "^5.4.1", "p-queue": "^5.0.0", "qs": "^6.9.1", @@ -180,7 +180,6 @@ "@types/lodash": "^4.14.195", "@types/nested-error-stacks": "^2.1.0", "@types/npm-which": "^3.0.3", - "@types/opener": "^1.4.0", "@types/proxyquire": "^1.3.28", "@types/react-dom": "^18.3.0", "@types/react-virtualized": "^9.21.30",