bump Deno branch to 0.9.0 (#27)

* cdp: add internal close api

* inject: add Window.close() api

* typedef: add Window.close()

* roadmap: add close api for 0.8.0

* release: 0.8.0

* readme: update project age

* paths: add more linux browsers

* readme: update screenshot

* readme: move trying gluon to earlier up

* inject: add useSessionId to Window.cdp.send()

* typedef: add useSessionId to CDPApi.send()

* idle: rewrite to use CDP to get processes instead of exec

* index: remove now unneeded parameters for idle creation

* chore: bump version to 0.9.0-alpha.0

* inject: pass IPC browser engine

* api: add Window.versions

* typedef: add future BrowserEngine and update Browser values

* typedef: add Window.versions

* index: rename internal variable

* idle: v2 - added sleep, utils, documenting, and more

* typedef: update for IdleApi v2

* roadmap: update

* chore: bump version to 0.9.0-alpha.2

* readme: add more feature specific statuses

* readme: make specific feature statuses into a table

* cdp: return protocol errors

* idle: move to new api dir

* controls: new Window.controls API

* darwin: full support (#4)

* meta: add pnpm-lock.yaml to .gitignore for my own sanity

* darwin: preliminary support

* chore: disable menubar key in firefox

* Add support new browsers Mac OS (#20)

Co-authored-by: a.artamonov <a.artamonov@sftpro.ru>

* roadmap: tweak

* roadmap: update done

* roadmap: remove overall

* typedef: change some voids to Promise<void>s

* typedef: add Window.controls

* release: 0.9.0

Co-authored-by: CanadaHonk <oj@oojmed.com>
Co-authored-by: Beef <beefers@riseup.net>
Co-authored-by: Alexander Artamonov <47431914+artamonovtech@users.noreply.github.com>
Co-authored-by: a.artamonov <a.artamonov@sftpro.ru>
This commit is contained in:
Drake 2023-01-03 16:40:59 -08:00 committed by GitHub
parent 11ef8cc79b
commit a86305eb41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 362 additions and 153 deletions

3
.gitignore vendored
View File

@ -2,6 +2,9 @@
node_modules
package-lock.json
# pnpm
pnpm-lock.yaml
# gluon
build
chrome_data

View File

@ -9,7 +9,7 @@
- **Chromium *and Firefox* supported as browser engine**, unlike any other active framework
- **Minimal and easy to use** - Gluon has a simple yet powerful API to make apps with a Deno backend
- **Fast build times** - Gluon has build times under 1 second on most machines for small projects
- **Actively developed** and **listening to feedback** - already on 5th major revision in ~1 week of development, quickly adding unplanned requested features if liked by the community (like Firefox support!)
- **Actively developed** and **listening to feedback** - new updates are coming around weekly, quickly adding unplanned requested features if liked by the community (like Firefox support!)
- **Lower memory usage** - compared to most other frameworks Gluon should have a slightly lower average memory usage by using browser flags to squeeze out more performance
<!-- - **No forks needed** - Gluon doesn't need forks of Deno or Chromium/etc to use them, it just uses normal versions -->

View File

@ -1,5 +1,18 @@
# Gluon Changelog
## v0.9.0 [2023-01-03]
- New `Window.versions` API with browser version info
- New `Window.controls` API to manage window state (minimize/maximize/etc)
- New additions and improvements to `Window.idle`:
- `Window.idle.sleep()` now performs a light version of hibernation
- Now uses CDP commands instead of native to detect processes
- Added new `useSessionId` option to `Window.cdp.send()`, allowing to send browser-level CDP commands instead of just to target
- Added initial Mac support
## v0.8.0 [2022-12-30]
- Rewrote browser detection to support more setups
- Added `Window.close()` API to close Gluon windows gracefully
## 0.7.0 [2022-12-20]
- Added typedef
- Added async IPC listener support

77
gluon.d.ts vendored
View File

@ -48,7 +48,10 @@ type CDPApi = {
method: string,
/** Parameters of CDP command. */
params?: Object
params?: Object,
/** Send session ID with the command (default true). */
useSessionId?: Boolean = true
): Promise<any>
};
@ -62,16 +65,15 @@ type IdleAutoOptions = {
type IdleApi = {
/** Put the window into hibernation. */
hibernate(): void,
hibernate(): Promise<void>,
/**
* Put the window to sleep.
* @todo Unimplemented (for Idle v2).
*/
sleep(): void,
sleep(): Promise<void>,
/** Wake up the window from hibernation or sleep. */
wake(): void,
wake(): Promise<void>,
/** Enable/disable automatic idle management, and set its options. */
auto(
@ -83,6 +85,54 @@ type IdleApi = {
): void
};
type VersionInfo = {
/** Name of component. */
name: string,
/** Full version of component. */
version: string,
/** Major version of component as a number. */
major: number
};
type BrowserVersions = {
/**
* Product (browser) version and name.
* @example
* Window.versions.product // { name: 'Chrome Canary', version: '111.0.5513.0', major: 111 }
*/
product: VersionInfo,
/**
* Browser engine (Chromium/Firefox) version and name.
* @example
* Window.versions.engine // { name: 'chromium', version: '111.0.5513.0', major: 111 }
*/
engine: VersionInfo,
/**
* JS engine (V8/SpiderMonkey) version and name.
* @example
* Window.versions.jsEngine // { name: 'v8', version: '11.1.86', major: 11 }
*/
jsEngine: VersionInfo
};
type ControlsApi = {
/** Minimize the browser window. */
minimize(): Promise<void>,
/**
* Maximize the browser window.
* Doesn't make the window appear (use show() before as well).
*/
maximize(): Promise<void>,
/** Show (unminimize) the browser window. */
show(): Promise<void>
}
type Window = {
/** API for accessing the window itself. */
window: WindowApi,
@ -97,12 +147,25 @@ type Window = {
* API for Gluon idle management (like hibernation).
* @experimental
*/
idle: IdleApi
idle: IdleApi,
/** Browser version info of the window: product (browser), engine (Chromium/Firefox), and JS engine (V8/SpiderMonkey). */
versions: BrowserVersions,
/** Control (minimize, maximize, etc) the browser window. */
controls: ControlsApi,
/** Close the Gluon window. */
close(): void
};
/** A browser that Gluon supports. */
type Browser = 'chrome'|'chrome_canary'|'chromium'|'edge'|'firefox'|'firefox_nightly';
type Browser = 'chrome'|'chrome_canary'|'chromium'|'chromium_snapshot'|'edge'|'firefox'|'firefox_nightly';
/** A browser engine that Gluon supports. */
type BrowserEngine = 'chromium'|'firefox';
/** Additional options for opening */
type OpenOptions = {

View File

@ -1,6 +1,6 @@
{
"name": "@gluon-framework/gluon",
"version": "0.8.0-dev",
"version": "0.9.0",
"description": "Make websites into desktop apps with system installed browsers and NodeJS.",
"main": "src/index.js",
"types": "gluon.d.ts",

View File

@ -3,20 +3,19 @@
> **Note** |
> Want more info on what some of these mean/are? Ask in [our Discord](https://discord.gg/RFtUCA8fST)!
## v0.8.0
- [X] Rewrite browser detection to support more setups
## v0.10.0
- [ ] Resources Window API
- [ ] On/await load Window API
## v0.9.0
- [X] Browser version info API
- [X] Idle API v2
- [X] Mac support
- [X] Window controls API
## v0.8.0
- [X] Rewrite browser detection to support more setups
- [X] Close API
## v0.7.0
- [X] Early Idle Window API
## Overall - December 2022 - January 2023
- [X] Total internal rewrite
- [X] Type definitions
- [X] "Hibernation" feasibility study
- [X] Early hibernation API
- [ ] Automated PR/commit CI testing
- [ ] System Tray API
- [ ] Electron "compatibility layer" for basic/simple apps as a demo of versatile API / pros?
- More? Need to know what's wanted

20
src/api/controls.js vendored Normal file
View File

@ -0,0 +1,20 @@
export default async (CDP) => {
const { windowId } = await CDP.send('Browser.getWindowForTarget');
const setWindowState = async state => await CDP.send('Browser.setWindowBounds', { windowId, bounds: { windowState: state }});
return {
minimize: async () => {
await setWindowState('minimized');
},
maximize: async () => {
await setWindowState('maximized');
},
show: async () => {
await setWindowState('minimized');
await setWindowState('normal');
}
};
};

166
src/api/idle.js Normal file
View File

@ -0,0 +1,166 @@
import { exec } from 'https://deno.land/std@0.170.0/node/child_process.ts';
const killProcesses = async pids => process.platform !== 'win32' ? Promise.resolve('') : new Promise(resolve => exec(`taskkill /F ${pids.map(x => `/PID ${x}`).join(' ')}`, (e, out) => resolve(out)));
export default async (CDP, { browserType }) => {
if (browserType !== 'chromium') { // current implementation is for chromium-based only
const warning = () => log(`Warning: Idle API is currently only for Chromium (running on ${browserType})`);
return {
hibernate: warning,
sleep: warning,
wake: warning,
auto: warning
};
}
const killNonCrit = async () => { // kill non-critical processes to save memory - crashes chromium internally but not fully
const procs = (await CDP.send('SystemInfo.getProcessInfo', {}, false)).processInfo;
const nonCriticalProcs = procs.filter(x => x.type !== 'browser'); // browser = the actual main chromium binary
await killProcesses(nonCriticalProcs.map(x => x.id));
log(`killed ${nonCriticalProcs.length} processes`);
};
const purgeMemory = async () => { // purge most memory we can
await CDP.send('Memory.forciblyPurgeJavaScriptMemory');
await CDP.send('HeapProfiler.collectGarbage');
};
const getScreenshot = async () => { // get a screenshot a webm base64 data url
const { data } = await CDP.send(`Page.captureScreenshot`, {
format: 'webp'
});
return `data:image/webp;base64,${data}`;
};
const getLastUrl = async () => {
const history = await CDP.send('Page.getNavigationHistory');
return history.entries[history.currentIndex].url;
};
let wakeUrl, hibernating = false;
const hibernate = async () => { // hibernate - crashing chromium internally to save max memory. users will see a crash/gone wrong page but we hopefully "reload" quick enough once visible again for not much notice.
if (hibernating) return;
if (process.platform !== 'win32') return sleep(); // sleep instead - full hibernation is windows only for now due to needing to do native things
hibernating = true;
const startTime = performance.now();
wakeUrl = await getLastUrl();
purgeMemory();
await killNonCrit();
purgeMemory();
log(`hibernated in ${(performance.now() - startTime).toFixed(2)}ms`);
};
const sleep = async () => { // light hibernate - instead of killing chromium processes we just navigate to a screenshot of the current page.
if (hibernating) return;
hibernating = true;
const startTime = performance.now();
wakeUrl = await getLastUrl();
purgeMemory();
await CDP.send(`Page.navigate`, {
url: lastScreenshot
});
purgeMemory();
log(`slept in ${(performance.now() - startTime).toFixed(2)}ms`);
};
const wake = async () => { // wake up from hibernation/sleep by navigating to the original page
if (!hibernating) return;
const startTime = performance.now();
await CDP.send('Page.navigate', {
url: wakeUrl
});
log(`began wake in ${(performance.now() - startTime).toFixed(2)}ms`);
hibernating = false;
};
const { windowId } = await CDP.send('Browser.getWindowForTarget');
let autoEnabled = Deno.args.includes('--force-auto-idle'), autoOptions = {
timeMinimizedToHibernate: 5
};
let autoInterval;
const startAuto = () => {
if (autoInterval) return; // already started
let lastState = '', lastStateWhen = performance.now();
autoInterval = setInterval(async () => {
const { bounds: { windowState } } = await CDP.send('Browser.getWindowBounds', { windowId });
if (windowState !== lastState) {
lastState = windowState;
lastStateWhen = performance.now();
}
if (!hibernating && windowState === 'minimized' && performance.now() - lastStateWhen > autoOptions.timeMinimizedToHibernate * 1000) await hibernate();
else if (hibernating && windowState !== 'minimized') await wake();
}, 200);
log('started auto idle');
};
const stopAuto = () => {
if (!autoInterval) return; // already stopped
clearInterval(autoInterval);
autoInterval = null;
log('stopped auto idle');
};
let lastScreenshot, takingScreenshot = false;
const screenshotInterval = setInterval(async () => {
if (takingScreenshot) return;
takingScreenshot = true;
lastScreenshot = await getScreenshot();
takingScreenshot = false;
}, 10000);
getScreenshot().then(x => lastScreenshot = x);
log(`idle API active (window id: ${windowId})`);
if (autoEnabled) startAuto();
const setWindowState = async state => await CDP.send('Browser.setWindowBounds', { windowId, bounds: { windowState: state }});
return {
hibernate,
sleep,
wake,
auto: (enabled, options) => {
autoEnabled = enabled;
autoOptions = {
...options,
...autoOptions
};
if (enabled) startAuto();
else stopAuto();
}
};
};

View File

@ -18,6 +18,8 @@ user_pref('privacy.window.maxInnerHeight', ${windowSize[1]});`}
user_pref('privacy.resistFingerprinting', true);
user_pref('fission.bfcacheInParent', false);
user_pref('fission.webContentIsolationStrategy', 0);
user_pref('ui.key.menuAccessKeyFocuses', false);
${process.platform === 'darwin' ? `user_pref('browser.tabs.inTitlebar', 0);` : `` }
`);
// user_pref('privacy.resistFingerprinting', false);

View File

@ -3,10 +3,9 @@ window.log = (...args) => console.log(`[${rgb(88, 101, 242, 'Gluon')}]`, ...args
Deno.version = { // have to do this because... Deno
...Deno.version,
gluon: '0.8.0-deno-dev'
gluon: '0.9.0-deno-dev'
};
import { join, dirname, delimiter, sep } from 'https://deno.land/std@0.170.0/node/path.ts';
import { access, readdir } from 'https://deno.land/std@0.170.0/node/fs/promises.ts';
import { fileURLToPath } from 'https://deno.land/std@0.170.0/node/url.ts';
@ -14,7 +13,8 @@ import { fileURLToPath } from 'https://deno.land/std@0.170.0/node/url.ts';
import Chromium from './browser/chromium.js';
import Firefox from './browser/firefox.js';
import IdleAPI from './lib/idle.js';
import IdleAPI from './api/idle.js';
import ControlsAPI from './api/controls.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@ -32,8 +32,23 @@ const browserPaths = ({
linux: { // these should be in path so just use the name of the binary
chrome: [ 'chrome', 'google-chrome', 'chrome-browser', 'google-chrome-stable' ],
chrome_canary: [ 'chrome-canary', 'google-chrome-canary', 'google-chrome-unstable', 'chrome-unstable' ],
chromium: [ 'chromium', 'chromium-browser' ],
chromium_snapshot: [ 'chromium-snapshot', 'chromium-snapshot-bin' ],
firefox: 'firefox',
firefox_nightly: 'firefox-nightly'
},
darwin: {
chrome: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
chrome_canary: '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
edge: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
chromium: '/Applications/Chromium.app/Contents/MacOS/Chromium',
firefox: '/Applications/Firefox.app/Contents/MacOS/firefox',
firefox_nightly: '/Applications/Firefox Nightly.app/Contents/MacOS/firefox'
}
})[Deno.build.os];
@ -98,7 +113,7 @@ const startBrowser = async (url, { windowSize, forceBrowser }) => {
const browserType = browserName.startsWith('firefox') ? 'firefox' : 'chromium';
const Browser = await (browserType === 'firefox' ? Firefox : Chromium)({
const Window = await (browserType === 'firefox' ? Firefox : Chromium)({
browserName: browserFriendlyName,
dataPath,
browserPath
@ -107,9 +122,10 @@ const startBrowser = async (url, { windowSize, forceBrowser }) => {
windowSize
});
Browser.idle = await IdleAPI(Browser.cdp, { browserType, dataPath });
Window.idle = await IdleAPI(Window.cdp, { browserType });
Window.controls = await ControlsAPI(Window.cdp);
return Browser;
return Window;
};
export const open = async (url, { windowSize, onLoad, forceBrowser } = {}) => {

View File

@ -1,6 +1,6 @@
import IPCApi from '../lib/ipc.js';
export default async (CDP, injectionType = 'browser', { browserName }) => {
export default async (CDP, proc, injectionType = 'browser', { browserName } = { browserName: 'unknown' }) => {
let pageLoadCallback = () => {}, onWindowMessage = () => {};
CDP.onMessage(msg => {
if (msg.method === 'Runtime.bindingCalled' && msg.params.name === '_gluonSend') onWindowMessage(JSON.parse(msg.params.payload));
@ -27,6 +27,9 @@ export default async (CDP, injectionType = 'browser', { browserName }) => {
log('browser:', browserInfo.product);
}
const browserEngine = browserInfo.jsVersion.startsWith('1.') ? 'firefox' : 'chromium';
CDP.sendMessage('Runtime.enable', {}, sessionId); // enable runtime API
CDP.sendMessage('Runtime.addBinding', { // setup sending from window to Node via Binding
@ -42,17 +45,24 @@ export default async (CDP, injectionType = 'browser', { browserName }) => {
const [ ipcMessageCallback, injectIPC, IPC ] = await IPCApi({
browserName,
browserInfo
browserInfo,
browserEngine
}, {
evalInWindow,
evalOnNewDocument: source => CDP.sendMessage('Page.addScriptToEvaluateOnNewDocument', { source }, sessionId),
pageLoadPromise: new Promise(res => pageLoadCallback = res)
});
onWindowMessage = ipcMessageCallback;
log('finished setup');
const generateVersionInfo = (name, version) => ({
name,
version,
major: parseInt(version.split('.')[0])
});
return {
window: {
eval: evalInWindow,
@ -61,7 +71,18 @@ export default async (CDP, injectionType = 'browser', { browserName }) => {
ipc: IPC,
cdp: {
send: (method, params) => CDP.sendMessage(method, params, sessionId)
send: (method, params, useSessionId = true) => CDP.sendMessage(method, params, useSessionId ? sessionId : undefined)
},
close: () => {
CDP.close();
proc.kill();
},
versions: {
product: generateVersionInfo(browserName, browserInfo.product.split('/')[1]),
engine: generateVersionInfo(browserEngine, browserInfo.product.split('/')[1]),
jsEngine: generateVersionInfo(browserEngine === 'chromium' ? 'v8' : 'spidermonkey', browserInfo.jsVersion)
}
};
};

View File

@ -30,5 +30,5 @@ export default async (browserPath, args, transport, extra) => {
break;
}
return await InjectInto(CDP, transport === 'stdio' ? 'browser' : 'target', extra);
return await InjectInto(CDP, proc, transport === 'stdio' ? 'browser' : 'target', extra);
};

View File

@ -3,6 +3,8 @@ import { get } from 'https://deno.land/std@0.170.0/node/http.ts';
export default async ({ pipe: { pipeWrite, pipeRead } = {}, port }) => {
let messageCallbacks = [], onReply = {};
const onMessage = msg => {
if (closed) return; // closed, ignore
msg = JSON.parse(msg);
// log('received', msg);
@ -16,10 +18,13 @@ export default async ({ pipe: { pipeWrite, pipeRead } = {}, port }) => {
for (const callback of messageCallbacks) callback(msg);
};
let _send;
let closed = false;
let _send, _close;
let msgId = 0;
const sendMessage = async (method, params = {}, sessionId = undefined) => {
if (closed) throw new Error('CDP connection closed');
const id = msgId++;
const msg = {
@ -38,6 +43,8 @@ export default async ({ pipe: { pipeWrite, pipeRead } = {}, port }) => {
onReply[id] = msg => res(msg);
});
if (reply.error) return new Error(reply.error.message);
return reply.result;
};
@ -79,11 +86,15 @@ export default async ({ pipe: { pipeWrite, pipeRead } = {}, port }) => {
const ws = new WebSocket(target.webSocketDebuggerUrl);
await new Promise(resolve => ws.onopen = resolve);
_send = data => ws.send(data);
ws.onmessage = ({ data }) => onMessage(data);
_send = data => ws.send(data);
_close = () => ws.close();
} else {
let pending = '';
pipeRead.on('data', buf => {
if (closed) return; // closed, ignore
let end = buf.indexOf('\0'); // messages are null separated
if (end === -1) { // no complete message yet
@ -110,6 +121,8 @@ export default async ({ pipe: { pipeWrite, pipeRead } = {}, port }) => {
pipeWrite.write(data);
pipeWrite.write('\0');
};
_close = () => {};
}
return {
@ -121,6 +134,12 @@ export default async ({ pipe: { pipeWrite, pipeRead } = {}, port }) => {
messageCallbacks.push(callback);
},
sendMessage
sendMessage,
close: () => {
closed = true;
_close();
}
};
};

View File

@ -1,113 +0,0 @@
import { exec } from 'https://deno.land/std@0.170.0/node/child_process.ts';
const getProcesses = async containing => Deno.build.os !== 'windows' ? Promise.resolve([]) : new Promise(resolve => exec(`wmic process get Commandline,ProcessID /format:csv`, (e, out) => {
resolve(out.toString().split('\r\n').slice(2).map(x => {
const parsed = x.trim().split(',').slice(1).reverse();
return [
parseInt(parsed[0]) || parsed[0], // pid to int
parsed.slice(1).join(',')
];
}).filter(x => x[1] && x[1].includes(containing)));
}));
const killProcesses = async pids => Deno.build.os !== 'windows' ? Promise.resolve('') : new Promise(resolve => exec(`taskkill /F ${pids.map(x => `/PID ${x}`).join(' ')}`, (e, out) => resolve(out)));
export default async (CDP, { browserType, dataPath }) => {
if (browserType !== 'chromium') { // current implementation is for chromium-based only
const warning = () => log(`Warning: Idle API is currently only for Chromium (running on ${browserType})`);
return {
hibernate: warning,
sleep: warning,
wake: warning,
auto: warning
};
};
const killNonCrit = async () => {
const procs = await getProcesses(dataPath);
const nonCriticalProcs = procs.filter(x => x[1].includes('type='));
await killProcesses(nonCriticalProcs.map(x => x[0]));
log(`killed ${nonCriticalProcs.length} processes`);
};
let hibernating = false;
const hibernate = async () => {
hibernating = true;
const startTime = performance.now();
await killNonCrit();
// await killNonCrit();
log(`hibernated in ${(performance.now() - startTime).toFixed(2)}ms`);
};
const wake = async () => {
const startTime = performance.now();
await CDP.send('Page.reload');
log(`began wake in ${(performance.now() - startTime).toFixed(2)}ms`);
hibernating = false;
};
const { windowId } = await CDP.send('Browser.getWindowForTarget');
let autoEnabled = Deno.args.includes('--force-auto-idle'), autoOptions = {
timeMinimizedToHibernate: 5
};
let autoInterval;
const startAuto = () => {
if (autoInterval) return; // already started
let lastState = '', lastStateWhen = performance.now();
autoInterval = setInterval(async () => {
const { bounds: { windowState } } = await CDP.send('Browser.getWindowBounds', { windowId });
if (windowState !== lastState) {
lastState = windowState;
lastStateWhen = performance.now();
}
if (!hibernating && windowState === 'minimized' && performance.now() - lastStateWhen > autoOptions.timeMinimizedToHibernate * 1000) await hibernate();
else if (hibernating && windowState !== 'minimized') await wake();
}, 200);
log('started auto idle');
};
const stopAuto = () => {
if (!autoInterval) return; // already stopped
clearInterval(autoInterval);
autoInterval = null;
log('stopped auto idle');
};
log(`idle API active (window id: ${windowId})`);
if (autoEnabled) startAuto();
return {
hibernate,
sleep: () => {},
wake,
auto: (enabled, options) => {
autoEnabled = enabled;
autoOptions = {
...options,
...autoOptions
};
if (enabled) startAuto();
else stopAuto();
}
};
};

View File

@ -1,4 +1,4 @@
export default ({ browserName, browserInfo }, { evalInWindow, evalOnNewDocument, pageLoadPromise }) => {
export default ({ browserName, browserInfo, browserEngine }, { evalInWindow, evalOnNewDocument, pageLoadPromise }) => {
const injection = `(() => {
if (window.Gluon) return;
let onIPCReply = {}, ipcListeners = {};
@ -8,7 +8,7 @@ window.Gluon = {
builder: '${'GLUGUN_VERSION' === 'G\LUGUN_VERSION' ? 'nothing' : 'Glugun GLUGUN_VERSION'}',
deno: '${Deno.version.deno}',
browser: '${browserInfo?.product.split('/')[1]}',
browserType: '${browserName.startsWith('Firefox') ? 'firefox' : 'chromium'}',
browserType: '${browserEngine}',
product: '${browserName}',
js: {