From e8e6b06c3c0eba251cafcd1892a7d183bb1ac481 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Sun, 5 Jan 2025 02:52:01 +0000 Subject: [PATCH] Add safari browser, using AppleScript to start/stop --- .github/workflows/CI.yaml | 4 + ARCHITECTURE.md | 27 ++++-- eslint.config.js | 2 + src/browsers.js | 173 ++++++++++++++++++++++++++++++++++++-- src/server.js | 17 ++-- src/util.js | 3 + 6 files changed, 207 insertions(+), 19 deletions(-) diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index bd9ac82..a5f478a 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -57,3 +57,7 @@ jobs: - name: Check system browsers run: node bin/qtap.js -v -b firefox -b chrome -b chromium -b edge test/pass.html + + - name: Check system browsers (Safari) + if: ${{ runner.os == 'macOS' }} + run: node bin/qtap.js -v -b safari test/pass.html diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ff0678d..cc95b38 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -69,12 +69,13 @@ One of the passed parameters is a standard [`AbortSignal` object](https://develo ```js // Using our utility -async function myBrowser(url, signal, logger) { +async function myBrowser (url, signal, logger) { await LocalBrowser.spawn(['/bin/mybrowser'], ['-headless', url], signal, logger); } -// Minimal custom implementation on native Node.js -async function myBrowser(url, signal, logger) { +// Minimal sub process +import child_process from 'node:child_process'; +async function myBrowser (url, signal, logger) { logger.debug('Spawning /bin/mybrowser'); const spawned = child_process.spawn('/bin/mybrowser', ['-headless', url], { signal }); await new Promise((resolve, reject) => { @@ -82,6 +83,19 @@ async function myBrowser(url, signal, logger) { spawned.on('exit', (code) => reject(new Error(`Process exited ${code}`))); }); } + +// Minimal custom +async function myBrowser (url, signal, logger) { + // * start browser and navigate to `url` + // * if you encounter problems, throw + await new Promise((resolve, reject) => { + // * once browser has stopped, call resolve() + // * if you encounter problems, call reject() + signal.addEventListener('abort', () => { + // stop browser + }); + }); +} ``` Alternatives considered: @@ -150,14 +164,13 @@ Alternatives considered: // Using our utility import qtap from 'qtap'; - function myBrowser (url, signal, logger) { + async function myBrowser (url, signal, logger) { await qtap.LocalBrowser.spawn(['/bin/mybrowser'], ['-headless', url], signal, logger ); } - // Minimal custom implementation + // Minimal sub process import child_process from 'node:child_process'; - - function myBrowser (url, signal, logger) { + async function myBrowser (url, signal, logger) { const spawned = child_process.spawn('/bin/mybrowser', ['-headless', url], { signal }); await new Promise((resolve, reject) => { spawned.on('error', (error) => { diff --git a/eslint.config.js b/eslint.config.js index 2967c1e..3272e57 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -15,6 +15,8 @@ export default [ rules: { 'comma-dangle': 'off', 'multiline-ternary': 'off', + 'no-throw-literal': 'off', + 'object-shorthand': 'off', 'operator-linebreak': ['error', 'before'], } } diff --git a/src/browsers.js b/src/browsers.js index 64e9d36..10c6fd2 100644 --- a/src/browsers.js +++ b/src/browsers.js @@ -6,7 +6,7 @@ import os from 'node:os'; import path from 'node:path'; import which from 'which'; -import { concatGenFn } from './util.js'; +import { concatGenFn, globalSignal } from './util.js'; const QTAP_DEBUG = process.env.QTAP_DEBUG === '1'; const tempDirs = []; @@ -96,7 +96,10 @@ const LocalBrowser = { /** * Create a new temporary directory and return its name. * - * The newly created directory will automatically will cleaned up. + * This creates subdirectories inside Node.js `os.tmpdir`, which honors + * any TMPDIR, TMP, or TEMP environment variable. + * + * The newly created directory is automatically cleaned up at the end of the process. * * @returns {string} */ @@ -279,12 +282,172 @@ async function chromium (paths, url, signal, logger) { '--disable-gpu', '--disable-dev-shm-usage' ]), - ...(process.env.CHROMIUM_FLAGS ? process.env.CHROMIUM_FLAGS.split(/\s+/) : []), + ...(process.env.CHROMIUM_FLAGS ? process.env.CHROMIUM_FLAGS.split(/\s+/) : ( + process.env.CI ? ['--no-sandbox'] : []) + ), url ]; await LocalBrowser.spawn(paths, args, signal, logger); } +/** + * Known approaches: + * + * - `Safari `. This does not allow URLs. Safari allows only local files to be passed. + * + * - `Safari redirect.html`, without other arguments, worked from 2012-2018, as used by Karma. + * This "trampoline" approach involves creating a temporary HTML file + * with ``, which we open instead. + * https://github.com/karma-runner/karma-safari-launcher/blob/v1.0.0/index.js + * https://github.com/karma-runner/karma/blob/v0.3.5/lib/launcher.js#L213 + * https://github.com/karma-runner/karma/commit/5513fd66ae + * + * This is no longer viable after macOS 10.14 Mojave, because macOS SIP prompts the user + * due to our temporary file being outside `~/Library/Containers/com.apple.Safari`. + * https://github.com/karma-runner/karma-safari-launcher/issues/29 + * + * - `open -F -W -n -b com.apple.Safari `. This starts correctly, but doesn't expose + * a PID to cleanly end the process. + * https://github.com/karma-runner/karma-safari-launcher/issues/29 + * + * - `Safari container/redirect.html`. macOS SIP denies this by default for the same reason. + * But, as long as you grant an exemption to Terminal to write to Safari's container, or + * grant it Full Disk Access, this is viable. + * https://github.com/flutter/engine/pull/27567 + * https://github.com/marcoscaceres/karma-safaritechpreview-launcher/issues/7 + * + * It seems that GitHub CI has pre-approved limited access in its macOS images, to make + * this work [1][2]. This might be viable if it is tolerable to prompt on first local use, + * and require granting said access to the Terminal in general (which has lasting + * consequences beyond QTap). + * + * - native app Swift/ObjectiveC proxy. This reportedly works but requires + * a binary which requires compilation and makes auditing significantly harder. + * https://github.com/karma-runner/karma-safari-launcher/issues/29 + * https://github.com/muthu90ec/karma-safarinative-launcher/ + * + * - `osascript -e