@@ -9,7 +9,7 @@ import which from 'which';
99
1010const QTAP_DEBUG = process . env . QTAP_DEBUG === '1' ;
1111
12- class LocalBrowser {
12+ const LocalBrowser = {
1313 /**
1414 * @param {string|Array<string|null>|Iterator<string|null> } candidates
1515 * Path to an executable command or an iterable list of candidate paths to
@@ -26,22 +26,23 @@ class LocalBrowser {
2626 * @param {AbortSignal } signal
2727 * @return {Promise }
2828 */
29- static async spawn ( candidates , args , signal , logger ) {
29+ async spawn ( candidates , args , signal , logger ) {
3030 if ( typeof candidates === 'string' ) {
3131 candidates = [ candidates ] ;
3232 }
3333 let exe ;
3434 for ( const candidate of candidates ) {
3535 if ( candidate !== null ) {
36- logger . debug ( 'browser_exe_check' , candidate ) ;
3736 // Optimization: Use fs.existsSync. It is on par with accessSync and statSync,
3837 // and beats concurrent fs/promises.access(cb) via Promise.all().
3938 // Starting the promise chain alone takes the same time as a loop with
4039 // 5x existsSync(), not even counting the await and boilerplate to manage it all.
4140 if ( fs . existsSync ( candidate ) ) {
42- logger . debug ( 'browser_exe_found' ) ;
41+ logger . debug ( 'browser_exe_found' , candidate ) ;
4342 exe = candidate ;
4443 break ;
44+ } else {
45+ logger . debug ( 'browser_exe_check' , candidate ) ;
4546 }
4647 }
4748 }
@@ -85,24 +86,24 @@ class LocalBrowser {
8586 }
8687 } ) ;
8788 } ) ;
88- }
89+ } ,
8990
9091 /**
9192 * Create a new temporary directory and return its name.
9293 *
9394 * @returns {string }
9495 */
95- static makeTempDir ( ) {
96+ makeTempDir ( ) {
9697 // Use mkdtemp (instead of only tmpdir) to avoid clash with past or concurrent qtap procesess.
9798 return fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'qtap_' ) ) ;
98- }
99+ } ,
99100
100101 /**
101102 * Detect Windows Subsystem for Linux
102103 *
103104 * @returns {bool }
104105 */
105- static isWsl ( ) {
106+ isWsl ( ) {
106107 try {
107108 return (
108109 process . platform === 'linux'
@@ -118,133 +119,103 @@ class LocalBrowser {
118119 return false ;
119120 }
120121 }
122+ } ;
121123
122- /**
123- * A browser is responsible for knowing whether the process failed to
124- * launch or spawn, and whether it exited unexpectedly.
125- *
126- * A browser is not responsible for knowing whether it succeeded in
127- * navigating to the given URL.
128- *
129- * It is the responsiblity of ControlServer to call controller.abort(),
130- * if it believes the browser has likely failed to load the start URL
131- * (e.g. a reasonable timeout if a browser has not sent its first TAP
132- * message, or has not sent anything else for a while).
133- *
134- * If a browser exits on its own (i.e. ControlServer did not call
135- * controller.abort), then start() should throw an Error or reject its
136- * returned Promise.
137- *
138- * @param {string } url
139- * @param {AbortSignal } signal
140- * @param {qtap-Logger } logger
141- * @return {Promise }
142- */
143- static async launch ( url , signal , logger ) {
144- throw new Error ( 'not implemented' ) ;
124+ async function firefox ( url , signal , logger ) {
125+ const profileDir = LocalBrowser . makeTempDir ( ) ;
126+ const args = [ url , '-profile' , profileDir , '-no-remote' , '-wait-for-browser' ] ;
127+ if ( ! QTAP_DEBUG ) {
128+ args . push ( '-headless' ) ;
145129 }
146130
147- /**
148- * Clean up any shared resources.
149- *
150- * The same browser may start() several times concurrently in order
151- * to test multiple URLs. In general, anything started or created
152- * by start() should also be stopped or otherwise cleaned up by start().
153- *
154- * If you lazy-create any shared resources (such as a tunnel connection
155- * for a cloud browser provider, a server or other socket, a cache directory,
156- * etc) then this method can be used to tear those down once at the end
157- * of the qtap process.
158- */
159- // TODO: Implement cleanupOnce support. Use case: browserstack tunnel.
160- // async cleanupOnce () {
161- // }
131+ // http://kb.mozillazine.org/About:config_entries
132+ // https://github.com/sitespeedio/browsertime/blob/v23.5.0/lib/firefox/settings/firefoxPreferences.js
133+ // https://github.com/airtap/the-last-browser-launcher/blob/v0.1.1/lib/launch/index.js
134+ // https://github.com/karma-runner/karma-firefox-launcher/blob/v2.1.3/index.js
135+ logger . debug ( 'firefox_prefs_create' , 'Creating temporary prefs.js file' ) ;
136+ fs . writeFileSync ( path . join ( profileDir , 'prefs.js' ) , firefox . createPrefsJs ( {
137+ 'app.update.disabledForTesting' : true , // Disable auto-updates
138+ 'browser.sessionstore.resume_from_crash' : false ,
139+ 'browser.shell.checkDefaultBrowser' : false ,
140+ 'dom.disable_open_during_load' : false ,
141+ 'dom.max_script_run_time' : 0 , // Disable "slow script" dialogs
142+ 'extensions.autoDisableScopes' : 1 ,
143+ 'extensions.update.enabled' : false , // Disable auto-updates
144+
145+ // Blank home, blank new tab, disable extra welcome tabs for "first launch"
146+ 'browser.EULA.override' : true ,
147+ 'browser.startup.firstrunSkipsHomepage' : false ,
148+ 'browser.startup.page' : 0 ,
149+ 'datareporting.policy.dataSubmissionPolicyBypassNotification' : true , // Avoid extra tab for mozilla.org/en-US/privacy/firefox/
150+ 'startup.homepage_override_url' : '' ,
151+ 'startup.homepage_welcome_url' : '' ,
152+ 'startup.homepage_welcome_url.additional' : '' ,
153+
154+ // Performance optimizations
155+ 'browser.bookmarks.max_backups' : 0 , // Optimization, via sitespeedio/browsertime
156+ 'browser.bookmarks.restore_default_bookmarks' : false , // Optimization
157+ 'browser.cache.disk.capacity' : 0 , // Optimization: Avoid disk writes
158+ 'browser.cache.disk.smart_size.enabled' : false , // Optimization
159+ 'browser.chrome.guess_favicon' : false , // Optimization: Avoid likely 404 for unspecified favicon
160+ 'browser.pagethumbnails.capturing_disabled' : true , // Optimization, via sitespeedio/browsertime
161+ 'browser.search.region' : 'US' , // Optimization: Avoid internal geoip lookup
162+ 'browser.sessionstore.enabled' : false , // Optimization
163+ 'dom.min_background_timeout_value' : 10 , // Optimization, via https://github.com/karma-runner/karma-firefox-launcher/issues/19
164+ } ) ) ;
165+
166+ try {
167+ await LocalBrowser . spawn ( firefox . getCandidates ( ) , args , signal , logger ) ;
168+ } finally {
169+ // On Windows, when spawn() returns after firefox.exe has stopped, it sometimes can't delete
170+ // some temporary files yet as they're somehow still in use (EBUSY). Perhaps a race condition,
171+ // or an lagged background process?
172+ // > BUSY: resource busy or locked,
173+ // > unlink 'C:\Users\RUNNER~1\AppData\Local\Temp\qtap_EZ4IoO\bounce-tracking-protection.sqlite'
174+ // Enable `maxRetries` to cover the common case, and beyond that try-catch
175+ // as it is not critical for test completion.
176+ // TODO: Can we abstract this so that it happens automatically for any directory
177+ // obtained via LocalBrowser.makeTempDir?
178+ try {
179+ fs . rmSync ( profileDir , { recursive : true , force : true , maxRetries : 2 } ) ;
180+ } catch ( e ) {
181+ logger . warning ( 'firefox_cleanup_fail' , e ) ;
182+ }
183+ }
162184}
163185
164- class FirefoxBrowser {
165- * getCandidates ( ) {
166- if ( process . env . FIREFOX_BIN ) yield process . env . FIREFOX_BIN ;
167-
168- // Find /usr/bin/firefox on platforms like linux (including WSL), freebsd, openbsd.
169- yield which . sync ( 'firefox' , { nothrow : true } ) ;
170-
171- if ( process . platform === 'darwin' ) {
172- if ( process . env . HOME ) yield process . env . HOME + '/Applications/Firefox.app/Contents/MacOS/firefox' ;
173- yield '/Applications/Firefox.app/Contents/MacOS/firefox' ;
174- }
186+ firefox . getCandidates = function * ( ) {
187+ if ( process . env . FIREFOX_BIN ) yield process . env . FIREFOX_BIN ;
175188
176- if ( process . platform === 'win32' ) {
177- if ( process . env . PROGRAMFILES ) yield process . env . PROGRAMFILES + '\\Mozilla Firefox\\firefox.exe' ;
178- if ( process . env [ 'PROGRAMFILES(X86)' ] ) yield process . env [ 'PROGRAMFILES(X86)' ] + '\\Mozilla Firefox\\firefox.exe' ;
179- yield 'C:\\Program Files\\Mozilla Firefox\\firefox.exe' ;
180- }
189+ // Find /usr/bin/firefox on platforms like linux (including WSL), freebsd, openbsd.
190+ yield which . sync ( 'firefox' , { nothrow : true } ) ;
181191
182- // TODO: Support launching Firefox on Windows from inside WSL
183- // if (LocalBrowser.isWsl()) { }
192+ if ( process . platform === 'darwin' ) {
193+ if ( process . env . HOME ) yield process . env . HOME + '/Applications/Firefox.app/Contents/MacOS/firefox' ;
194+ yield '/Applications/Firefox.app/Contents/MacOS/firefox' ;
184195 }
185196
186- static createPrefsJs ( prefs ) {
187- let js = '' ;
188- for ( const key in prefs ) {
189- js += 'user_pref("' + key + '", ' + JSON . stringify ( prefs [ key ] ) + ');\n' ;
190- }
191- return js ;
197+ if ( process . platform === 'win32' ) {
198+ if ( process . env . PROGRAMFILES ) yield process . env . PROGRAMFILES + '\\Mozilla Firefox\\firefox.exe' ;
199+ if ( process . env [ 'PROGRAMFILES(X86)' ] ) yield process . env [ 'PROGRAMFILES(X86)' ] + '\\Mozilla Firefox\\firefox.exe' ;
200+ yield 'C:\\Program Files\\Mozilla Firefox\\firefox.exe' ;
192201 }
193202
194- async launch ( url , signal , logger ) {
195- const profileDir = LocalBrowser . makeTempDir ( ) ;
196- const args = [ url , '-profile' , profileDir , '-no-remote' , '-wait-for-browser' ] ;
197- if ( ! QTAP_DEBUG ) {
198- args . push ( '-headless' ) ;
199- }
200-
201- // http://kb.mozillazine.org/About:config_entries
202- // https://github.com/sitespeedio/browsertime/blob/v23.5.0/lib/firefox/settings/firefoxPreferences.js
203- // https://github.com/airtap/the-last-browser-launcher/blob/v0.1.1/lib/launch/index.js
204- // https://github.com/karma-runner/karma-firefox-launcher/blob/v2.1.3/index.js
205- logger . debug ( 'firefox_prefs_create' , 'Creating temporary prefs.js file' ) ;
206- fs . writeFileSync ( path . join ( profileDir , 'prefs.js' ) , FirefoxBrowser . createPrefsJs ( {
207- 'app.update.disabledForTesting' : true , // Disable auto-updates
208- 'browser.sessionstore.resume_from_crash' : false ,
209- 'browser.shell.checkDefaultBrowser' : false ,
210- 'dom.disable_open_during_load' : false ,
211- 'dom.max_script_run_time' : 0 , // Disable "slow script" dialogs
212- 'extensions.autoDisableScopes' : 1 ,
213- 'extensions.update.enabled' : false , // Disable auto-updates
214-
215- // Blank home, blank new tab, disable extra welcome tabs for "first launch"
216- 'browser.EULA.override' : true ,
217- 'browser.startup.firstrunSkipsHomepage' : false ,
218- 'browser.startup.page' : 0 ,
219- 'datareporting.policy.dataSubmissionPolicyBypassNotification' : true , // Avoid extra tab for mozilla.org/en-US/privacy/firefox/
220- 'startup.homepage_override_url' : '' ,
221- 'startup.homepage_welcome_url' : '' ,
222- 'startup.homepage_welcome_url.additional' : '' ,
223-
224- // Performance optimizations
225- 'browser.bookmarks.max_backups' : 0 , // Optimization, via sitespeedio/browsertime
226- 'browser.bookmarks.restore_default_bookmarks' : false , // Optimization
227- 'browser.cache.disk.capacity' : 0 , // Optimization: Avoid disk writes
228- 'browser.cache.disk.smart_size.enabled' : false , // Optimization
229- 'browser.chrome.guess_favicon' : false , // Optimization: Avoid likely 404 for unspecified favicon
230- 'browser.pagethumbnails.capturing_disabled' : true , // Optimization, via sitespeedio/browsertime
231- 'browser.search.region' : 'US' , // Optimization: Avoid internal geoip lookup
232- 'browser.sessionstore.enabled' : false , // Optimization
233- 'dom.min_background_timeout_value' : 10 , // Optimization, via https://github.com/karma-runner/karma-firefox-launcher/issues/19
234- } ) ) ;
203+ // TODO: Support launching Firefox on Windows from inside WSL
204+ // if (LocalBrowser.isWsl()) { }
205+ } ;
235206
236- try {
237- await LocalBrowser . spawn ( this . getCandidates ( ) , args , signal , logger ) ;
238- } finally {
239- fs . rmSync ( profileDir , { recursive : true , force : true } ) ;
240- }
207+ firefox . createPrefsJs = function ( prefs ) {
208+ let js = '' ;
209+ for ( const key in prefs ) {
210+ js += 'user_pref("' + key + '", ' + JSON . stringify ( prefs [ key ] ) + ');\n' ;
241211 }
242- }
212+ return js ;
213+ } ;
243214
244215export default {
245216 LocalBrowser,
246217
247- firefox : FirefoxBrowser ,
218+ firefox,
248219 // https://github.com/vweevers/win-detect-browsers/blob/v7.0.0/lib/browsers.js
249220 //
250221 // TODO: safari: [],
0 commit comments