From 191c8fefb0b01289df366853e69ab61dfa458533 Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Fri, 3 Oct 2025 15:45:37 -0400 Subject: [PATCH 01/16] chore: start launcher vitest conversion and conver browsers spec to vitest --- guides/esm-migration.md | 2 +- .../__snapshots__/browsers_spec.ts.js | 123 ---------------- packages/launcher/package.json | 11 +- packages/launcher/test/spec_helper.ts | 10 -- .../unit/__snapshots__/browsers.spec.ts.snap | 131 ++++++++++++++++++ .../{browsers_spec.ts => browsers.spec.ts} | 21 ++- packages/launcher/vitest.config.ts | 9 ++ yarn.lock | 18 +-- 8 files changed, 158 insertions(+), 167 deletions(-) delete mode 100644 packages/launcher/__snapshots__/browsers_spec.ts.js delete mode 100644 packages/launcher/test/spec_helper.ts create mode 100644 packages/launcher/test/unit/__snapshots__/browsers.spec.ts.snap rename packages/launcher/test/unit/{browsers_spec.ts => browsers.spec.ts} (80%) create mode 100644 packages/launcher/vitest.config.ts diff --git a/guides/esm-migration.md b/guides/esm-migration.md index f3fbae0fd9a..e0ce58c6380 100644 --- a/guides/esm-migration.md +++ b/guides/esm-migration.md @@ -100,7 +100,7 @@ When migrating some of these projects away from the `ts-node` entry [see `@packa - [x] packages/electron ✅ **COMPLETED** - [ ] packages/https-proxy - [ ] packages/icons -- [ ] packages/launcher +- [x] packages/launcher ✅ **COMPLETED** - [ ] packages/net-stubbing - [x] packages/network ✅ **COMPLETED** - [ ] packages/packherd-require diff --git a/packages/launcher/__snapshots__/browsers_spec.ts.js b/packages/launcher/__snapshots__/browsers_spec.ts.js deleted file mode 100644 index b44419587a1..00000000000 --- a/packages/launcher/__snapshots__/browsers_spec.ts.js +++ /dev/null @@ -1,123 +0,0 @@ -exports['browsers returns the expected list of browsers 1'] = [ - { - 'name': 'chrome', - 'family': 'chromium', - 'channel': 'stable', - 'displayName': 'Chrome', - 'versionRegex': {}, - 'binary': [ - 'google-chrome', - 'chrome', - 'google-chrome-stable', - ], - }, - { - 'name': 'chrome', - 'family': 'chromium', - 'channel': 'beta', - 'displayName': 'Chrome Beta', - 'versionRegex': {}, - 'binary': 'google-chrome-beta', - }, - { - 'name': 'chrome', - 'family': 'chromium', - 'channel': 'canary', - 'displayName': 'Chrome Canary', - 'versionRegex': {}, - 'binary': 'google-chrome-canary', - }, - { - 'name': 'chrome-for-testing', - 'family': 'chromium', - 'channel': 'stable', - 'displayName': 'Chrome for Testing', - 'versionRegex': {}, - 'binary': 'chrome', - }, - { - 'name': 'chromium', - 'family': 'chromium', - 'channel': 'stable', - 'displayName': 'Chromium', - 'versionRegex': {}, - 'binary': [ - 'chromium-browser', - 'chromium', - ], - }, - { - 'name': 'firefox', - 'family': 'firefox', - 'channel': 'stable', - 'displayName': 'Firefox', - 'versionRegex': {}, - 'binary': 'firefox', - }, - { - 'name': 'firefox', - 'family': 'firefox', - 'channel': 'dev', - 'displayName': 'Firefox Developer Edition', - 'versionRegex': {}, - 'binary': [ - 'firefox-developer-edition', - 'firefox', - ], - }, - { - 'name': 'firefox', - 'family': 'firefox', - 'channel': 'nightly', - 'displayName': 'Firefox Nightly', - 'versionRegex': {}, - 'binary': [ - 'firefox-nightly', - 'firefox-trunk', - ], - }, - { - 'name': 'edge', - 'family': 'chromium', - 'channel': 'stable', - 'displayName': 'Edge', - 'versionRegex': {}, - 'binary': [ - 'edge', - 'microsoft-edge', - ], - }, - { - 'name': 'edge', - 'family': 'chromium', - 'channel': 'beta', - 'displayName': 'Edge Beta', - 'versionRegex': {}, - 'binary': [ - 'edge-beta', - 'microsoft-edge-beta', - ], - }, - { - 'name': 'edge', - 'family': 'chromium', - 'channel': 'canary', - 'displayName': 'Edge Canary', - 'versionRegex': {}, - 'binary': [ - 'edge-canary', - 'microsoft-edge-canary', - ], - }, - { - 'name': 'edge', - 'family': 'chromium', - 'channel': 'dev', - 'displayName': 'Edge Dev', - 'versionRegex': {}, - 'binary': [ - 'edge-dev', - 'microsoft-edge-dev', - ], - }, -] diff --git a/packages/launcher/package.json b/packages/launcher/package.json index 40f45053fa6..1c4f334f1ce 100644 --- a/packages/launcher/package.json +++ b/packages/launcher/package.json @@ -11,7 +11,8 @@ "lint": "eslint --ext .js,.jsx,.ts,.tsx,.json, .", "size": "t=\"cypress-v0.0.0.tgz\"; yarn pack --filename \"${t}\"; wc -c \"cli/${t}\"; tar tvf \"${t}\"; rm \"${t}\";", "test": "yarn test-unit", - "test-unit": "mocha --reporter mocha-multi-reporters --reporter-options configFile=../../mocha-reporter-config.json", + "test-debug": "vitest --inspect-brk --no-file-parallelism --test-timeout=0", + "test-unit": "vitest run", "tslint": "tslint --config ../ts/tslint.json --project ." }, "dependencies": { @@ -29,13 +30,9 @@ "@packages/data-context": "0.0.0-development", "@packages/ts": "0.0.0-development", "@packages/types": "0.0.0-development", - "chai": "3.5.0", - "chai-as-promised": "7.1.1", - "mocha": "3.5.3", "mock-fs": "5.4.0", - "sinon": "^10.0.0", - "sinon-chai": "3.7.0", - "typescript": "~5.4.5" + "typescript": "~5.4.5", + "vitest": "^3.2.4" }, "files": [ "index.js", diff --git a/packages/launcher/test/spec_helper.ts b/packages/launcher/test/spec_helper.ts deleted file mode 100644 index 9c18fb9bd66..00000000000 --- a/packages/launcher/test/spec_helper.ts +++ /dev/null @@ -1,10 +0,0 @@ -import chai from 'chai' -import sinon from 'sinon' -import 'sinon-chai' -import chaiAsPromised from 'chai-as-promised' - -chai.use(chaiAsPromised) - -afterEach(() => { - sinon.restore() -}) diff --git a/packages/launcher/test/unit/__snapshots__/browsers.spec.ts.snap b/packages/launcher/test/unit/__snapshots__/browsers.spec.ts.snap new file mode 100644 index 00000000000..b4133413e0c --- /dev/null +++ b/packages/launcher/test/unit/__snapshots__/browsers.spec.ts.snap @@ -0,0 +1,131 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`browsers > returns the expected list of browsers 1`] = ` +[ + { + "binary": [ + "google-chrome", + "chrome", + "google-chrome-stable", + ], + "channel": "stable", + "displayName": "Chrome", + "family": "chromium", + "name": "chrome", + "validator": [Function], + "versionRegex": /Google Chrome\\(\\?! for Testing\\) \\(\\\\S\\+\\)/m, + }, + { + "binary": "google-chrome-beta", + "channel": "beta", + "displayName": "Chrome Beta", + "family": "chromium", + "name": "chrome", + "versionRegex": /Google Chrome \\(\\\\S\\+\\) beta/m, + }, + { + "binary": "google-chrome-canary", + "channel": "canary", + "displayName": "Chrome Canary", + "family": "chromium", + "name": "chrome", + "versionRegex": /Google Chrome Canary \\(\\\\S\\+\\)/m, + }, + { + "binary": "chrome", + "channel": "stable", + "displayName": "Chrome for Testing", + "family": "chromium", + "name": "chrome-for-testing", + "versionRegex": /Google Chrome for Testing \\(\\\\S\\+\\)/m, + }, + { + "binary": [ + "chromium-browser", + "chromium", + ], + "channel": "stable", + "displayName": "Chromium", + "family": "chromium", + "name": "chromium", + "versionRegex": /Chromium \\(\\\\S\\+\\)/m, + }, + { + "binary": "firefox", + "channel": "stable", + "displayName": "Firefox", + "family": "firefox", + "name": "firefox", + "validator": [Function], + "versionRegex": /\\^Mozilla Firefox \\(\\[\\^\\\\sab\\]\\+\\)\\$/m, + }, + { + "binary": [ + "firefox-developer-edition", + "firefox", + ], + "channel": "dev", + "displayName": "Firefox Developer Edition", + "family": "firefox", + "name": "firefox", + "validator": [Function], + "versionRegex": /\\^Mozilla Firefox \\(\\\\S\\+b\\\\S\\*\\)\\$/m, + }, + { + "binary": [ + "firefox-nightly", + "firefox-trunk", + ], + "channel": "nightly", + "displayName": "Firefox Nightly", + "family": "firefox", + "name": "firefox", + "validator": [Function], + "versionRegex": /\\^Mozilla Firefox \\(\\\\S\\+a\\\\S\\*\\)\\$/m, + }, + { + "binary": [ + "edge", + "microsoft-edge", + ], + "channel": "stable", + "displayName": "Edge", + "family": "chromium", + "name": "edge", + "versionRegex": /Microsoft Edge \\(\\\\S\\+\\)/im, + }, + { + "binary": [ + "edge-beta", + "microsoft-edge-beta", + ], + "channel": "beta", + "displayName": "Edge Beta", + "family": "chromium", + "name": "edge", + "versionRegex": /Microsoft Edge\\.\\+\\?\\(\\\\S\\*\\(\\?= beta\\)\\|\\(\\?<=beta \\)\\\\S\\*\\)/im, + }, + { + "binary": [ + "edge-canary", + "microsoft-edge-canary", + ], + "channel": "canary", + "displayName": "Edge Canary", + "family": "chromium", + "name": "edge", + "versionRegex": /Microsoft Edge\\.\\+\\?\\(\\\\S\\*\\(\\?= canary\\)\\|\\(\\?<=canary \\)\\\\S\\*\\)/im, + }, + { + "binary": [ + "edge-dev", + "microsoft-edge-dev", + ], + "channel": "dev", + "displayName": "Edge Dev", + "family": "chromium", + "name": "edge", + "versionRegex": /Microsoft Edge\\.\\+\\?\\(\\\\S\\*\\(\\?= dev\\)\\|\\(\\?<=dev \\)\\\\S\\*\\)/im, + }, +] +`; diff --git a/packages/launcher/test/unit/browsers_spec.ts b/packages/launcher/test/unit/browsers.spec.ts similarity index 80% rename from packages/launcher/test/unit/browsers_spec.ts rename to packages/launcher/test/unit/browsers.spec.ts index 19f8f9ba808..edfdb2ad1ca 100644 --- a/packages/launcher/test/unit/browsers_spec.ts +++ b/packages/launcher/test/unit/browsers.spec.ts @@ -1,18 +1,17 @@ +import { describe, it, expect } from 'vitest' import _ from 'lodash' import { knownBrowsers } from '../../lib/known-browsers' -import { expect } from 'chai' -const snapshot = require('snap-shot-it') describe('browsers', () => { it('returns the expected list of browsers', () => { - snapshot(knownBrowsers) + expect(knownBrowsers).toMatchSnapshot() }) // https://github.com/cypress-io/cypress/issues/6669 it('exports multiline versionRegexes', () => { expect(_.every(knownBrowsers.map(({ versionRegex }) => { return versionRegex.multiline - }))).to.be.true + }))).toBe(true) }) describe('browser.validator', () => { @@ -21,7 +20,7 @@ describe('browsers', () => { path: '/path/to/firefox', } - context('validator defined', () => { + describe('validator defined', () => { it('when conditions met: marks browser as not supported and generates warning message', () => { const foundBrowser = { ...firefoxBrowser, @@ -43,8 +42,8 @@ describe('browsers', () => { const result = foundBrowser.validator(foundBrowser, 'win32') - expect(result.isSupported).to.be.false - expect(result.warningMessage).to.contain('Cypress does not support running Firefox version 101 on Windows due to a blocking bug in Firefox.') + expect(result.isSupported).toBe(false) + expect(result.warningMessage).toContain('Cypress does not support running Firefox version 101 on Windows due to a blocking bug in Firefox.') }) it('when conditions not met: marks browser as not supported and generates warning message', () => { @@ -68,8 +67,8 @@ describe('browsers', () => { const result = foundBrowser.validator(foundBrowser, 'win32') - expect(result.isSupported).to.be.true - expect(result.warningMessage).to.be.undefined + expect(result.isSupported).toBe(true) + expect(result.warningMessage).toBeUndefined() }) describe('firefox validation', () => { @@ -85,8 +84,8 @@ describe('browsers', () => { displayName: 'Firefox', }) - expect(result.isSupported).to.be.false - expect(result.warningMessage).to.equal('Cypress does not support running Firefox version 134 due to lack of WebDriver BiDi support. To use Firefox with Cypress, install version 135 or newer.') + expect(result.isSupported).toBe(false) + expect(result.warningMessage).toEqual('Cypress does not support running Firefox version 134 due to lack of WebDriver BiDi support. To use Firefox with Cypress, install version 135 or newer.') }) }) }) diff --git a/packages/launcher/vitest.config.ts b/packages/launcher/vitest.config.ts new file mode 100644 index 00000000000..1a9a321880f --- /dev/null +++ b/packages/launcher/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['test/**/*.spec.ts'], + globals: true, + environment: 'node', + }, +}) diff --git a/yarn.lock b/yarn.lock index 6ac4e67f682..930aebcae07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7107,7 +7107,7 @@ resolved "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz#719df7fb41766bc143369eaa0dd56d8dc87c9958" integrity sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg== -"@sinonjs/commons@^1", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.4.0", "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.7.2", "@sinonjs/commons@^1.8.1", "@sinonjs/commons@^1.8.3": +"@sinonjs/commons@^1", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.4.0", "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.7.2", "@sinonjs/commons@^1.8.3": version "1.8.6" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9" integrity sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ== @@ -7205,7 +7205,7 @@ lodash.get "^4.4.2" type-detect "^4.0.8" -"@sinonjs/samsam@^5.0.2", "@sinonjs/samsam@^5.0.3", "@sinonjs/samsam@^5.3.1": +"@sinonjs/samsam@^5.0.2", "@sinonjs/samsam@^5.0.3": version "5.3.1" resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.3.1.tgz#375a45fe6ed4e92fca2fb920e007c48232a6507f" integrity sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg== @@ -24494,7 +24494,7 @@ nise@^3.0.1: lolex "^5.0.1" path-to-regexp "^1.7.0" -nise@^4.0.1, nise@^4.1.0: +nise@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/nise/-/nise-4.1.0.tgz#8fb75a26e90b99202fa1e63f448f58efbcdedaf6" integrity sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA== @@ -29683,18 +29683,6 @@ sinon@8.1.1: nise "^3.0.1" supports-color "^7.1.0" -sinon@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-10.0.0.tgz#52279f97e35646ff73d23207d0307977c9b81430" - integrity sha512-XAn5DxtGVJBlBWYrcYKEhWCz7FLwZGdyvANRyK06419hyEpdT0dMc5A8Vcxg5SCGHc40CsqoKsc1bt1CbJPfNw== - dependencies: - "@sinonjs/commons" "^1.8.1" - "@sinonjs/fake-timers" "^6.0.1" - "@sinonjs/samsam" "^5.3.1" - diff "^4.0.2" - nise "^4.1.0" - supports-color "^7.1.0" - sinon@^9.0.0: version "9.0.2" resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.0.2.tgz#b9017e24633f4b1c98dfb6e784a5f0509f5fd85d" From 0aa252e3c7631661fe378d6014a3dbb01f298d53 Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Sat, 4 Oct 2025 22:16:08 -0400 Subject: [PATCH 02/16] chore: convert darwin spec to vitest --- .../launcher/__snapshots__/darwin_spec.ts.js | 219 ----------------- packages/launcher/lib/darwin/util.ts | 6 +- .../unit/__snapshots__/darwin.spec.ts.snap | 227 ++++++++++++++++++ packages/launcher/test/unit/darwin.spec.ts | 208 ++++++++++++++++ packages/launcher/test/unit/darwin_spec.ts | 175 -------------- 5 files changed, 438 insertions(+), 397 deletions(-) delete mode 100644 packages/launcher/__snapshots__/darwin_spec.ts.js create mode 100644 packages/launcher/test/unit/__snapshots__/darwin.spec.ts.snap create mode 100644 packages/launcher/test/unit/darwin.spec.ts delete mode 100644 packages/launcher/test/unit/darwin_spec.ts diff --git a/packages/launcher/__snapshots__/darwin_spec.ts.js b/packages/launcher/__snapshots__/darwin_spec.ts.js deleted file mode 100644 index 8b20e11f91d..00000000000 --- a/packages/launcher/__snapshots__/darwin_spec.ts.js +++ /dev/null @@ -1,219 +0,0 @@ -exports['darwin browser detection detects browsers as expected 1'] = [ - { - 'name': 'chrome', - 'family': 'chromium', - 'channel': 'stable', - 'displayName': 'Chrome', - 'versionRegex': {}, - 'binary': [ - 'google-chrome', - 'chrome', - 'google-chrome-stable', - ], - 'path': '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', - 'version': 'someVersion', - 'findAppParams': { - 'appName': 'Google Chrome.app', - 'executable': 'Contents/MacOS/Google Chrome', - 'bundleId': 'com.google.Chrome', - 'versionProperty': 'KSVersion', - }, - }, - { - 'name': 'chrome', - 'family': 'chromium', - 'channel': 'beta', - 'displayName': 'Chrome Beta', - 'versionRegex': {}, - 'binary': 'google-chrome-beta', - 'path': '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta', - 'version': 'someVersion', - 'findAppParams': { - 'appName': 'Google Chrome Beta.app', - 'executable': 'Contents/MacOS/Google Chrome Beta', - 'bundleId': 'com.google.Chrome.beta', - 'versionProperty': 'KSVersion', - }, - }, - { - 'name': 'chrome', - 'family': 'chromium', - 'channel': 'canary', - 'displayName': 'Chrome Canary', - 'versionRegex': {}, - 'binary': 'google-chrome-canary', - 'path': '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', - 'version': 'someVersion', - 'findAppParams': { - 'appName': 'Google Chrome Canary.app', - 'executable': 'Contents/MacOS/Google Chrome Canary', - 'bundleId': 'com.google.Chrome.canary', - 'versionProperty': 'KSVersion', - }, - }, - { - 'name': 'chrome-for-testing', - 'family': 'chromium', - 'channel': 'stable', - 'displayName': 'Chrome for Testing', - 'versionRegex': {}, - 'binary': 'chrome', - 'path': '/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing', - 'version': 'someVersion', - 'findAppParams': { - 'appName': 'Google Chrome for Testing.app', - 'executable': 'Contents/MacOS/Google Chrome for Testing', - 'bundleId': 'com.google.chrome.for.testing', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, - { - 'name': 'chromium', - 'family': 'chromium', - 'channel': 'stable', - 'displayName': 'Chromium', - 'versionRegex': {}, - 'binary': [ - 'chromium-browser', - 'chromium', - ], - 'path': '/Applications/Chromium.app/Contents/MacOS/Chromium', - 'version': 'someVersion', - 'findAppParams': { - 'appName': 'Chromium.app', - 'executable': 'Contents/MacOS/Chromium', - 'bundleId': 'org.chromium.Chromium', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, - { - 'name': 'firefox', - 'family': 'firefox', - 'channel': 'stable', - 'displayName': 'Firefox', - 'versionRegex': {}, - 'binary': 'firefox', - 'path': '/Applications/Firefox.app/Contents/MacOS/firefox', - 'version': 'someVersion', - 'findAppParams': { - 'appName': 'Firefox.app', - 'executable': 'Contents/MacOS/firefox', - 'bundleId': 'org.mozilla.firefox', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, - { - 'name': 'firefox', - 'family': 'firefox', - 'channel': 'dev', - 'displayName': 'Firefox Developer Edition', - 'versionRegex': {}, - 'binary': [ - 'firefox-developer-edition', - 'firefox', - ], - 'path': '/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox', - 'version': 'someVersion', - 'findAppParams': { - 'appName': 'Firefox Developer Edition.app', - 'executable': 'Contents/MacOS/firefox', - 'bundleId': 'org.mozilla.firefoxdeveloperedition', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, - { - 'name': 'firefox', - 'family': 'firefox', - 'channel': 'nightly', - 'displayName': 'Firefox Nightly', - 'versionRegex': {}, - 'binary': [ - 'firefox-nightly', - 'firefox-trunk', - ], - 'path': '/Applications/Firefox Nightly.app/Contents/MacOS/firefox', - 'version': 'someVersion', - 'findAppParams': { - 'appName': 'Firefox Nightly.app', - 'executable': 'Contents/MacOS/firefox', - 'bundleId': 'org.mozilla.nightly', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, - { - 'name': 'edge', - 'family': 'chromium', - 'channel': 'stable', - 'displayName': 'Edge', - 'versionRegex': {}, - 'binary': [ - 'edge', - 'microsoft-edge', - ], - 'path': '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', - 'version': 'someVersion', - 'findAppParams': { - 'appName': 'Microsoft Edge.app', - 'executable': 'Contents/MacOS/Microsoft Edge', - 'bundleId': 'com.microsoft.Edge', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, - { - 'name': 'edge', - 'family': 'chromium', - 'channel': 'beta', - 'displayName': 'Edge Beta', - 'versionRegex': {}, - 'binary': [ - 'edge-beta', - 'microsoft-edge-beta', - ], - 'path': '/Applications/Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta', - 'version': 'someVersion', - 'findAppParams': { - 'appName': 'Microsoft Edge Beta.app', - 'executable': 'Contents/MacOS/Microsoft Edge Beta', - 'bundleId': 'com.microsoft.Edge.Beta', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, - { - 'name': 'edge', - 'family': 'chromium', - 'channel': 'canary', - 'displayName': 'Edge Canary', - 'versionRegex': {}, - 'binary': [ - 'edge-canary', - 'microsoft-edge-canary', - ], - 'path': '/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary', - 'version': 'someVersion', - 'findAppParams': { - 'appName': 'Microsoft Edge Canary.app', - 'executable': 'Contents/MacOS/Microsoft Edge Canary', - 'bundleId': 'com.microsoft.Edge.Canary', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, - { - 'name': 'edge', - 'family': 'chromium', - 'channel': 'dev', - 'displayName': 'Edge Dev', - 'versionRegex': {}, - 'binary': [ - 'edge-dev', - 'microsoft-edge-dev', - ], - 'path': '/Applications/Microsoft Edge Dev.app/Contents/MacOS/Microsoft Edge Dev', - 'version': 'someVersion', - 'findAppParams': { - 'appName': 'Microsoft Edge Dev.app', - 'executable': 'Contents/MacOS/Microsoft Edge Dev', - 'bundleId': 'com.microsoft.Edge.Dev', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, -] diff --git a/packages/launcher/lib/darwin/util.ts b/packages/launcher/lib/darwin/util.ts index 2e87bc4b709..da01b3d0a45 100644 --- a/packages/launcher/lib/darwin/util.ts +++ b/packages/launcher/lib/darwin/util.ts @@ -1,9 +1,9 @@ import Debug from 'debug' import { notInstalledErr } from '../errors' import { utils } from '../utils' -import * as fs from 'fs-extra' -import * as path from 'path' -import * as plist from 'plist' +import fs from 'fs-extra' +import path from 'path' +import plist from 'plist' const debugVerbose = Debug('cypress-verbose:launcher:darwin:util') diff --git a/packages/launcher/test/unit/__snapshots__/darwin.spec.ts.snap b/packages/launcher/test/unit/__snapshots__/darwin.spec.ts.snap new file mode 100644 index 00000000000..c8039cb57e6 --- /dev/null +++ b/packages/launcher/test/unit/__snapshots__/darwin.spec.ts.snap @@ -0,0 +1,227 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`darwin browser detection > detects browsers as expected 1`] = ` +[ + { + "binary": [ + "google-chrome", + "chrome", + "google-chrome-stable", + ], + "channel": "stable", + "displayName": "Chrome", + "family": "chromium", + "findAppParams": { + "appName": "Google Chrome.app", + "bundleId": "com.google.Chrome", + "executable": "Contents/MacOS/Google Chrome", + "versionProperty": "KSVersion", + }, + "name": "chrome", + "path": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "validator": [Function], + "version": "someVersion", + "versionRegex": /Google Chrome\\(\\?! for Testing\\) \\(\\\\S\\+\\)/m, + }, + { + "binary": "google-chrome-beta", + "channel": "beta", + "displayName": "Chrome Beta", + "family": "chromium", + "findAppParams": { + "appName": "Google Chrome Beta.app", + "bundleId": "com.google.Chrome.beta", + "executable": "Contents/MacOS/Google Chrome Beta", + "versionProperty": "KSVersion", + }, + "name": "chrome", + "path": "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta", + "version": "someVersion", + "versionRegex": /Google Chrome \\(\\\\S\\+\\) beta/m, + }, + { + "binary": "google-chrome-canary", + "channel": "canary", + "displayName": "Chrome Canary", + "family": "chromium", + "findAppParams": { + "appName": "Google Chrome Canary.app", + "bundleId": "com.google.Chrome.canary", + "executable": "Contents/MacOS/Google Chrome Canary", + "versionProperty": "KSVersion", + }, + "name": "chrome", + "path": "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", + "version": "someVersion", + "versionRegex": /Google Chrome Canary \\(\\\\S\\+\\)/m, + }, + { + "binary": "chrome", + "channel": "stable", + "displayName": "Chrome for Testing", + "family": "chromium", + "findAppParams": { + "appName": "Google Chrome for Testing.app", + "bundleId": "com.google.chrome.for.testing", + "executable": "Contents/MacOS/Google Chrome for Testing", + "versionProperty": "CFBundleShortVersionString", + }, + "name": "chrome-for-testing", + "path": "/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing", + "version": "someVersion", + "versionRegex": /Google Chrome for Testing \\(\\\\S\\+\\)/m, + }, + { + "binary": [ + "chromium-browser", + "chromium", + ], + "channel": "stable", + "displayName": "Chromium", + "family": "chromium", + "findAppParams": { + "appName": "Chromium.app", + "bundleId": "org.chromium.Chromium", + "executable": "Contents/MacOS/Chromium", + "versionProperty": "CFBundleShortVersionString", + }, + "name": "chromium", + "path": "/Applications/Chromium.app/Contents/MacOS/Chromium", + "version": "someVersion", + "versionRegex": /Chromium \\(\\\\S\\+\\)/m, + }, + { + "binary": "firefox", + "channel": "stable", + "displayName": "Firefox", + "family": "firefox", + "findAppParams": { + "appName": "Firefox.app", + "bundleId": "org.mozilla.firefox", + "executable": "Contents/MacOS/firefox", + "versionProperty": "CFBundleShortVersionString", + }, + "name": "firefox", + "path": "/Applications/Firefox.app/Contents/MacOS/firefox", + "validator": [Function], + "version": "someVersion", + "versionRegex": /\\^Mozilla Firefox \\(\\[\\^\\\\sab\\]\\+\\)\\$/m, + }, + { + "binary": [ + "firefox-developer-edition", + "firefox", + ], + "channel": "dev", + "displayName": "Firefox Developer Edition", + "family": "firefox", + "findAppParams": { + "appName": "Firefox Developer Edition.app", + "bundleId": "org.mozilla.firefoxdeveloperedition", + "executable": "Contents/MacOS/firefox", + "versionProperty": "CFBundleShortVersionString", + }, + "name": "firefox", + "path": "/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox", + "validator": [Function], + "version": "someVersion", + "versionRegex": /\\^Mozilla Firefox \\(\\\\S\\+b\\\\S\\*\\)\\$/m, + }, + { + "binary": [ + "firefox-nightly", + "firefox-trunk", + ], + "channel": "nightly", + "displayName": "Firefox Nightly", + "family": "firefox", + "findAppParams": { + "appName": "Firefox Nightly.app", + "bundleId": "org.mozilla.nightly", + "executable": "Contents/MacOS/firefox", + "versionProperty": "CFBundleShortVersionString", + }, + "name": "firefox", + "path": "/Applications/Firefox Nightly.app/Contents/MacOS/firefox", + "validator": [Function], + "version": "someVersion", + "versionRegex": /\\^Mozilla Firefox \\(\\\\S\\+a\\\\S\\*\\)\\$/m, + }, + { + "binary": [ + "edge", + "microsoft-edge", + ], + "channel": "stable", + "displayName": "Edge", + "family": "chromium", + "findAppParams": { + "appName": "Microsoft Edge.app", + "bundleId": "com.microsoft.Edge", + "executable": "Contents/MacOS/Microsoft Edge", + "versionProperty": "CFBundleShortVersionString", + }, + "name": "edge", + "path": "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", + "version": "someVersion", + "versionRegex": /Microsoft Edge \\(\\\\S\\+\\)/im, + }, + { + "binary": [ + "edge-beta", + "microsoft-edge-beta", + ], + "channel": "beta", + "displayName": "Edge Beta", + "family": "chromium", + "findAppParams": { + "appName": "Microsoft Edge Beta.app", + "bundleId": "com.microsoft.Edge.Beta", + "executable": "Contents/MacOS/Microsoft Edge Beta", + "versionProperty": "CFBundleShortVersionString", + }, + "name": "edge", + "path": "/Applications/Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta", + "version": "someVersion", + "versionRegex": /Microsoft Edge\\.\\+\\?\\(\\\\S\\*\\(\\?= beta\\)\\|\\(\\?<=beta \\)\\\\S\\*\\)/im, + }, + { + "binary": [ + "edge-canary", + "microsoft-edge-canary", + ], + "channel": "canary", + "displayName": "Edge Canary", + "family": "chromium", + "findAppParams": { + "appName": "Microsoft Edge Canary.app", + "bundleId": "com.microsoft.Edge.Canary", + "executable": "Contents/MacOS/Microsoft Edge Canary", + "versionProperty": "CFBundleShortVersionString", + }, + "name": "edge", + "path": "/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary", + "version": "someVersion", + "versionRegex": /Microsoft Edge\\.\\+\\?\\(\\\\S\\*\\(\\?= canary\\)\\|\\(\\?<=canary \\)\\\\S\\*\\)/im, + }, + { + "binary": [ + "edge-dev", + "microsoft-edge-dev", + ], + "channel": "dev", + "displayName": "Edge Dev", + "family": "chromium", + "findAppParams": { + "appName": "Microsoft Edge Dev.app", + "bundleId": "com.microsoft.Edge.Dev", + "executable": "Contents/MacOS/Microsoft Edge Dev", + "versionProperty": "CFBundleShortVersionString", + }, + "name": "edge", + "path": "/Applications/Microsoft Edge Dev.app/Contents/MacOS/Microsoft Edge Dev", + "version": "someVersion", + "versionRegex": /Microsoft Edge\\.\\+\\?\\(\\\\S\\*\\(\\?= dev\\)\\|\\(\\?<=dev \\)\\\\S\\*\\)/im, + }, +] +`; diff --git a/packages/launcher/test/unit/darwin.spec.ts b/packages/launcher/test/unit/darwin.spec.ts new file mode 100644 index 00000000000..71d1467cd60 --- /dev/null +++ b/packages/launcher/test/unit/darwin.spec.ts @@ -0,0 +1,208 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import os from 'os' +import cp from 'child_process' +import fs from 'fs-extra' +import { PassThrough } from 'stream' +import { FoundBrowser } from '@packages/types' +import * as darwinHelper from '../../lib/darwin' +import * as linuxHelper from '../../lib/linux' +import * as darwinUtil from '../../lib/darwin/util' +import { launch } from '../../lib/browsers' +import { knownBrowsers } from '../../lib/known-browsers' + +vi.mock('os', async (importActual) => { + const actual = await importActual() + + return { + default: { + // @ts-expect-error + ...actual.default, + arch: vi.fn(), + platform: vi.fn(), + }, + } +}) + +vi.mock('fs-extra', async (importActual) => { + const actual = await importActual() + + return { + default: { + // @ts-expect-error + ...actual.default, + readFile: vi.fn(), + }, + } +}) + +vi.mock('child_process', async (importActual) => { + const actual = await importActual() + + return { + default: { + // @ts-expect-error + ...actual.default, + spawn: vi.fn(), + }, + } +}) + +function generatePlist (key, value) { + return ` + + + + + ${key} + ${value} + + + ` +} + +describe('darwin browser detection', () => { + beforeEach(() => { + vi.unstubAllEnvs() + vi.resetAllMocks() + vi.mocked(fs.readFile).mockRejectedValue({ code: 'ENOENT' }) + }) + + it('detects browsers as expected', async () => { + // this test uses the macOS detectors to stub out the expected calls + const flatFindAppParams: darwinUtil.FindAppParams[] = [] + + for (const browser in darwinHelper.browsers) { + for (const channel in darwinHelper.browsers[browser]) { + flatFindAppParams.push(darwinHelper.browsers[browser][channel]) + } + } + + // @ts-expect-error + vi.mocked(fs.readFile).mockImplementation((file: string, _options: any): Promise => { + const foundAppParams = flatFindAppParams.find((findAppParams) => `/Applications/${findAppParams.appName}/Contents/Info.plist` === file) + + if (foundAppParams) { + return Promise.resolve(generatePlist(foundAppParams.versionProperty, 'someVersion')) + } + + throw new Error('File not found') + }) + + const mappedBrowsers = [] + + for (const browser of knownBrowsers) { + const foundBrowser = await darwinHelper.detect(browser) + const findAppParams = darwinHelper.browsers[browser.name][browser.channel] + + mappedBrowsers.push({ + ...browser, + ...foundBrowser, + findAppParams, + }) + } + + expect(mappedBrowsers).toMatchSnapshot() + }) + + it('getVersionString is re-exported from linuxHelper', () => { + expect(darwinHelper.getVersionString).toEqual(linuxHelper.getVersionString) + }) + + describe('forces correct architecture', () => { + beforeEach(() => { + vi.unstubAllEnvs() + vi.stubEnv('env2', 'false') + vi.stubEnv('env3', 'true') + vi.mocked(os.platform).mockReturnValue('darwin') + vi.mocked(cp.spawn).mockImplementation(() => { + const mock: any = { + on: vi.fn(), + once: vi.fn(), + stdout: new PassThrough(), + stderr: new PassThrough(), + kill: vi.fn(), + } + + mock.on.mockImplementation((event: string, callback: (...args: any[]) => void) => { + if (event === 'exit') { + setTimeout(() => callback(), 0) + } + + if (event === 'close') { + setTimeout(() => callback(), 0) + } + }) + + mock.stderr.end() + mock.stdout.end() + + return mock as cp.ChildProcess + }) + }) + + describe('in version detection', () => { + it('uses arch and ARCHPREFERENCE on arm64', async () => { + vi.mocked(os.arch).mockReturnValue('arm64') + + // this will error since we aren't setting stdout + await (darwinHelper.detect(knownBrowsers[0]).catch(() => {})) + + expect(cp.spawn).toHaveBeenNthCalledWith(1, 'arch', [knownBrowsers[0].binary, '--version'], expect.objectContaining({ + env: expect.objectContaining({ + ARCHPREFERENCE: 'arm64,x86_64', + env2: 'false', + env3: 'true', + }), + })) + }) + + it('does not use `arch` on x64', async () => { + vi.mocked(os.arch).mockReturnValue('x64') + + // this will error since we aren't setting stdout + await (darwinHelper.detect(knownBrowsers[0]).catch(() => {})) + + expect(cp.spawn).toHaveBeenNthCalledWith(1, knownBrowsers[0].binary, ['--version'], expect.objectContaining({ + env: expect.objectContaining({ + env2: 'false', + env3: 'true', + }), + })) + }) + }) + + describe('in browser launching', () => { + it('uses arch and ARCHPREFERENCE on arm64', async () => { + vi.mocked(os.arch).mockReturnValue('arm64') + + await launch({ path: 'chrome' } as unknown as FoundBrowser, 'url', 123, ['arg1'], { env1: 'true', env2: 'true' }) + + expect(cp.spawn).toHaveBeenNthCalledWith(1, 'arch', ['chrome', 'url', 'arg1'], expect.objectContaining({ + env: expect.objectContaining({ + ARCHPREFERENCE: 'arm64,x86_64', + env1: 'true', + env2: 'false', + env3: 'true', + }), + })) + }) + + it('does not use `arch` on x64', async () => { + vi.mocked(os.arch).mockReturnValue('x64') + + await launch({ path: 'chrome' } as unknown as FoundBrowser, 'url', 123, ['arg1'], { env1: 'true', env2: 'true' }) + + expect(cp.spawn).toHaveBeenNthCalledWith(1, 'chrome', ['url', 'arg1'], expect.objectContaining({ + env: expect.objectContaining({ + env1: 'true', + env2: 'false', + env3: 'true', + }), + })) + + // @ts-expect-error + expect(cp.spawn.mock.calls[0][2].env).not.toHaveProperty('ARCHPREFERENCE') + }) + }) + }) +}) diff --git a/packages/launcher/test/unit/darwin_spec.ts b/packages/launcher/test/unit/darwin_spec.ts deleted file mode 100644 index 0e03b11904a..00000000000 --- a/packages/launcher/test/unit/darwin_spec.ts +++ /dev/null @@ -1,175 +0,0 @@ -import _ from 'lodash' -import os from 'os' -import cp from 'child_process' -import * as darwinHelper from '../../lib/darwin' -import * as linuxHelper from '../../lib/linux' -import * as darwinUtil from '../../lib/darwin/util' -import { utils } from '../../lib/utils' -import { expect } from 'chai' -import sinon, { SinonStub } from 'sinon' -import { launch } from '../../lib/browsers' -import { knownBrowsers } from '../../lib/known-browsers' -import Bluebird from 'bluebird' -import fse from 'fs-extra' -import snapshot from 'snap-shot-it' -import { PassThrough } from 'stream' -import { FoundBrowser } from '@packages/types' - -function generatePlist (key, value) { - return ` - - - - - ${key} - ${value} - - - ` -} - -function stubBrowser (findAppParams: darwinUtil.FindAppParams) { - (utils.getOutput as unknown as SinonStub) - .withArgs(`mdfind 'kMDItemCFBundleIdentifier=="${findAppParams.bundleId}"' | head -1`) - .resolves({ stdout: `/Applications/${findAppParams.appName}` }) - - ;(fse.readFile as SinonStub) - .withArgs(`/Applications/${findAppParams.appName}/Contents/Info.plist`) - .resolves(generatePlist(findAppParams.versionProperty, 'someVersion')) -} - -function spawnStub () { - const stub: any = { - on: sinon.stub(), - stdout: new PassThrough(), - stderr: new PassThrough(), - kill: sinon.stub(), - } - - stub.once = stub.on - - stub.on.withArgs('exit').callsArgAsync(1) - stub.on.withArgs('close').callsArgAsync(1) - - stub.stderr.end() - stub.stdout.end() - - return stub as cp.ChildProcess -} - -describe('darwin browser detection', () => { - let getOutput: SinonStub - - beforeEach(() => { - sinon.stub(fse, 'readFile').rejects({ code: 'ENOENT' }) - getOutput = sinon.stub(utils, 'getOutput').resolves({ stdout: '' }) - }) - - it('detects browsers as expected', async () => { - // this test uses the macOS detectors to stub out the expected calls - _.forEach(darwinHelper.browsers, (channels) => { - _.forEach(channels, stubBrowser) - }) - - // then, it uses the main browsers list to attempt detection of all browsers, which should succeed - const detected = (await Bluebird.mapSeries(knownBrowsers, (browser) => { - return darwinHelper.detect(browser) - .then((foundBrowser) => { - const findAppParams = darwinHelper.browsers[browser.name][browser.channel] - - return _.merge(browser, foundBrowser, { findAppParams }) - }) - })) - - snapshot(detected) - }) - - it('getVersionString is re-exported from linuxHelper', () => { - expect(darwinHelper.getVersionString).to.eq(linuxHelper.getVersionString) - }) - - context('forces correct architecture', () => { - function stubForArch (arch: 'arm64' | 'x64') { - sinon.stub(process, 'env').value({ env2: 'false', env3: 'true' }) - sinon.stub(os, 'arch').returns(arch) - sinon.stub(os, 'platform').returns('darwin') - getOutput.restore() - - return sinon.stub(cp, 'spawn').returns(spawnStub()) - } - - context('in version detection', () => { - it('uses arch and ARCHPREFERENCE on arm64', async () => { - const cpSpawn = stubForArch('arm64') - - // this will error since we aren't setting stdout - await (darwinHelper.detect(knownBrowsers[0]).catch(() => {})) - - // first call is mdfind, second call is getVersionString - const { args } = cpSpawn.getCall(1) - - expect(args[0]).to.eq('arch') - expect(args[1]).to.deep.eq([knownBrowsers[0].binary, '--version']) - expect(args[2].env).to.deep.include({ - ARCHPREFERENCE: 'arm64,x86_64', - env2: 'false', - env3: 'true', - }) - }) - - it('does not use `arch` on x64', async () => { - const cpSpawn = stubForArch('x64') - - // this will error since we aren't setting stdout - await (darwinHelper.detect(knownBrowsers[0]).catch(() => {})) - - // first call is mdfind, second call is getVersionString - const { args } = cpSpawn.getCall(1) - - expect(args[0]).to.eq(knownBrowsers[0].binary) - expect(args[1]).to.deep.eq(['--version']) - expect(args[2].env).to.deep.include({ - env2: 'false', - env3: 'true', - }) - }) - }) - - context('in browser launching', () => { - it('uses arch and ARCHPREFERENCE on arm64', async () => { - const cpSpawn = stubForArch('arm64') - - await launch({ path: 'chrome' } as unknown as FoundBrowser, 'url', 123, ['arg1'], { env1: 'true', env2: 'true' }) - - const { args } = cpSpawn.getCall(0) - - expect(args[0]).to.eq('arch') - expect(args[1]).to.deep.eq(['chrome', 'url', 'arg1']) - expect(args[2].env).to.deep.include({ - ARCHPREFERENCE: 'arm64,x86_64', - env1: 'true', - env2: 'false', - env3: 'true', - }) - }) - - it('does not use `arch` on x64', async () => { - const cpSpawn = stubForArch('x64') - - await launch({ path: 'chrome' } as unknown as FoundBrowser, 'url', 123, ['arg1'], { env1: 'true', env2: 'true' }) - - const { args } = cpSpawn.getCall(0) - - expect(args[0]).to.eq('chrome') - expect(args[1]).to.deep.eq(['url', 'arg1']) - expect(args[2].env).to.deep.include({ - env1: 'true', - env2: 'false', - env3: 'true', - }) - - expect(args[2].env).to.not.have.property('ARCHPREFERENCE') - }) - }) - }) -}) From 60cd31733dac4ed9dafe3b91a81aea4cc9f68e00 Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Sat, 4 Oct 2025 23:19:19 -0400 Subject: [PATCH 03/16] chore: convert linux spec to vitest --- packages/launcher/test/unit/linux.spec.ts | 320 ++++++++++++++++++++++ packages/launcher/test/unit/linux_spec.ts | 230 ---------------- 2 files changed, 320 insertions(+), 230 deletions(-) create mode 100644 packages/launcher/test/unit/linux.spec.ts delete mode 100644 packages/launcher/test/unit/linux_spec.ts diff --git a/packages/launcher/test/unit/linux.spec.ts b/packages/launcher/test/unit/linux.spec.ts new file mode 100644 index 00000000000..e72db123e88 --- /dev/null +++ b/packages/launcher/test/unit/linux.spec.ts @@ -0,0 +1,320 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import _ from 'lodash' +import cp from 'child_process' +import { EventEmitter } from 'events' +import * as linuxHelper from '../../lib/linux' +import { log } from '../log' +import { detect } from '../../lib/detect' +import { goalBrowsers } from '../fixtures' +import os from 'os' +import mockFs from 'mock-fs' + +vi.mock('os', async (importActual) => { + const actual = await importActual() + + return { + default: { + // @ts-expect-error + ...actual.default, + platform: vi.fn(), + release: vi.fn(), + homedir: vi.fn(), + }, + } +}) + +vi.mock('child_process', async (importActual) => { + const actual = await importActual() + + return { + default: { + // @ts-expect-error + ...actual.default, + spawn: vi.fn(), + }, + } +}) + +describe('linux browser detection', () => { + let cpSpawnCallback: (cmd: string, args: readonly string[], opts, cp: cp.ChildProcess) => void + + beforeEach(() => { + vi.unstubAllEnvs() + vi.resetAllMocks() + + vi.mocked(os.platform).mockReturnValue('linux') + vi.mocked(os.release).mockReturnValue('1.0.0') + + vi.mocked(cp.spawn).mockImplementation((cmd, args, opts) => { + const cpSpawnMock = { + on: vi.fn(), + stdout: new EventEmitter(), + stderr: new EventEmitter(), + kill: vi.fn(), + } + + cpSpawnMock.on.mockImplementation((event: string, callback: (...args: any[]) => void) => { + if (event === 'exit') { + setTimeout(() => callback(), 0) + } + + if (event === 'close') { + setTimeout(() => callback(), 0) + } + }) + + cpSpawnCallback(cmd, args, opts, cpSpawnMock as unknown as cp.ChildProcess) + + return cpSpawnMock as unknown as cp.ChildProcess + }) + }) + + afterEach(() => { + mockFs.restore() + }) + + it('detects browser by running --version', async () => { + const goal = goalBrowsers[0] + + cpSpawnCallback = (cmd, args, opts, cpSpawnMock) => { + if (cmd === 'test-browser') { + setTimeout(() => { + cpSpawnMock.stdout.emit('data', 'test-browser v100.1.2.3') + }, 0) + } + } + + // @ts-expect-error + const browser = await linuxHelper.detect(goal) + + expect(browser).toEqual({ + name: 'test-browser-name', + path: 'test-browser', + version: '100.1.2.3', + }) + }) + + // https://github.com/cypress-io/cypress/pull/7039 + it('sets profilePath on snapcraft chromium', async () => { + vi.mocked(os.homedir).mockReturnValue('/home/foo') + + cpSpawnCallback = (cmd, args, opts, cpSpawnMock) => { + if (cmd === 'chromium') { + setTimeout(() => { + cpSpawnMock.stdout.emit('data', 'Chromium 64.2.3 snap') + }, 0) + } + } + + const [browser] = await detect() + + expect(browser).toEqual({ + channel: 'stable', + name: 'chromium', + family: 'chromium', + displayName: 'Chromium', + majorVersion: '64', + path: 'chromium', + profilePath: '/home/foo/snap/chromium/current', + version: '64.2.3', + }) + }) + + // https://github.com/cypress-io/cypress/issues/19793 + describe('sets profilePath on snapcraft firefox', () => { + const expectedSnapFirefox = { + channel: 'stable', + name: 'firefox', + family: 'firefox', + displayName: 'Firefox', + majorVersion: '135', + path: 'firefox', + profilePath: '/home/foo/snap/firefox/current', + version: '135.0.1', + } + + beforeEach(() => { + cpSpawnCallback = (cmd, args, opts, cpSpawnMock) => { + if (cmd === 'firefox') { + setTimeout(() => { + cpSpawnMock.stdout.emit('data', 'Mozilla Firefox 135.0.1') + }, 0) + } + } + + vi.mocked(os.homedir).mockReturnValue('/home/foo') + }) + + it('with shim script', async () => { + vi.stubEnv('PATH', '/bin') + mockFs({ + '/bin/firefox': mockFs.symlink({ path: '/usr/bin/firefox' }), + '/usr/bin/firefox': mockFs.file({ mode: 0o777, content: 'foo bar foo bar foo bar\nexec /snap/bin/firefox\n' }), + }) + + const [browser] = await detect() + + expect(browser).toEqual(expectedSnapFirefox) + }) + + it('with /snap/bin in path', async () => { + vi.stubEnv('PATH', '/bin:/snap/bin') + mockFs({ + '/snap/bin/firefox': mockFs.file({ mode: 0o777, content: 'binary' }), + }) + + const [browser] = await detect() + + expect(browser).toEqual(expectedSnapFirefox) + }) + + it('with symlink to /snap/bin in path', async () => { + vi.stubEnv('PATH', '/bin') + mockFs({ + '/bin/firefox': mockFs.symlink({ path: '/snap/bin/firefox' }), + '/snap/bin/firefox': mockFs.file({ mode: 0o777, content: 'binary' }), + }) + + const [browser] = await detect() + + expect(browser).toEqual(expectedSnapFirefox) + }) + }) + + // https://github.com/cypress-io/cypress/issues/6669 + it('detects browser if the --version stdout is multiline', async () => { + cpSpawnCallback = (cmd, args, opts, cpSpawnMock) => { + if (cmd === 'multiline-foo') { + setTimeout(() => { + cpSpawnMock.stdout.emit('data', ` + Running without a11y support! + foo-browser v9001.1.2.3 + `) + }, 0) + } + } + + const goal = _.defaults({ binary: 'multiline-foo' }, _.find(goalBrowsers, { name: 'foo-browser' })) + + // @ts-expect-error + const [browser] = await detect([goal]) + + expect(browser).toEqual({ + displayName: 'Foo Browser', + majorVersion: '9001', + name: 'foo-browser', + path: 'multiline-foo', + version: '9001.1.2.3', + }) + }) + + // despite using detect(), this test is in linux/spec instead of detect_spec because it is + // testing side effects that occur within the Linux-specific detect function + // https://github.com/cypress-io/cypress/issues/1400 + it('properly eliminates duplicates', async () => { + const expected = [ + { + displayName: 'Test Browser', + name: 'test-browser-name', + version: '100.1.2.3', + path: 'test-browser', + majorVersion: '100', + }, + { + displayName: 'Foo Browser', + name: 'foo-browser', + version: '100.1.2.3', + path: 'foo-browser', + majorVersion: '100', + }, + ] + + cpSpawnCallback = (cmd, args, opts, cpSpawnMock) => { + if (cmd === 'test-browser') { + setTimeout(() => { + cpSpawnMock.stdout.emit('data', 'test-browser v100.1.2.3') + }, 0) + } + + if (cmd === 'foo-browser') { + setTimeout(() => { + cpSpawnMock.stdout.emit('data', 'foo-browser v100.1.2.3') + }, 0) + } + } + + // @ts-expect-error + const browsers = await detect(goalBrowsers) + + log('Browsers: %o', browsers) + log('Expected browsers: %o', expected) + expect(browsers).toEqual(expected) + }) + + it('considers multiple binary names', async () => { + const goalBrowsers = [ + { + name: 'foo-browser', + versionRegex: /v(\S+)$/, + binary: ['foo-browser', 'foo-bar-browser'], + }, + ] + + const expected = [ + { + name: 'foo-browser', + version: '100.1.2.3', + path: 'foo-browser', + majorVersion: '100', + }, + ] + + cpSpawnCallback = (cmd, args, opts, cpSpawnMock) => { + if (cmd === 'foo-browser' || cmd === 'foo-bar-browser') { + setTimeout(() => { + cpSpawnMock.stdout.emit('data', 'foo-browser v100.1.2.3') + }, 0) + } + } + + //@ts-expect-error + const browsers = await detect(goalBrowsers) + + log('Browsers: %o', browsers) + log('Expected browsers: %o', expected) + expect(browsers).toEqual(expected) + }) + + describe('#getVersionString', () => { + it('runs the command with `--version` and returns trimmed output', async () => { + cpSpawnCallback = (cmd, args, opts, cpSpawnMock) => { + if (cmd === 'foo') { + setTimeout(() => { + cpSpawnMock.stdout.emit('data', ' bar ') + }, 0) + } + } + + const versionString = await linuxHelper.getVersionString('foo') + + expect(versionString).toEqual('bar') + }) + + it('rejects with errors', async () => { + const err = new Error() + + cpSpawnCallback = (cmd, args, opts, cpSpawnMock) => { + if (cmd === 'foo') { + // @ts-expect-error - overriding the mock on this method + cpSpawnMock.on.mockImplementation((event: string, callback: (...args: any[]) => void) => { + if (event === 'error') { + setTimeout(() => callback(err), 0) + } + }) + } + } + + await expect(linuxHelper.getVersionString('foo')).rejects.toThrow(err) + }) + }) +}) diff --git a/packages/launcher/test/unit/linux_spec.ts b/packages/launcher/test/unit/linux_spec.ts deleted file mode 100644 index 429894fc9d6..00000000000 --- a/packages/launcher/test/unit/linux_spec.ts +++ /dev/null @@ -1,230 +0,0 @@ -require('../spec_helper') - -import _ from 'lodash' -import * as linuxHelper from '../../lib/linux' -import 'chai-as-promised' -import { log } from '../log' -import { detect } from '../../lib/detect' -import { goalBrowsers } from '../fixtures' -import { expect } from 'chai' -import { utils } from '../../lib/utils' -import os from 'os' -import sinon, { SinonStub } from 'sinon' -import mockFs from 'mock-fs' - -describe('linux browser detection', () => { - let execa: SinonStub - let cachedEnv = { ...process.env } - - beforeEach(() => { - execa = sinon.stub(utils, 'getOutput') - - sinon.stub(os, 'platform').returns('linux') - sinon.stub(os, 'release').returns('1.0.0') - - execa.withArgs('test-browser', ['--version']) - .resolves({ stdout: 'test-browser v100.1.2.3' }) - - execa.withArgs('foo-browser', ['--version']) - .resolves({ stdout: 'foo-browser v100.1.2.3' }) - - execa.withArgs('foo-bar-browser', ['--version']) - .resolves({ stdout: 'foo-browser v100.1.2.3' }) - - execa.withArgs('/foo/bar/browser', ['--version']) - .resolves({ stdout: 'foo-browser v9001.1.2.3' }) - }) - - afterEach(() => { - Object.assign(process.env, cachedEnv) - mockFs.restore() - sinon.restore() - }) - - it('detects browser by running --version', () => { - const goal = goalBrowsers[0] - const checkBrowser = (browser) => { - expect(browser).to.deep.equal({ - name: 'test-browser-name', - path: 'test-browser', - version: '100.1.2.3', - }) - } - - // @ts-ignore - return linuxHelper.detect(goal).then(checkBrowser) - }) - - // https://github.com/cypress-io/cypress/pull/7039 - it('sets profilePath on snapcraft chromium', () => { - execa.withArgs('chromium', ['--version']) - .resolves({ stdout: 'Chromium 64.2.3 snap' }) - - sinon.stub(os, 'homedir').returns('/home/foo') - - const checkBrowser = ([browser]) => { - expect(browser).to.deep.equal({ - channel: 'stable', - name: 'chromium', - family: 'chromium', - displayName: 'Chromium', - majorVersion: '64', - path: 'chromium', - profilePath: '/home/foo/snap/chromium/current', - version: '64.2.3', - }) - } - - return detect().then(checkBrowser) - }) - - // https://github.com/cypress-io/cypress/issues/19793 - context('sets profilePath on snapcraft firefox', () => { - const expectedSnapFirefox = { - channel: 'stable', - name: 'firefox', - family: 'firefox', - displayName: 'Firefox', - majorVersion: '135', - path: 'firefox', - profilePath: '/home/foo/snap/firefox/current', - version: '135.0.1', - } - - beforeEach(() => { - execa.withArgs('firefox', ['--version']) - .resolves({ stdout: 'Mozilla Firefox 135.0.1' }) - - sinon.stub(os, 'homedir').returns('/home/foo') - }) - - it('with shim script', async () => { - process.env.PATH = '/bin' - mockFs({ - '/bin/firefox': mockFs.symlink({ path: '/usr/bin/firefox' }), - '/usr/bin/firefox': mockFs.file({ mode: 0o777, content: 'foo bar foo bar foo bar\nexec /snap/bin/firefox\n' }), - }) - - const [browser] = await detect() - - expect(browser).to.deep.equal(expectedSnapFirefox) - }) - - it('with /snap/bin in path', async () => { - process.env.PATH = '/bin:/snap/bin' - mockFs({ - '/snap/bin/firefox': mockFs.file({ mode: 0o777, content: 'binary' }), - }) - - const [browser] = await detect() - - expect(browser).to.deep.equal(expectedSnapFirefox) - }) - - it('with symlink to /snap/bin in path', async () => { - process.env.PATH = '/bin' - mockFs({ - '/bin/firefox': mockFs.symlink({ path: '/snap/bin/firefox' }), - '/snap/bin/firefox': mockFs.file({ mode: 0o777, content: 'binary' }), - }) - - const [browser] = await detect() - - expect(browser).to.deep.equal(expectedSnapFirefox) - }) - }) - - // https://github.com/cypress-io/cypress/issues/6669 - it('detects browser if the --version stdout is multiline', () => { - execa.withArgs('multiline-foo', ['--version']) - .resolves({ - stdout: ` - Running without a11y support! - foo-browser v9001.1.2.3 - `, - }) - - const goal = _.defaults({ binary: 'multiline-foo' }, _.find(goalBrowsers, { name: 'foo-browser' })) - const checkBrowser = (browser) => { - expect(browser).to.deep.equal({ - name: 'foo-browser', - path: 'multiline-foo', - version: '9001.1.2.3', - }) - } - - // @ts-ignore - return linuxHelper.detect(goal).then(checkBrowser) - }) - - // despite using detect(), this test is in linux/spec instead of detect_spec because it is - // testing side effects that occur within the Linux-specific detect function - // https://github.com/cypress-io/cypress/issues/1400 - it('properly eliminates duplicates', () => { - const expected = [ - { - displayName: 'Test Browser', - name: 'test-browser-name', - version: '100.1.2.3', - path: 'test-browser', - majorVersion: '100', - }, - { - displayName: 'Foo Browser', - name: 'foo-browser', - version: '100.1.2.3', - path: 'foo-browser', - majorVersion: '100', - }, - ] - - // @ts-ignore - return detect(goalBrowsers).then((browsers) => { - log('Browsers: %o', browsers) - log('Expected browsers: %o', expected) - expect(browsers).to.deep.equal(expected) - }) - }) - - it('considers multiple binary names', () => { - const goalBrowsers = [ - { - name: 'foo-browser', - versionRegex: /v(\S+)$/, - binary: ['foo-browser', 'foo-bar-browser'], - }, - ] - - const expected = [ - { - name: 'foo-browser', - version: '100.1.2.3', - path: 'foo-browser', - majorVersion: '100', - }, - ] - - // @ts-ignore - return detect(goalBrowsers).then((browsers) => { - log('Browsers: %o', browsers) - log('Expected browsers: %o', expected) - expect(browsers).to.deep.equal(expected) - }) - }) - - context('#getVersionString', () => { - it('runs the command with `--version` and returns trimmed output', async () => { - execa.withArgs('foo', ['--version']).resolves({ stdout: ' bar ' }) - - expect(await linuxHelper.getVersionString('foo')).to.eq('bar') - }) - - it('rejects with errors', async () => { - const err = new Error() - - execa.withArgs('foo', ['--version']).rejects(err) - - await expect(linuxHelper.getVersionString('foo')).to.be.rejectedWith(err) - }) - }) -}) From 5a62b5eed7bcb3fe7f2d6d0310b191bc9f84c21a Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Sun, 5 Oct 2025 22:38:15 -0400 Subject: [PATCH 04/16] chore: convert windows spec to vitest --- .../launcher/__snapshots__/windows_spec.ts.js | 403 -------------- packages/launcher/lib/windows/index.ts | 12 +- .../unit/__snapshots__/windows.spec.ts.snap | 291 ++++++++++ packages/launcher/test/unit/windows.spec.ts | 525 ++++++++++++++++++ packages/launcher/test/unit/windows_spec.ts | 307 ---------- 5 files changed, 822 insertions(+), 716 deletions(-) delete mode 100644 packages/launcher/__snapshots__/windows_spec.ts.js create mode 100644 packages/launcher/test/unit/__snapshots__/windows.spec.ts.snap create mode 100644 packages/launcher/test/unit/windows.spec.ts delete mode 100644 packages/launcher/test/unit/windows_spec.ts diff --git a/packages/launcher/__snapshots__/windows_spec.ts.js b/packages/launcher/__snapshots__/windows_spec.ts.js deleted file mode 100644 index e6e106b341d..00000000000 --- a/packages/launcher/__snapshots__/windows_spec.ts.js +++ /dev/null @@ -1,403 +0,0 @@ -exports['windows browser detection detects browsers as expected 1'] = [ - { - 'name': 'chrome', - 'family': 'chromium', - 'channel': 'stable', - 'displayName': 'Chrome', - 'versionRegex': {}, - 'binary': [ - 'google-chrome', - 'chrome', - 'google-chrome-stable', - ], - 'path': 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe', - 'version': '1.2.3', - 'findAppParams': { - 'appName': 'Google Chrome.app', - 'executable': 'Contents/MacOS/Google Chrome', - 'bundleId': 'com.google.Chrome', - 'versionProperty': 'KSVersion', - }, - }, - { - 'name': 'chrome', - 'family': 'chromium', - 'channel': 'beta', - 'displayName': 'Chrome Beta', - 'versionRegex': {}, - 'binary': 'google-chrome-beta', - 'path': 'C:/Program Files (x86)/Google/Chrome Beta/Application/chrome.exe', - 'version': '6.7.8', - 'findAppParams': { - 'appName': 'Google Chrome Beta.app', - 'executable': 'Contents/MacOS/Google Chrome Beta', - 'bundleId': 'com.google.Chrome.beta', - 'versionProperty': 'KSVersion', - }, - }, - { - 'name': 'chrome', - 'family': 'chromium', - 'channel': 'canary', - 'displayName': 'Chrome Canary', - 'versionRegex': {}, - 'binary': 'google-chrome-canary', - 'path': 'C:/Users/flotwig/AppData/Local/Google/Chrome SxS/Application/chrome.exe', - 'version': '3.4.5', - 'findAppParams': { - 'appName': 'Google Chrome Canary.app', - 'executable': 'Contents/MacOS/Google Chrome Canary', - 'bundleId': 'com.google.Chrome.canary', - 'versionProperty': 'KSVersion', - }, - }, - { - 'name': 'chrome-for-testing', - 'family': 'chromium', - 'channel': 'stable', - 'displayName': 'Chrome for Testing', - 'versionRegex': {}, - 'binary': 'chrome', - 'path': 'C:/Program Files/Google/Chrome for Testing/chrome.exe', - 'version': '1.2.3', - 'findAppParams': { - 'appName': 'Google Chrome for Testing.app', - 'executable': 'Contents/MacOS/Google Chrome for Testing', - 'bundleId': 'com.google.chrome.for.testing', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, - { - 'name': 'chromium', - 'family': 'chromium', - 'channel': 'stable', - 'displayName': 'Chromium', - 'versionRegex': {}, - 'binary': [ - 'chromium-browser', - 'chromium', - ], - 'path': 'C:/Program Files (x86)/Google/chrome-win32/chrome.exe', - 'version': '2.3.4', - 'findAppParams': { - 'appName': 'Chromium.app', - 'executable': 'Contents/MacOS/Chromium', - 'bundleId': 'org.chromium.Chromium', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, - { - 'name': 'firefox', - 'family': 'firefox', - 'channel': 'stable', - 'displayName': 'Firefox', - 'versionRegex': {}, - 'binary': 'firefox', - 'path': 'C:/Program Files/Mozilla Firefox/firefox.exe', - 'version': '72', - 'findAppParams': { - 'appName': 'Firefox.app', - 'executable': 'Contents/MacOS/firefox', - 'bundleId': 'org.mozilla.firefox', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, - { - 'name': 'firefox', - 'family': 'firefox', - 'channel': 'dev', - 'displayName': 'Firefox Developer Edition', - 'versionRegex': {}, - 'binary': [ - 'firefox-developer-edition', - 'firefox', - ], - 'path': 'C:/Program Files (x86)/Firefox Developer Edition/firefox.exe', - 'version': '73', - 'findAppParams': { - 'appName': 'Firefox Developer Edition.app', - 'executable': 'Contents/MacOS/firefox', - 'bundleId': 'org.mozilla.firefoxdeveloperedition', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, - { - 'name': 'firefox', - 'family': 'firefox', - 'channel': 'nightly', - 'displayName': 'Firefox Nightly', - 'versionRegex': {}, - 'binary': [ - 'firefox-nightly', - 'firefox-trunk', - ], - 'path': 'C:/Program Files/Firefox Nightly/firefox.exe', - 'version': '74', - 'findAppParams': { - 'appName': 'Firefox Nightly.app', - 'executable': 'Contents/MacOS/firefox', - 'bundleId': 'org.mozilla.nightly', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, - { - 'name': 'edge', - 'family': 'chromium', - 'channel': 'stable', - 'displayName': 'Edge', - 'versionRegex': {}, - 'binary': [ - 'edge', - 'microsoft-edge', - ], - 'path': 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', - 'version': '11', - 'findAppParams': { - 'appName': 'Microsoft Edge.app', - 'executable': 'Contents/MacOS/Microsoft Edge', - 'bundleId': 'com.microsoft.Edge', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, - { - 'name': 'edge', - 'family': 'chromium', - 'channel': 'beta', - 'displayName': 'Edge Beta', - 'versionRegex': {}, - 'binary': [ - 'edge-beta', - 'microsoft-edge-beta', - ], - 'path': 'C:/Program Files (x86)/Microsoft/Edge Beta/Application/msedge.exe', - 'version': '12', - 'findAppParams': { - 'appName': 'Microsoft Edge Beta.app', - 'executable': 'Contents/MacOS/Microsoft Edge Beta', - 'bundleId': 'com.microsoft.Edge.Beta', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, - { - 'name': 'edge', - 'family': 'chromium', - 'channel': 'canary', - 'displayName': 'Edge Canary', - 'versionRegex': {}, - 'binary': [ - 'edge-canary', - 'microsoft-edge-canary', - ], - 'path': 'C:/Users/flotwig/AppData/Local/Microsoft/Edge SxS/Application/msedge.exe', - 'version': '14', - 'findAppParams': { - 'appName': 'Microsoft Edge Canary.app', - 'executable': 'Contents/MacOS/Microsoft Edge Canary', - 'bundleId': 'com.microsoft.Edge.Canary', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, - { - 'name': 'edge', - 'family': 'chromium', - 'channel': 'dev', - 'displayName': 'Edge Dev', - 'versionRegex': {}, - 'binary': [ - 'edge-dev', - 'microsoft-edge-dev', - ], - 'path': 'C:/Program Files (x86)/Microsoft/Edge Dev/Application/msedge.exe', - 'version': '13', - 'findAppParams': { - 'appName': 'Microsoft Edge Dev.app', - 'executable': 'Contents/MacOS/Microsoft Edge Dev', - 'bundleId': 'com.microsoft.Edge.Dev', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, -] - -exports['windows browser detection detects Chrome Beta 64-bit install 1'] = [ - { - 'name': 'chrome', - 'family': 'chromium', - 'channel': 'beta', - 'displayName': 'Chrome Beta', - 'versionRegex': {}, - 'binary': 'google-chrome-beta', - 'path': 'C:/Program Files/Google/Chrome Beta/Application/chrome.exe', - 'version': '9.0.1', - 'findAppParams': { - 'appName': 'Google Chrome Beta.app', - 'executable': 'Contents/MacOS/Google Chrome Beta', - 'bundleId': 'com.google.Chrome.beta', - 'versionProperty': 'KSVersion', - }, - }, -] - -exports['windows browser detection detects Chrome 64-bit install 1'] = [ - { - 'name': 'chrome', - 'family': 'chromium', - 'channel': 'stable', - 'displayName': 'Chrome', - 'versionRegex': {}, - 'binary': [ - 'google-chrome', - 'chrome', - 'google-chrome-stable', - ], - 'path': 'C:/Program Files/Google/Chrome/Application/chrome.exe', - 'version': '4.4.4', - 'findAppParams': { - 'appName': 'Google Chrome.app', - 'executable': 'Contents/MacOS/Google Chrome', - 'bundleId': 'com.google.Chrome', - 'versionProperty': 'KSVersion', - }, - }, -] - -exports['windows browser detection detects Chrome for Testing 32-bit install 1'] = [ - { - 'name': 'chrome-for-testing', - 'family': 'chromium', - 'channel': 'stable', - 'displayName': 'Chrome for Testing', - 'versionRegex': {}, - 'binary': 'chrome', - 'path': 'C:/Program Files (x86)/Google/Chrome for Testing/chrome.exe', - 'version': '5.5.5', - 'findAppParams': { - 'appName': 'Google Chrome for Testing.app', - 'executable': 'Contents/MacOS/Google Chrome for Testing', - 'bundleId': 'com.google.chrome.for.testing', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, -] - -exports['windows browser detection detects Firefox local installs 1'] = [ - { - 'name': 'firefox', - 'family': 'firefox', - 'channel': 'stable', - 'displayName': 'Firefox', - 'versionRegex': {}, - 'binary': 'firefox', - 'path': 'C:/Users/flotwig/AppData/Local/Mozilla Firefox/firefox.exe', - 'version': '100', - 'findAppParams': { - 'appName': 'Firefox.app', - 'executable': 'Contents/MacOS/firefox', - 'bundleId': 'org.mozilla.firefox', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, - { - 'name': 'firefox', - 'family': 'firefox', - 'channel': 'dev', - 'displayName': 'Firefox Developer Edition', - 'versionRegex': {}, - 'binary': [ - 'firefox-developer-edition', - 'firefox', - ], - 'path': 'C:/Users/flotwig/AppData/Local/Firefox Developer Edition/firefox.exe', - 'version': '300', - 'findAppParams': { - 'appName': 'Firefox Developer Edition.app', - 'executable': 'Contents/MacOS/firefox', - 'bundleId': 'org.mozilla.firefoxdeveloperedition', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, - { - 'name': 'firefox', - 'family': 'firefox', - 'channel': 'nightly', - 'displayName': 'Firefox Nightly', - 'versionRegex': {}, - 'binary': [ - 'firefox-nightly', - 'firefox-trunk', - ], - 'path': 'C:/Users/flotwig/AppData/Local/Firefox Nightly/firefox.exe', - 'version': '200', - 'findAppParams': { - 'appName': 'Firefox Nightly.app', - 'executable': 'Contents/MacOS/firefox', - 'bundleId': 'org.mozilla.nightly', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, -] - -exports['windows browser detection detects Chromium 64-bit install 1'] = [ - { - 'name': 'chromium', - 'family': 'chromium', - 'channel': 'stable', - 'displayName': 'Chromium', - 'versionRegex': {}, - 'binary': [ - 'chromium-browser', - 'chromium', - ], - 'path': 'C:/Program Files/Google/chrome-win/chrome.exe', - 'version': '6.6.6', - 'findAppParams': { - 'appName': 'Chromium.app', - 'executable': 'Contents/MacOS/Chromium', - 'bundleId': 'org.chromium.Chromium', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, -] - -exports['windows browser detection detects Chromium 32-bit install in Chromium folder 1'] = [ - { - 'name': 'chromium', - 'family': 'chromium', - 'channel': 'stable', - 'displayName': 'Chromium', - 'versionRegex': {}, - 'binary': [ - 'chromium-browser', - 'chromium', - ], - 'path': 'C:/Program Files (x86)/Google/Chromium/chrome.exe', - 'version': '7.7.7', - 'findAppParams': { - 'appName': 'Chromium.app', - 'executable': 'Contents/MacOS/Chromium', - 'bundleId': 'org.chromium.Chromium', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, -] - -exports['windows browser detection detects Chromium 64-bit install in Chromium folder 1'] = [ - { - 'name': 'chromium', - 'family': 'chromium', - 'channel': 'stable', - 'displayName': 'Chromium', - 'versionRegex': {}, - 'binary': [ - 'chromium-browser', - 'chromium', - ], - 'path': 'C:/Program Files/Google/Chromium/chrome.exe', - 'version': '8.8.8', - 'findAppParams': { - 'appName': 'Chromium.app', - 'executable': 'Contents/MacOS/Chromium', - 'bundleId': 'org.chromium.Chromium', - 'versionProperty': 'CFBundleShortVersionString', - }, - }, -] diff --git a/packages/launcher/lib/windows/index.ts b/packages/launcher/lib/windows/index.ts index c1897b67420..69c1986b503 100644 --- a/packages/launcher/lib/windows/index.ts +++ b/packages/launcher/lib/windows/index.ts @@ -1,4 +1,4 @@ -import * as fse from 'fs-extra' +import fs from 'fs-extra' import winVersionInfo from 'win-version-info' import os from 'os' import { join, normalize, win32 } from 'path' @@ -13,23 +13,23 @@ const debugVerbose = Debug('cypress-verbose:launcher:windows') function formFullAppPath (name: string) { return [ - `C:/Program Files (x86)/Google/Chrome/Application/${name}.exe`, `C:/Program Files/Google/Chrome/Application/${name}.exe`, + `C:/Program Files (x86)/Google/Chrome/Application/${name}.exe`, ].map(normalize) } function formChromeBetaAppPath () { return [ - 'C:/Program Files (x86)/Google/Chrome Beta/Application/chrome.exe', 'C:/Program Files/Google/Chrome Beta/Application/chrome.exe', + 'C:/Program Files (x86)/Google/Chrome Beta/Application/chrome.exe', ].map(normalize) } function formChromiumAppPath () { return [ - 'C:/Program Files (x86)/Google/chrome-win32/chrome.exe', 'C:/Program Files/Google/chrome-win/chrome.exe', 'C:/Program Files/Google/Chromium/chrome.exe', + 'C:/Program Files (x86)/Google/chrome-win32/chrome.exe', 'C:/Program Files (x86)/Google/Chromium/chrome.exe', ].map(normalize) } @@ -144,7 +144,7 @@ function getWindowsBrowser (browser: Browser): Promise { let path = doubleEscape(exePath) - return fse.pathExists(path) + return fs.pathExists(path) .then((exists) => { debugVerbose('found %s ? %o', path, { exists }) @@ -154,7 +154,7 @@ function getWindowsBrowser (browser: Browser): Promise { // Use module.exports.getVersionString here, rather than our local reference // to that variable so that the tests can easily mock it - return module.exports.getVersionString(path).then((version) => { + return getVersionString(path).then((version) => { debug('got version string for %s: %o', browser.name, { exePath, version }) return { diff --git a/packages/launcher/test/unit/__snapshots__/windows.spec.ts.snap b/packages/launcher/test/unit/__snapshots__/windows.spec.ts.snap new file mode 100644 index 00000000000..e5962ff17c8 --- /dev/null +++ b/packages/launcher/test/unit/__snapshots__/windows.spec.ts.snap @@ -0,0 +1,291 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`windows browser detection > detects Chrome 64-bit install 1`] = ` +{ + "binary": [ + "google-chrome", + "chrome", + "google-chrome-stable", + ], + "channel": "stable", + "displayName": "Chrome", + "family": "chromium", + "name": "chrome", + "path": "C:/Program Files/Google/Chrome/Application/chrome.exe", + "validator": [Function], + "version": "4.4.4", + "versionRegex": /Google Chrome\\(\\?! for Testing\\) \\(\\\\S\\+\\)/m, +} +`; + +exports[`windows browser detection > detects Chrome Beta 64-bit install 1`] = ` +{ + "binary": "google-chrome-beta", + "channel": "beta", + "displayName": "Chrome Beta", + "family": "chromium", + "name": "chrome", + "path": "C:/Program Files/Google/Chrome Beta/Application/chrome.exe", + "version": "9.0.1", + "versionRegex": /Google Chrome \\(\\\\S\\+\\) beta/m, +} +`; + +exports[`windows browser detection > detects Chrome for Testing 32-bit install 1`] = ` +{ + "binary": "chrome", + "channel": "stable", + "displayName": "Chrome for Testing", + "family": "chromium", + "name": "chrome-for-testing", + "path": "C:/Program Files (x86)/Google/Chrome for Testing/chrome.exe", + "version": "5.5.5", + "versionRegex": /Google Chrome for Testing \\(\\\\S\\+\\)/m, +} +`; + +exports[`windows browser detection > detects Chromium 32-bit install in Chromium folder 1`] = ` +{ + "binary": [ + "chromium-browser", + "chromium", + ], + "channel": "stable", + "displayName": "Chromium", + "family": "chromium", + "name": "chromium", + "path": "C:/Program Files (x86)/Google/Chromium/chrome.exe", + "version": "7.7.7", + "versionRegex": /Chromium \\(\\\\S\\+\\)/m, +} +`; + +exports[`windows browser detection > detects Chromium 64-bit install 1`] = ` +{ + "binary": [ + "chromium-browser", + "chromium", + ], + "channel": "stable", + "displayName": "Chromium", + "family": "chromium", + "name": "chromium", + "path": "C:/Program Files/Google/chrome-win/chrome.exe", + "version": "6.6.6", + "versionRegex": /Chromium \\(\\\\S\\+\\)/m, +} +`; + +exports[`windows browser detection > detects Chromium 64-bit install in Chromium folder 1`] = ` +{ + "binary": [ + "chromium-browser", + "chromium", + ], + "channel": "stable", + "displayName": "Chromium", + "family": "chromium", + "name": "chromium", + "path": "C:/Program Files/Google/Chromium/chrome.exe", + "version": "8.8.8", + "versionRegex": /Chromium \\(\\\\S\\+\\)/m, +} +`; + +exports[`windows browser detection > detects Firefox local installs 1`] = ` +[ + { + "binary": "firefox", + "channel": "stable", + "displayName": "Firefox", + "family": "firefox", + "name": "firefox", + "path": "C:/Users/flotwig/AppData/Local/Mozilla Firefox/firefox.exe", + "validator": [Function], + "version": "100", + "versionRegex": /\\^Mozilla Firefox \\(\\[\\^\\\\sab\\]\\+\\)\\$/m, + }, + { + "binary": [ + "firefox-developer-edition", + "firefox", + ], + "channel": "dev", + "displayName": "Firefox Developer Edition", + "family": "firefox", + "name": "firefox", + "path": "C:/Users/flotwig/AppData/Local/Firefox Developer Edition/firefox.exe", + "validator": [Function], + "version": "300", + "versionRegex": /\\^Mozilla Firefox \\(\\\\S\\+b\\\\S\\*\\)\\$/m, + }, + { + "binary": [ + "firefox-nightly", + "firefox-trunk", + ], + "channel": "nightly", + "displayName": "Firefox Nightly", + "family": "firefox", + "name": "firefox", + "path": "C:/Users/flotwig/AppData/Local/Firefox Nightly/firefox.exe", + "validator": [Function], + "version": "200", + "versionRegex": /\\^Mozilla Firefox \\(\\\\S\\+a\\\\S\\*\\)\\$/m, + }, +] +`; + +exports[`windows browser detection > detects browsers as expected 1`] = ` +[ + { + "binary": [ + "google-chrome", + "chrome", + "google-chrome-stable", + ], + "channel": "stable", + "displayName": "Chrome", + "family": "chromium", + "name": "chrome", + "path": "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe", + "validator": [Function], + "version": "1.2.3", + "versionRegex": /Google Chrome\\(\\?! for Testing\\) \\(\\\\S\\+\\)/m, + }, + { + "binary": "google-chrome-beta", + "channel": "beta", + "displayName": "Chrome Beta", + "family": "chromium", + "name": "chrome", + "path": "C:/Program Files (x86)/Google/Chrome Beta/Application/chrome.exe", + "version": "6.7.8", + "versionRegex": /Google Chrome \\(\\\\S\\+\\) beta/m, + }, + { + "binary": "google-chrome-canary", + "channel": "canary", + "displayName": "Chrome Canary", + "family": "chromium", + "name": "chrome", + "path": "C:/Users/flotwig/AppData/Local/Google/Chrome SxS/Application/chrome.exe", + "version": "3.4.5", + "versionRegex": /Google Chrome Canary \\(\\\\S\\+\\)/m, + }, + { + "binary": "chrome", + "channel": "stable", + "displayName": "Chrome for Testing", + "family": "chromium", + "name": "chrome-for-testing", + "path": "C:/Program Files/Google/Chrome for Testing/chrome.exe", + "version": "1.2.3", + "versionRegex": /Google Chrome for Testing \\(\\\\S\\+\\)/m, + }, + { + "binary": [ + "chromium-browser", + "chromium", + ], + "channel": "stable", + "displayName": "Chromium", + "family": "chromium", + "name": "chromium", + "path": "C:/Program Files/Google/chrome-win/chrome.exe", + "version": "2.3.4", + "versionRegex": /Chromium \\(\\\\S\\+\\)/m, + }, + { + "binary": "firefox", + "channel": "stable", + "displayName": "Firefox", + "family": "firefox", + "name": "firefox", + "path": "C:/Program Files/Mozilla Firefox/firefox.exe", + "validator": [Function], + "version": "72", + "versionRegex": /\\^Mozilla Firefox \\(\\[\\^\\\\sab\\]\\+\\)\\$/m, + }, + { + "binary": [ + "firefox-developer-edition", + "firefox", + ], + "channel": "dev", + "displayName": "Firefox Developer Edition", + "family": "firefox", + "name": "firefox", + "path": "C:/Program Files (x86)/Firefox Developer Edition/firefox.exe", + "validator": [Function], + "version": "73", + "versionRegex": /\\^Mozilla Firefox \\(\\\\S\\+b\\\\S\\*\\)\\$/m, + }, + { + "binary": [ + "firefox-nightly", + "firefox-trunk", + ], + "channel": "nightly", + "displayName": "Firefox Nightly", + "family": "firefox", + "name": "firefox", + "path": "C:/Program Files/Firefox Nightly/firefox.exe", + "validator": [Function], + "version": "74", + "versionRegex": /\\^Mozilla Firefox \\(\\\\S\\+a\\\\S\\*\\)\\$/m, + }, + { + "binary": [ + "edge", + "microsoft-edge", + ], + "channel": "stable", + "displayName": "Edge", + "family": "chromium", + "name": "edge", + "path": "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe", + "version": "11", + "versionRegex": /Microsoft Edge \\(\\\\S\\+\\)/im, + }, + { + "binary": [ + "edge-beta", + "microsoft-edge-beta", + ], + "channel": "beta", + "displayName": "Edge Beta", + "family": "chromium", + "name": "edge", + "path": "C:/Program Files (x86)/Microsoft/Edge Beta/Application/msedge.exe", + "version": "12", + "versionRegex": /Microsoft Edge\\.\\+\\?\\(\\\\S\\*\\(\\?= beta\\)\\|\\(\\?<=beta \\)\\\\S\\*\\)/im, + }, + { + "binary": [ + "edge-canary", + "microsoft-edge-canary", + ], + "channel": "canary", + "displayName": "Edge Canary", + "family": "chromium", + "name": "edge", + "path": "C:/Users/flotwig/AppData/Local/Microsoft/Edge SxS/Application/msedge.exe", + "version": "14", + "versionRegex": /Microsoft Edge\\.\\+\\?\\(\\\\S\\*\\(\\?= canary\\)\\|\\(\\?<=canary \\)\\\\S\\*\\)/im, + }, + { + "binary": [ + "edge-dev", + "microsoft-edge-dev", + ], + "channel": "dev", + "displayName": "Edge Dev", + "family": "chromium", + "name": "edge", + "path": "C:/Program Files (x86)/Microsoft/Edge Dev/Application/msedge.exe", + "version": "13", + "versionRegex": /Microsoft Edge\\.\\+\\?\\(\\\\S\\*\\(\\?= dev\\)\\|\\(\\?<=dev \\)\\\\S\\*\\)/im, + }, +] +`; diff --git a/packages/launcher/test/unit/windows.spec.ts b/packages/launcher/test/unit/windows.spec.ts new file mode 100644 index 00000000000..b7eb3fd2e07 --- /dev/null +++ b/packages/launcher/test/unit/windows.spec.ts @@ -0,0 +1,525 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import winVersionInfo from 'win-version-info' +import _ from 'lodash' +import * as windowsHelper from '../../lib/windows' +import { knownBrowsers } from '../../lib/known-browsers' +import fs from 'fs-extra' +import os from 'os' +import type { Browser } from '@packages/types' +import { detectByPath } from '../../lib/detect' +import { goalBrowsers } from '../fixtures' + +vi.mock('os', async (importActual) => { + const actual = await importActual() + + return { + default: { + // @ts-expect-error + ...actual.default, + homedir: vi.fn(), + platform: vi.fn(), + }, + } +}) + +vi.mock('fs-extra', async (importActual) => { + const actual = await importActual() + + return { + default: { + // @ts-expect-error + ...actual.default, + pathExists: vi.fn(), + }, + } +}) + +vi.mock('win-version-info', () => { + return { + default: vi.fn(), + } +}) + +describe('windows browser detection', () => { + const HOMEDIR = 'C:/Users/flotwig' + + let mockBrowsers: { path: string, version: string }[] = [] + + beforeEach(() => { + vi.resetAllMocks() + mockBrowsers = [ + // chrome + { + path: 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe', + version: '1.2.3', + }, + // chromium - 32-bit will be preferred for passivity + { + path: 'C:/Program Files (x86)/Google/chrome-win32/chrome.exe', + version: '2.3.4', + }, + { + path: 'C:/Program Files/Google/chrome-win/chrome.exe', + version: '2.3.4', + }, + // chrome-for-testing - 64-bit will be preferred + { + path: 'C:/Program Files (x86)/Google/Chrome for Testing/chrome.exe', + version: '1.2.3', + }, + { + path: 'C:/Program Files/Google/Chrome for Testing/chrome.exe', + version: '1.2.3', + }, + // chrome beta + { + path: 'C:/Program Files (x86)/Google/Chrome Beta/Application/chrome.exe', + version: '6.7.8', + }, + // chrome canary is installed in homedir + { + path: `${HOMEDIR}/AppData/Local/Google/Chrome SxS/Application/chrome.exe`, + version: '3.4.5', + }, + // have 32-bit and 64-bit ff - 64-bit will be preferred + { + path: 'C:/Program Files (x86)/Mozilla Firefox/firefox.exe', + version: '72', + }, + { + path: 'C:/Program Files/Mozilla Firefox/firefox.exe', + version: '72', + }, + // 32-bit dev edition + { + path: 'C:/Program Files (x86)/Firefox Developer Edition/firefox.exe', + version: '73', + }, + // 64-bit nightly edition + { + path: 'C:/Program Files/Firefox Nightly/firefox.exe', + version: '74', + }, + { + path: 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', + version: '11', + }, + { + path: 'C:/Program Files (x86)/Microsoft/Edge Beta/Application/msedge.exe', + version: '12', + }, + { + path: 'C:/Program Files (x86)/Microsoft/Edge Dev/Application/msedge.exe', + version: '13', + }, + { + // edge canary is installed in homedir + path: `${HOMEDIR}/AppData/Local/Microsoft/Edge SxS/Application/msedge.exe`, + version: '14', + }, + ] + + vi.mocked(os.homedir).mockReturnValue(HOMEDIR) + vi.mocked(os.platform).mockReturnValue('win32') + + vi.mocked(fs.pathExists).mockImplementation((path) => { + const browser = mockBrowsers.find((browser) => windowsHelper.doubleEscape(browser.path) === path) + + if (!browser) { + return Promise.resolve(false) + } + + return Promise.resolve(true) + }) + + vi.mocked(winVersionInfo).mockImplementation((path) => { + const browser = mockBrowsers.find((browser) => windowsHelper.doubleEscape(browser.path) === path) + + if (!browser) { + throw new Error('Browser not found') + } + + return { FileVersion: browser?.version } + }) + }) + + it('detects browsers as expected', async () => { + const mappedBrowsers = [] + + for (const browser of knownBrowsers) { + const foundBrowser = await windowsHelper.detect(browser) + + mappedBrowsers.push({ + ...browser, + ...foundBrowser, + }) + } + + expect(mappedBrowsers).toMatchSnapshot() + }) + + it('detects Chrome Beta 64-bit install', async () => { + // mock installing the 64-bit (32-bit installed already in mockBrowsers) + // should prefer the 64-bit install over the 32-bit install + mockBrowsers.push({ + path: 'C:/Program Files/Google/Chrome Beta/Application/chrome.exe', + version: '9.0.1', + }) + + const chrome = _.find(knownBrowsers, { name: 'chrome', channel: 'beta' })! as Browser + + const foundBrowser = await windowsHelper.detect(chrome) + + const snapshotBrowser = { + ...chrome, + ...foundBrowser, + } + + expect(snapshotBrowser.version).toEqual('9.0.1') + expect(snapshotBrowser).toMatchSnapshot() + }) + + // @see https://github.com/cypress-io/cypress/issues/8425 + it('detects Chrome 64-bit install', async () => { + // mock installing the 64-bit (32-bit installed already in mockBrowsers) + // should prefer the 64-bit install over the 32-bit install + mockBrowsers.push({ + path: 'C:/Program Files/Google/Chrome/Application/chrome.exe', + version: '4.4.4', + }) + + const chrome = _.find(knownBrowsers, { name: 'chrome', channel: 'stable' })! as Browser + + const foundBrowser = await windowsHelper.detect(chrome) + + const snapshotBrowser = { + ...chrome, + ...foundBrowser, + } + + expect(snapshotBrowser.version).toEqual('4.4.4') + expect(snapshotBrowser).toMatchSnapshot() + }) + + it('detects Chrome for Testing 32-bit install', async () => { + // mock uninstalling the 32-bit and 64-bit + const foundCFTInstalls = _.remove(mockBrowsers, (browser) => browser.path.includes('Chrome for Testing')) + + expect(foundCFTInstalls).toHaveLength(2) + + // mock installing the 32-bit + mockBrowsers.push({ + path: 'C:/Program Files (x86)/Google/Chrome for Testing/chrome.exe', + version: '5.5.5', + }) + + const chromeForTesting = _.find(knownBrowsers, { name: 'chrome-for-testing' })! + + const foundBrowser = await windowsHelper.detect(chromeForTesting) + + const snapshotBrowser = { + ...chromeForTesting, + ...foundBrowser, + } + + expect(snapshotBrowser.version).toEqual('5.5.5') + expect(snapshotBrowser).toMatchSnapshot() + }) + + // @see https://github.com/cypress-io/cypress/issues/8432 + it('detects Firefox local installs', async () => { + // mock uninstalling Firefox in the Program Files directory + const foundFirefoxInstalls = _.remove(mockBrowsers, (browser) => browser.path.includes('Firefox')) + + expect(foundFirefoxInstalls).toHaveLength(4) + + // mock installing Firefox in the local app data directory + mockBrowsers.push({ + path: `${HOMEDIR}/AppData/Local/Mozilla Firefox/firefox.exe`, + version: '100', + }) + + mockBrowsers.push({ + path: `${HOMEDIR}/AppData/Local/Firefox Nightly/firefox.exe`, + version: '200', + }) + + mockBrowsers.push({ + path: `${HOMEDIR}/AppData/Local/Firefox Developer Edition/firefox.exe`, + version: '300', + }) + + const firefoxBrowsers = _.filter(knownBrowsers, { family: 'firefox' }) + + const mappedBrowsers = [] + + for (const browser of firefoxBrowsers) { + const foundBrowser = await windowsHelper.detect(browser) + + mappedBrowsers.push({ + ...browser, + ...foundBrowser, + }) + } + + expect(mappedBrowsers.map((browser) => browser.version).sort()).toEqual(['100', '200', '300']) + expect(mappedBrowsers).toMatchSnapshot() + }) + + it('detects Chromium 64-bit install', async () => { + // mock updating the 64-bit install of chrome + const foundChromiumInstalls = _.remove(mockBrowsers, (browser) => browser.path === 'C:/Program Files/Google/chrome-win/chrome.exe') + + expect(foundChromiumInstalls).toHaveLength(1) + + mockBrowsers.push({ + path: 'C:/Program Files/Google/chrome-win/chrome.exe', + version: '6.6.6', + }) + + const chromium = _.find(knownBrowsers, { name: 'chromium' })! + + const foundBrowser = await windowsHelper.detect(chromium) + + const snapshotBrowser = { + ...chromium, + ...foundBrowser, + } + + expect(snapshotBrowser.version).toEqual('6.6.6') + expect(snapshotBrowser).toMatchSnapshot() + }) + + it('detects Chromium 32-bit install in Chromium folder', async () => { + // mock uninstalling the 64-bit and 32-bit in the Google path + const foundChromiumInstalls = _.remove(mockBrowsers, (browser) => browser.path.includes('chrome-win')) + + expect(foundChromiumInstalls).toHaveLength(2) + + // mock installing the 32-bit + mockBrowsers.push({ + path: 'C:/Program Files (x86)/Google/Chromium/chrome.exe', + version: '7.7.7', + }) + + const chromium = _.find(knownBrowsers, { name: 'chromium' })! + + const foundBrowser = await windowsHelper.detect(chromium) + + const snapshotBrowser = { + ...chromium, + ...foundBrowser, + } + + expect(snapshotBrowser.version).toEqual('7.7.7') + expect(snapshotBrowser).toMatchSnapshot() + }) + + it('detects Chromium 64-bit install in Chromium folder', async () => { + // mock uninstalling the 64-bit and 32-bit in the Google path + const foundChromiumInstalls = _.remove(mockBrowsers, (browser) => browser.path.includes('chrome-win')) + + expect(foundChromiumInstalls).toHaveLength(2) + + // mock installing the 32-bit + mockBrowsers.push({ + path: 'C:/Program Files/Google/Chromium/chrome.exe', + version: '8.8.8', + }) + + const chromium = _.find(knownBrowsers, { name: 'chromium' })! + + const foundBrowser = await windowsHelper.detect(chromium) + + const snapshotBrowser = { + ...chromium, + ...foundBrowser, + } + + expect(snapshotBrowser.version).toEqual('8.8.8') + expect(snapshotBrowser).toMatchSnapshot() + }) + + it('works with :browserName format in Windows', async () => { + let path = `${HOMEDIR}/foo/bar/browser.exe` + let win10Path = windowsHelper.doubleEscape(path) + + mockBrowsers.push({ + path, + version: '100', + }) + + const foundBrowser = await detectByPath(`${path}:foo-browser`, goalBrowsers as Browser[]) + + const fooBrowser = goalBrowsers.find(({ name }) => name === 'foo-browser')! + + expect(foundBrowser).toEqual( + { + ...fooBrowser, + displayName: 'Custom Foo Browser', + info: `Loaded from ${win10Path}`, + custom: true, + version: '100', + majorVersion: '100', + path: win10Path, + }, + ) + }) + + it('identifies browser if name in path', async () => { + let path = `${HOMEDIR}/foo/bar/chrome.exe` + let win10Path = windowsHelper.doubleEscape(path) + + mockBrowsers.push({ + path, + version: '100', + }) + + const foundBrowser = await detectByPath(path) + + const chromeBrowser = knownBrowsers.find(({ name }) => name === 'chrome')! + + expect(foundBrowser).toEqual( + { + ...chromeBrowser, + displayName: 'Custom Chrome', + info: `Loaded from ${win10Path}`, + custom: true, + version: '100', + majorVersion: '100', + path: win10Path, + }, + ) + }) + + describe('#getVersionString', () => { + it('returns the FileVersion from win-version-info', async () => { + mockBrowsers.push({ + path: 'foo', + version: 'bar', + }) + + const versionString = await windowsHelper.getVersionString('foo') + + expect(versionString).toEqual('bar') + }) + }) + + describe('#getPathData', () => { + it('returns path and browserKey given path with browser key', () => { + const browserPath = 'C:\\foo\\bar.exe' + const res = windowsHelper.getPathData(`${browserPath}:firefox`) + + expect(res.path).toEqual(windowsHelper.doubleEscape(browserPath)) + expect(res.browserKey).toEqual('firefox') + }) + + it('returns path and browserKey given path with a lot of slashes plus browser key', () => { + const browserPath = 'C:\\\\\\\\foo\\\\\\bar.exe' + const res = windowsHelper.getPathData(`${browserPath}:firefox`) + + expect(res.path).toEqual(windowsHelper.doubleEscape(browserPath)) + expect(res.browserKey).toEqual('firefox') + }) + + it('returns path and browserKey given nix path with browser key', () => { + const browserPath = 'C:/foo/bar.exe' + const res = windowsHelper.getPathData(`${browserPath}:firefox`) + + expect(res.path).toEqual(windowsHelper.doubleEscape(browserPath)) + expect(res.browserKey).toEqual('firefox') + }) + + it('returns path and chrome given just path', () => { + const browserPath = 'C:\\foo\\bar\\chrome.exe' + const res = windowsHelper.getPathData(browserPath) + + expect(res.path).toEqual(windowsHelper.doubleEscape(browserPath)) + expect(res.browserKey).toEqual('chrome') + }) + + it('returns path and chrome given just nix path', () => { + const browserPath = 'C:/foo/bar/chrome.exe' + const res = windowsHelper.getPathData(browserPath) + + expect(res.path).toEqual(windowsHelper.doubleEscape(browserPath)) + expect(res.browserKey).toEqual('chrome') + }) + + it('returns path and edge given just path for edge', () => { + const browserPath = 'C:\\foo\\bar\\edge.exe' + const res = windowsHelper.getPathData(browserPath) + + expect(res.path).toEqual(windowsHelper.doubleEscape(browserPath)) + expect(res.browserKey).toEqual('edge') + }) + + it('returns path and edge given just path for msedge', () => { + const browserPath = 'C:\\foo\\bar\\msedge.exe' + const res = windowsHelper.getPathData(browserPath) + + expect(res.path).toEqual(windowsHelper.doubleEscape(browserPath)) + expect(res.browserKey).toEqual('edge') + }) + + it('returns path and edge given just nix path', () => { + const browserPath = 'C:/foo/bar/edge.exe' + const res = windowsHelper.getPathData(browserPath) + + expect(res.path).toEqual(windowsHelper.doubleEscape(browserPath)) + expect(res.browserKey).toEqual('edge') + }) + + it('returns path and edge given just nix path for msedge', () => { + const browserPath = 'C:/foo/bar/msedge.exe' + const res = windowsHelper.getPathData(browserPath) + + expect(res.path).toEqual(windowsHelper.doubleEscape(browserPath)) + expect(res.browserKey).toEqual('edge') + }) + + it('returns path and firefox given just path', () => { + const browserPath = 'C:\\foo\\bar\\firefox.exe' + const res = windowsHelper.getPathData(browserPath) + + expect(res.path).toEqual(windowsHelper.doubleEscape(browserPath)) + expect(res.browserKey).toEqual('firefox') + }) + + it('returns path and firefox given just nix path', () => { + const browserPath = 'C:/foo/bar/firefox.exe' + const res = windowsHelper.getPathData(browserPath) + + expect(res.path).toEqual(windowsHelper.doubleEscape(browserPath)) + expect(res.browserKey).toEqual('firefox') + }) + }) + + describe('#doubleEscape', () => { + let winPath = 'C:\\\\foo\\\\bar.exe' + + it('converts nix path into double escaped win path', async () => { + let nixPath = 'C:/foo/bar.exe' + + expect(windowsHelper.doubleEscape(nixPath)).toEqual(winPath) + }) + + it('converts win path with different backslash combination into double escaped win path', async () => { + let badWinPath = 'C:\\\\\\\\\\foo\\bar.exe' + + expect(windowsHelper.doubleEscape(badWinPath)).toEqual(winPath) + }) + + it('converts single escaped win path into double escaped win path', async () => { + let badWinPath = 'C:\\foo\\bar.exe' + + expect(windowsHelper.doubleEscape(badWinPath)).toEqual(winPath) + }) + + it('does not affect an already double escaped win path', async () => { + let badWinPath = 'C:\\\\foo\\\\bar.exe' + + expect(windowsHelper.doubleEscape(badWinPath)).toEqual(badWinPath) + }) + }) +}) diff --git a/packages/launcher/test/unit/windows_spec.ts b/packages/launcher/test/unit/windows_spec.ts deleted file mode 100644 index af321beca64..00000000000 --- a/packages/launcher/test/unit/windows_spec.ts +++ /dev/null @@ -1,307 +0,0 @@ -import _ from 'lodash' -import { expect } from 'chai' -import * as windowsHelper from '../../lib/windows' -import { normalize } from 'path' -import sinon, { SinonStub } from 'sinon' -import { knownBrowsers } from '../../lib/known-browsers' -import Bluebird from 'bluebird' -import fse from 'fs-extra' -import os from 'os' -import snapshot from 'snap-shot-it' -import type { Browser } from '@packages/types' -import { detectByPath } from '../../lib/detect' -import { goalBrowsers } from '../fixtures' - -function stubBrowser (path: string, version: string) { - path = windowsHelper.doubleEscape(normalize(path)) - - ;(windowsHelper.getVersionString as unknown as SinonStub) - .withArgs(path) - .resolves(version) - - ;(fse.pathExists as SinonStub) - .withArgs(path) - .resolves(true) -} - -function detect (goalBrowsers: Browser[]) { - return Bluebird.mapSeries(goalBrowsers, (browser) => { - return windowsHelper.detect(browser) - .then((foundBrowser) => { - return _.merge(browser, foundBrowser) - }) - }) -} - -const HOMEDIR = 'C:/Users/flotwig' - -describe('windows browser detection', () => { - beforeEach(() => { - sinon.stub(fse, 'pathExists').resolves(false) - sinon.stub(os, 'homedir').returns(HOMEDIR) - sinon.stub(windowsHelper, 'getVersionString').rejects() - }) - - it('detects browsers as expected', async () => { - // chrome - stubBrowser('C:/Program Files (x86)/Google/Chrome/Application/chrome.exe', '1.2.3') - // chromium - 32-bit will be preferred for passivity - stubBrowser('C:/Program Files (x86)/Google/chrome-win32/chrome.exe', '2.3.4') - stubBrowser('C:/Program Files/Google/chrome-win/chrome.exe', '2.3.4') - - // chrome-for-testing - 64-bit will be preferred - stubBrowser('C:/Program Files (x86)/Google/Chrome for Testing/chrome.exe', '1.2.3') - stubBrowser('C:/Program Files/Google/Chrome for Testing/chrome.exe', '1.2.3') - - // chrome beta - stubBrowser('C:/Program Files (x86)/Google/Chrome Beta/Application/chrome.exe', '6.7.8') - - // chrome canary is installed in homedir - stubBrowser(`${HOMEDIR}/AppData/Local/Google/Chrome SxS/Application/chrome.exe`, '3.4.5') - - // have 32-bit and 64-bit ff - 64-bit will be preferred - stubBrowser('C:/Program Files (x86)/Mozilla Firefox/firefox.exe', '72') - stubBrowser('C:/Program Files/Mozilla Firefox/firefox.exe', '72') - - // 32-bit dev edition - stubBrowser('C:/Program Files (x86)/Firefox Developer Edition/firefox.exe', '73') - - // 64-bit nightly edition - stubBrowser('C:/Program Files/Firefox Nightly/firefox.exe', '74') - - stubBrowser('C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', '11') - stubBrowser('C:/Program Files (x86)/Microsoft/Edge Beta/Application/msedge.exe', '12') - stubBrowser('C:/Program Files (x86)/Microsoft/Edge Dev/Application/msedge.exe', '13') - - // edge canary is installed in homedir - stubBrowser(`${HOMEDIR}/AppData/Local/Microsoft/Edge SxS/Application/msedge.exe`, '14') - - snapshot(await detect(knownBrowsers)) - }) - - it('detects Chrome Beta 64-bit install', async () => { - stubBrowser('C:/Program Files/Google/Chrome Beta/Application/chrome.exe', '9.0.1') - const chrome = _.find(knownBrowsers, { name: 'chrome', channel: 'beta' })! - - snapshot(await detect([chrome])) - }) - - // @see https://github.com/cypress-io/cypress/issues/8425 - it('detects Chrome 64-bit install', async () => { - stubBrowser('C:/Program Files/Google/Chrome/Application/chrome.exe', '4.4.4') - const chrome = _.find(knownBrowsers, { name: 'chrome', channel: 'stable' })! - - snapshot(await detect([chrome])) - }) - - it('detects Chrome for Testing 32-bit install', async () => { - stubBrowser('C:/Program Files (x86)/Google/Chrome for Testing/chrome.exe', '5.5.5') - const chromeForTesting = _.find(knownBrowsers, { name: 'chrome-for-testing' })! - - snapshot(await detect([chromeForTesting])) - }) - - // @see https://github.com/cypress-io/cypress/issues/8432 - it('detects Firefox local installs', async () => { - stubBrowser(`${HOMEDIR}/AppData/Local/Mozilla Firefox/firefox.exe`, '100') - stubBrowser(`${HOMEDIR}/AppData/Local/Firefox Nightly/firefox.exe`, '200') - stubBrowser(`${HOMEDIR}/AppData/Local/Firefox Developer Edition/firefox.exe`, '300') - - const firefoxes = _.filter(knownBrowsers, { family: 'firefox' }) - - snapshot(await detect(firefoxes)) - }) - - it('detects Chromium 64-bit install', async () => { - stubBrowser('C:/Program Files/Google/chrome-win/chrome.exe', '6.6.6') - const chromium = _.find(knownBrowsers, { name: 'chromium' })! - - snapshot(await detect([chromium])) - }) - - it('detects Chromium 32-bit install in Chromium folder', async () => { - stubBrowser('C:/Program Files (x86)/Google/Chromium/chrome.exe', '7.7.7') - const chromium = _.find(knownBrowsers, { name: 'chromium' })! - - snapshot(await detect([chromium])) - }) - - it('detects Chromium 64-bit install in Chromium folder', async () => { - stubBrowser('C:/Program Files/Google/Chromium/chrome.exe', '8.8.8') - const chromium = _.find(knownBrowsers, { name: 'chromium' })! - - snapshot(await detect([chromium])) - }) - - it('works with :browserName format in Windows', () => { - sinon.stub(os, 'platform').returns('win32') - let path = `${HOMEDIR}/foo/bar/browser.exe` - let win10Path = windowsHelper.doubleEscape(path) - - stubBrowser(path, '100') - - return detectByPath(`${path}:foo-browser`, goalBrowsers as Browser[]).then((browser) => { - expect(browser).to.deep.equal( - Object.assign({}, goalBrowsers.find((gb) => { - return gb.name === 'foo-browser' - }), { - displayName: 'Custom Foo Browser', - info: `Loaded from ${win10Path}`, - custom: true, - version: '100', - majorVersion: '100', - path: win10Path, - }), - ) - }) - }) - - it('identifies browser if name in path', async () => { - sinon.stub(os, 'platform').returns('win32') - let path = `${HOMEDIR}/foo/bar/chrome.exe` - let win10Path = windowsHelper.doubleEscape(path) - - stubBrowser(path, '100') - - return detectByPath(path).then((browser) => { - expect(browser).to.deep.equal( - Object.assign({}, knownBrowsers.find((gb) => { - return gb.name === 'chrome' - }), { - displayName: 'Custom Chrome', - info: `Loaded from ${win10Path}`, - custom: true, - version: '100', - majorVersion: '100', - path: win10Path, - }), - ) - }) - }) - - context('#getVersionString', () => { - it('returns the FileVersion from win-version-info', async () => { - stubBrowser('foo', 'bar') - - expect(await windowsHelper.getVersionString('foo')).to.eq('bar') - }) - }) - - context('#getPathData', () => { - it('returns path and browserKey given path with browser key', () => { - const browserPath = 'C:\\foo\\bar.exe' - const res = windowsHelper.getPathData(`${browserPath}:firefox`) - - expect(res.path).to.eq(windowsHelper.doubleEscape(browserPath)) - expect(res.browserKey).to.eq('firefox') - }) - - it('returns path and browserKey given path with a lot of slashes plus browser key', () => { - const browserPath = 'C:\\\\\\\\foo\\\\\\bar.exe' - const res = windowsHelper.getPathData(`${browserPath}:firefox`) - - expect(res.path).to.eq(windowsHelper.doubleEscape(browserPath)) - expect(res.browserKey).to.eq('firefox') - }) - - it('returns path and browserKey given nix path with browser key', () => { - const browserPath = 'C:/foo/bar.exe' - const res = windowsHelper.getPathData(`${browserPath}:firefox`) - - expect(res.path).to.eq(windowsHelper.doubleEscape(browserPath)) - expect(res.browserKey).to.eq('firefox') - }) - - it('returns path and chrome given just path', () => { - const browserPath = 'C:\\foo\\bar\\chrome.exe' - const res = windowsHelper.getPathData(browserPath) - - expect(res.path).to.eq(windowsHelper.doubleEscape(browserPath)) - expect(res.browserKey).to.eq('chrome') - }) - - it('returns path and chrome given just nix path', () => { - const browserPath = 'C:/foo/bar/chrome.exe' - const res = windowsHelper.getPathData(browserPath) - - expect(res.path).to.eq(windowsHelper.doubleEscape(browserPath)) - expect(res.browserKey).to.eq('chrome') - }) - - it('returns path and edge given just path for edge', () => { - const browserPath = 'C:\\foo\\bar\\edge.exe' - const res = windowsHelper.getPathData(browserPath) - - expect(res.path).to.eq(windowsHelper.doubleEscape(browserPath)) - expect(res.browserKey).to.eq('edge') - }) - - it('returns path and edge given just path for msedge', () => { - const browserPath = 'C:\\foo\\bar\\msedge.exe' - const res = windowsHelper.getPathData(browserPath) - - expect(res.path).to.eq(windowsHelper.doubleEscape(browserPath)) - expect(res.browserKey).to.eq('edge') - }) - - it('returns path and edge given just nix path', () => { - const browserPath = 'C:/foo/bar/edge.exe' - const res = windowsHelper.getPathData(browserPath) - - expect(res.path).to.eq(windowsHelper.doubleEscape(browserPath)) - expect(res.browserKey).to.eq('edge') - }) - - it('returns path and edge given just nix path for msedge', () => { - const browserPath = 'C:/foo/bar/msedge.exe' - const res = windowsHelper.getPathData(browserPath) - - expect(res.path).to.eq(windowsHelper.doubleEscape(browserPath)) - expect(res.browserKey).to.eq('edge') - }) - - it('returns path and firefox given just path', () => { - const browserPath = 'C:\\foo\\bar\\firefox.exe' - const res = windowsHelper.getPathData(browserPath) - - expect(res.path).to.eq(windowsHelper.doubleEscape(browserPath)) - expect(res.browserKey).to.eq('firefox') - }) - - it('returns path and firefox given just nix path', () => { - const browserPath = 'C:/foo/bar/firefox.exe' - const res = windowsHelper.getPathData(browserPath) - - expect(res.path).to.eq(windowsHelper.doubleEscape(browserPath)) - expect(res.browserKey).to.eq('firefox') - }) - }) - - context('#doubleEscape', () => { - let winPath = 'C:\\\\foo\\\\bar.exe' - - it('converts nix path into double escaped win path', async () => { - let nixPath = 'C:/foo/bar.exe' - - expect(windowsHelper.doubleEscape(nixPath)).to.eq(winPath) - }) - - it('converts win path with different backslash combination into double escaped win path', async () => { - let badWinPath = 'C:\\\\\\\\\\foo\\bar.exe' - - expect(windowsHelper.doubleEscape(badWinPath)).to.eq(winPath) - }) - - it('converts single escaped win path into double escaped win path', async () => { - let badWinPath = 'C:\\foo\\bar.exe' - - expect(windowsHelper.doubleEscape(badWinPath)).to.eq(winPath) - }) - - it('does not affect an already double escaped win path', async () => { - let badWinPath = 'C:\\\\foo\\\\bar.exe' - - expect(windowsHelper.doubleEscape(badWinPath)).to.eq(badWinPath) - }) - }) -}) From 41aca91d17d4e6739dd1d97500d745fdfee573a6 Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Sun, 5 Oct 2025 23:13:41 -0400 Subject: [PATCH 05/16] chore: convert detect spect to vitest --- packages/launcher/test/unit/detect.spec.ts | 283 +++++++++++++++++++++ packages/launcher/test/unit/detect_spec.ts | 174 ------------- 2 files changed, 283 insertions(+), 174 deletions(-) create mode 100644 packages/launcher/test/unit/detect.spec.ts delete mode 100644 packages/launcher/test/unit/detect_spec.ts diff --git a/packages/launcher/test/unit/detect.spec.ts b/packages/launcher/test/unit/detect.spec.ts new file mode 100644 index 00000000000..2cf13854748 --- /dev/null +++ b/packages/launcher/test/unit/detect.spec.ts @@ -0,0 +1,283 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import _ from 'lodash' +import cp from 'child_process' +import { EventEmitter } from 'stream' +import { detect, detectByPath, getMajorVersion } from '../../lib/detect' +import { goalBrowsers } from '../fixtures' +import os from 'os' +import { log } from '../log' +import { detect as linuxDetect } from '../../lib/linux' +import { detect as darwinDetect } from '../../lib/darwin' +import { detect as windowsDetect } from '../../lib/windows' +import type { Browser } from '@packages/types' + +vi.mock('child_process', async (importActual) => { + const actual = await importActual() + + return { + default: { + // @ts-expect-error + ...actual.default, + spawn: vi.fn(), + }, + } +}) + +vi.mock('../../lib/linux', async (importActual) => { + const actual = await importActual() + + return { + // @ts-expect-error + ...actual, + detect: vi.fn(), + } +}) + +vi.mock('../../lib/darwin', async (importActual) => { + const actual = await importActual() + + return { + // @ts-expect-error + ...actual, + detect: vi.fn(), + } +}) + +vi.mock('../../lib/windows', async (importActual) => { + const actual = await importActual() + + return { + // @ts-expect-error + ...actual, + detect: vi.fn(), + } +}) + +const isWindows = () => { + return os.platform() === 'win32' +} + +describe('detect', () => { + beforeEach(async () => { + vi.unstubAllEnvs() + vi.resetAllMocks() + + const { detect: linuxDetectActual } = await vi.importActual('../../lib/linux') + const { detect: darwinDetectActual } = await vi.importActual('../../lib/darwin') + const { detect: windowsDetectActual } = await vi.importActual('../../lib/windows') + + vi.mocked(linuxDetect).mockImplementation(linuxDetectActual) + vi.mocked(darwinDetect).mockImplementation(darwinDetectActual) + vi.mocked(windowsDetect).mockImplementation(windowsDetectActual) + }) + + // making simple to debug tests + // using DEBUG=... flag + + // we are only going to run tests on platforms with at least + // one browser. This test, is really E2E because it finds + // real browsers + it('detects available browsers', async () => { + const browsers = await detect() + + log('detected browsers %j', browsers) + expect(browsers).toBeInstanceOf(Array) + + const mainProps = browsers.map((val) => _.pick(val, ['name', 'version'])) + + log('%d browsers\n%j', browsers.length, mainProps) + + if (isWindows()) { + // we might not find any browsers on Windows CI + expect(browsers.length).toBeGreaterThanOrEqual(0) + } else { + expect(browsers.length).toBeGreaterThan(0) + } + }) + + describe('#getMajorVersion', () => { + it('parses major version from provided string', () => { + expect(getMajorVersion('123.45.67')).toEqual('123') + expect(getMajorVersion('Browser 77.1.0')).to.eq('Browser 77') + expect(getMajorVersion('999')).toEqual('999') + }) + }) + + describe('#detect', () => { + const testBrowser = { + name: 'test-browser', + family: 'chromium', + channel: 'test-channel', + displayName: 'Test Browser', + versionRegex: /Test Browser (\S+)/m, + binary: 'test-browser-beta', + } + + it('validates browser with own validator property', async () => { + // @ts-expect-error + vi.mocked(linuxDetect).mockImplementation((browser) => { + return Promise.resolve({ + name: browser.name, + path: '/path/to/test-browser', + version: '130', + }) + }) + + vi.mocked(darwinDetect).mockImplementation((browser) => { + return Promise.resolve({ + name: browser.name, + path: '/path/to/test-browser', + version: '130', + }) + }) + + // @ts-expect-error + vi.mocked(windowsDetect).mockImplementation((browser) => { + return Promise.resolve({ + name: browser.name, + path: '/path/to/test-browser', + version: '130', + }) + }) + + const mockValidator = vi.fn().mockReturnValue({ isSupported: true }) + + const foundBrowsers = await detect([{ ...testBrowser as Browser, validator: mockValidator }]) + + expect(foundBrowsers).toHaveLength(1) + + const foundTestBrowser = foundBrowsers[0] + + expect(foundTestBrowser.name).toEqual('test-browser') + expect(foundTestBrowser.displayName).toEqual('Test Browser') + expect(foundTestBrowser.majorVersion, 'majorVersion').toEqual('130') + expect(foundTestBrowser.unsupportedVersion, 'unsupportedVersion').toBeUndefined() + expect(foundTestBrowser.warning, 'warning').toBeUndefined() + expect(mockValidator).toHaveBeenCalled() + }) + }) + + describe('#detectByPath', () => { + let cpSpawnCallback: (cmd: string, args: readonly string[], opts, cp: cp.ChildProcess) => void + + beforeEach(() => { + vi.unstubAllEnvs() + vi.resetAllMocks() + + vi.mocked(cp.spawn).mockImplementation((cmd, args, opts) => { + const cpSpawnMock = { + on: vi.fn(), + stdout: new EventEmitter(), + stderr: new EventEmitter(), + kill: vi.fn(), + } + + cpSpawnMock.on.mockImplementation((event: string, callback: (...args: any[]) => void) => { + if (event === 'exit') { + setTimeout(() => callback(), 0) + } + + if (event === 'close') { + setTimeout(() => callback(), 0) + } + }) + + cpSpawnCallback(cmd, args, opts, cpSpawnMock as unknown as cp.ChildProcess) + + return cpSpawnMock as unknown as cp.ChildProcess + }) + + cpSpawnCallback = (cmd, args, opts, cpSpawnMock) => { + // FIXME: these tests really should be reworked to run the same regardless of OS/CPU architecture + const command = os.arch() === 'arm64' ? args[0] : cmd + + if (command === '/Applications/My Shiny New Browser.app') { + setTimeout(() => { + cpSpawnMock.stdout.emit('data', 'foo-browser v100.1.2.3') + }, 0) + + return + } + + if (command === '/foo/bar/browser') { + setTimeout(() => { + cpSpawnMock.stdout.emit('data', 'foo-browser v9001.1.2.3') + }, 0) + + return + } + + if (command === '/not/a/browser') { + setTimeout(() => { + cpSpawnMock.stdout.emit('data', 'not a browser version string') + }, 0) + + return + } + + if (command === '/not/a/real/path') { + setTimeout(() => { + cpSpawnMock.stdout.emit('data', '') + }, 0) + + return + } + } + }) + + it('detects by path', async () => { + // @ts-expect-error + const foundBrowser = await detectByPath('/foo/bar/browser', goalBrowsers) + + const expectedBrowser = goalBrowsers.find(({ name }) => name === 'foo-browser') + + expect(foundBrowser).toEqual({ + ...expectedBrowser, + displayName: 'Custom Foo Browser', + info: 'Loaded from /foo/bar/browser', + custom: true, + version: '9001.1.2.3', + majorVersion: '9001', + path: '/foo/bar/browser', + }) + }) + + it('rejects when there was no matching versionRegex', () => { + // @ts-ignore + return detectByPath('/not/a/browser', goalBrowsers) + .then(() => { + throw Error('Should not find a browser') + }) + .catch((err) => { + expect(err.notDetectedAtPath).to.be.true + }) + }) + + it('rejects when there was an error executing the command', async () => { + try { + // @ts-expect-error + await detectByPath('/not/a/real/path', goalBrowsers) + throw Error('Should not find a browser') + } catch (err) { + expect(err.notDetectedAtPath).toBe(true) + } + }) + + it('works with spaces in the path', async () => { + // @ts-expect-error + const foundBrowser = await detectByPath('/Applications/My Shiny New Browser.app', goalBrowsers) + + const expectedBrowser = goalBrowsers.find(({ name }) => name === 'foo-browser') + + expect(foundBrowser).toEqual({ + ...expectedBrowser, + displayName: 'Custom Foo Browser', + info: 'Loaded from /Applications/My Shiny New Browser.app', + custom: true, + version: '100.1.2.3', + majorVersion: '100', + path: '/Applications/My Shiny New Browser.app', + }) + }) + }) +}) diff --git a/packages/launcher/test/unit/detect_spec.ts b/packages/launcher/test/unit/detect_spec.ts deleted file mode 100644 index 3dec639cb4e..00000000000 --- a/packages/launcher/test/unit/detect_spec.ts +++ /dev/null @@ -1,174 +0,0 @@ -require('../spec_helper') -import _ from 'lodash' -import { detect, detectByPath, getMajorVersion } from '../../lib/detect' -import { goalBrowsers } from '../fixtures' -import { expect } from 'chai' -import { utils } from '../../lib/utils' -import sinon, { SinonStub } from 'sinon' -import os from 'os' -import { log } from '../log' -import * as linuxHelper from '../../lib/linux' -import * as darwinHelper from '../../lib/darwin' -import * as windowsHelper from '../../lib/windows' -import type { Browser } from '@packages/types' - -const isWindows = () => { - return os.platform() === 'win32' -} - -const stubHelpers = (detect) => { - sinon.stub(linuxHelper, 'detect').callsFake(detect) - sinon.stub(darwinHelper, 'detect').callsFake(detect) - sinon.stub(windowsHelper, 'detect').callsFake(detect) -} - -describe('detect', () => { - // making simple to debug tests - // using DEBUG=... flag - const checkBrowsers = (browsers) => { - log('detected browsers %j', browsers) - expect(browsers).to.be.an('array') - - const mainProps = browsers.map((val) => _.pick(val, ['name', 'version'])) - - log('%d browsers\n%j', browsers.length, mainProps) - - if (isWindows()) { - // we might not find any browsers on Windows CI - expect(browsers.length).to.be.gte(0) - } else { - expect(browsers.length).to.be.gt(0) - } - } - - // we are only going to run tests on platforms with at least - // one browser. This test, is really E2E because it finds - // real browsers - it('detects available browsers', () => { - return detect().then(checkBrowsers) - }) - - describe('#getMajorVersion', () => { - it('parses major version from provided string', () => { - expect(getMajorVersion('123.45.67')).to.eq('123') - expect(getMajorVersion('Browser 77.1.0')).to.eq('Browser 77') - expect(getMajorVersion('999')).to.eq('999') - }) - }) - - describe('#detect', () => { - const testBrowser = { - name: 'test-browser', - family: 'chromium', - channel: 'test-channel', - displayName: 'Test Browser', - versionRegex: /Test Browser (\S+)/m, - binary: 'test-browser-beta', - } - - it('validates browser with own validator property', async () => { - stubHelpers((browser) => { - return Promise.resolve({ - name: browser.name, - path: '/path/to/test-browser', - version: '130', - }) - }) - - const mockValidator = sinon.stub().returns({ isSupported: true }) - - const foundBrowsers = await detect([{ ...testBrowser as Browser, validator: mockValidator }]) - - expect(foundBrowsers).to.have.length(1) - - const foundTestBrowser = foundBrowsers[0] - - expect(foundTestBrowser.name).to.eq('test-browser') - expect(foundTestBrowser.displayName).to.eq('Test Browser') - expect(foundTestBrowser.majorVersion, 'majorVersion').to.eq('130') - expect(foundTestBrowser.unsupportedVersion, 'unsupportedVersion').to.be.undefined - expect(foundTestBrowser.warning, 'warning').to.be.undefined - expect(mockValidator).to.have.been.called - }) - }) - - describe('#detectByPath', () => { - let execa: SinonStub - - beforeEach(() => { - execa = sinon.stub(utils, 'getOutput') - - execa.withArgs('/Applications/My Shiny New Browser.app', ['--version']) - .resolves({ stdout: 'foo-browser v100.1.2.3' }) - - execa.withArgs('/foo/bar/browser', ['--version']) - .resolves({ stdout: 'foo-browser v9001.1.2.3' }) - - execa.withArgs('/not/a/browser', ['--version']) - .resolves({ stdout: 'not a browser version string' }) - - execa.withArgs('/not/a/real/path', ['--version']) - .rejects() - }) - - it('detects by path', () => { - // @ts-ignore - return detectByPath('/foo/bar/browser', goalBrowsers) - .then((browser) => { - expect(browser).to.deep.equal( - Object.assign({}, goalBrowsers.find((gb) => { - return gb.name === 'foo-browser' - }), { - displayName: 'Custom Foo Browser', - info: 'Loaded from /foo/bar/browser', - custom: true, - version: '9001.1.2.3', - majorVersion: '9001', - path: '/foo/bar/browser', - }), - ) - }) - }) - - it('rejects when there was no matching versionRegex', () => { - // @ts-ignore - return detectByPath('/not/a/browser', goalBrowsers) - .then(() => { - throw Error('Should not find a browser') - }) - .catch((err) => { - expect(err.notDetectedAtPath).to.be.true - }) - }) - - it('rejects when there was an error executing the command', () => { - // @ts-ignore - return detectByPath('/not/a/real/path', goalBrowsers) - .then(() => { - throw Error('Should not find a browser') - }) - .catch((err) => { - expect(err.notDetectedAtPath).to.be.true - }) - }) - - it('works with spaces in the path', () => { - // @ts-ignore - return detectByPath('/Applications/My Shiny New Browser.app', goalBrowsers) - .then((browser) => { - expect(browser).to.deep.equal( - Object.assign({}, goalBrowsers.find((gb) => { - return gb.name === 'foo-browser' - }), { - displayName: 'Custom Foo Browser', - info: 'Loaded from /Applications/My Shiny New Browser.app', - custom: true, - version: '100.1.2.3', - majorVersion: '100', - path: '/Applications/My Shiny New Browser.app', - }), - ) - }) - }) - }) -}) From 667e4787de266142e448aa5b8c8425515d351e91 Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Sun, 5 Oct 2025 23:15:29 -0400 Subject: [PATCH 06/16] chore: cleanup unused files --- .circleci/src/pipeline/@pipeline.yml | 2 +- packages/launcher/test/mocha.opts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 packages/launcher/test/mocha.opts diff --git a/.circleci/src/pipeline/@pipeline.yml b/.circleci/src/pipeline/@pipeline.yml index 94f58f9c051..b162ba5c52e 100644 --- a/.circleci/src/pipeline/@pipeline.yml +++ b/.circleci/src/pipeline/@pipeline.yml @@ -1786,7 +1786,7 @@ jobs: source ./scripts/ensure-node.sh yarn lerna run types - sanitize-verify-and-store-mocha-results: - expectedResultCount: 10 + expectedResultCount: 9 verify-release-readiness: <<: *defaults diff --git a/packages/launcher/test/mocha.opts b/packages/launcher/test/mocha.opts deleted file mode 100644 index 3622a281707..00000000000 --- a/packages/launcher/test/mocha.opts +++ /dev/null @@ -1,4 +0,0 @@ -test/unit ---compilers ts:@packages/ts/register ---timeout 10000 ---recursive From bc821e8affa4222d7ff51554fc0c4c2955d6ec8b Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Mon, 6 Oct 2025 10:48:05 -0400 Subject: [PATCH 07/16] chore: update browsers orb and replace existing google chrome install --- .circleci/src/pipeline/@pipeline.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/src/pipeline/@pipeline.yml b/.circleci/src/pipeline/@pipeline.yml index b162ba5c52e..a28e582266c 100644 --- a/.circleci/src/pipeline/@pipeline.yml +++ b/.circleci/src/pipeline/@pipeline.yml @@ -17,7 +17,7 @@ ubuntu-2004-current: &ubuntu-2004-current ubuntu-2004:2024.11.1 ubuntu-2004-older: &ubuntu-2004-older ubuntu-2004:2024.05.1 orbs: - browser-tools: circleci/browser-tools@2.1.1 + browser-tools: circleci/browser-tools@2.3.1 defaults: &defaults parallelism: 1 @@ -563,6 +563,7 @@ commands: # https://www.ubuntuupdates.org/package/google_chrome/stable/main/base/google-chrome-stable channel: << parameters.google-chrome-channel >> chrome_version: << parameters.google-chrome-version >> + replace_existing: true - when: condition: equal: [ 'beta', << parameters.google-chrome-channel>> ] From 1b1de360d545e584fe418a7627945c871eb40f0f Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Mon, 6 Oct 2025 18:48:11 -0400 Subject: [PATCH 08/16] chore: fix detect spec to use actual implementation of cp.spawn --- packages/launcher/test/unit/detect.spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/launcher/test/unit/detect.spec.ts b/packages/launcher/test/unit/detect.spec.ts index 2cf13854748..6b8fe7ba2fd 100644 --- a/packages/launcher/test/unit/detect.spec.ts +++ b/packages/launcher/test/unit/detect.spec.ts @@ -69,6 +69,10 @@ describe('detect', () => { vi.mocked(linuxDetect).mockImplementation(linuxDetectActual) vi.mocked(darwinDetect).mockImplementation(darwinDetectActual) vi.mocked(windowsDetect).mockImplementation(windowsDetectActual) + + const { spawn } = await vi.importActual('child_process') + + vi.mocked(cp.spawn).mockImplementation(spawn) }) // making simple to debug tests @@ -98,7 +102,7 @@ describe('detect', () => { describe('#getMajorVersion', () => { it('parses major version from provided string', () => { expect(getMajorVersion('123.45.67')).toEqual('123') - expect(getMajorVersion('Browser 77.1.0')).to.eq('Browser 77') + expect(getMajorVersion('Browser 77.1.0')).toEqual('Browser 77') expect(getMajorVersion('999')).toEqual('999') }) }) @@ -249,7 +253,7 @@ describe('detect', () => { throw Error('Should not find a browser') }) .catch((err) => { - expect(err.notDetectedAtPath).to.be.true + expect(err.notDetectedAtPath).toBe(true) }) }) From ebc2603756e212932e01f727571536a17cd33541 Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Tue, 7 Oct 2025 10:26:32 -0400 Subject: [PATCH 09/16] chore: convert launcher to async/await and remove bluebird --- packages/launcher/lib/browsers.ts | 4 +- packages/launcher/lib/darwin/index.ts | 12 +-- packages/launcher/lib/darwin/util.ts | 69 ++++++++------- packages/launcher/lib/detect.ts | 73 ++++++++++------ packages/launcher/lib/linux/index.ts | 69 ++++++++------- packages/launcher/lib/types.ts | 3 +- packages/launcher/lib/utils.ts | 113 ++++++++++++------------- packages/launcher/lib/windows/index.ts | 28 +++--- packages/launcher/package.json | 1 - 9 files changed, 200 insertions(+), 172 deletions(-) diff --git a/packages/launcher/lib/browsers.ts b/packages/launcher/lib/browsers.ts index 3a53865e3ae..9396e8bbbaa 100644 --- a/packages/launcher/lib/browsers.ts +++ b/packages/launcher/lib/browsers.ts @@ -1,6 +1,6 @@ import Debug from 'debug' import type * as cp from 'child_process' -import { utils } from './utils' +import { spawnWithArch } from './utils' import type { FoundBrowser } from '@packages/types' import type { Readable } from 'stream' @@ -36,7 +36,7 @@ export function launch ( debug('spawning browser with opts %o', { browser, url, spawnOpts }) - const proc = utils.spawnWithArch(browser.path, args, spawnOpts) + const proc = spawnWithArch(browser.path, args, spawnOpts) proc.stdout.on('data', (buf) => { debug('%s stdout: %s', browser.name, String(buf).trim()) diff --git a/packages/launcher/lib/darwin/index.ts b/packages/launcher/lib/darwin/index.ts index 8d4f7ee06c3..2880f20f3cd 100644 --- a/packages/launcher/lib/darwin/index.ts +++ b/packages/launcher/lib/darwin/index.ts @@ -103,7 +103,7 @@ export const getVersionNumber = linuxHelper.getVersionNumber export const getPathData = linuxHelper.getPathData -export function detect (browser: Browser): Promise { +export async function detect (browser: Browser): Promise { let findAppParams = get(browsers, [browser.name, browser.channel]) if (!findAppParams) { @@ -113,11 +113,13 @@ export function detect (browser: Browser): Promise { return linuxHelper.detect(browser) } - return findApp(findAppParams) - .then((val) => ({ name: browser.name, ...val })) - .catch((err) => { + try { + const val = await findApp(findAppParams) + + return { name: browser.name, ...val } + } catch (err) { debugVerbose('could not detect %s using findApp %o, falling back to linux detection method', browser.name, err) return linuxHelper.detect(browser) - }) + } } diff --git a/packages/launcher/lib/darwin/util.ts b/packages/launcher/lib/darwin/util.ts index da01b3d0a45..096ecd8846b 100644 --- a/packages/launcher/lib/darwin/util.ts +++ b/packages/launcher/lib/darwin/util.ts @@ -1,6 +1,6 @@ import Debug from 'debug' import { notInstalledErr } from '../errors' -import { utils } from '../utils' +import execa from 'execa' import fs from 'fs-extra' import path from 'path' import plist from 'plist' @@ -8,7 +8,7 @@ import plist from 'plist' const debugVerbose = Debug('cypress-verbose:launcher:darwin:util') /** parses Info.plist file from given application and returns a property */ -export function parsePlist (p: string, property: string): Promise { +export async function parsePlist (p: string, property: string): Promise { const pl = path.join(p, 'Contents', 'Info.plist') debugVerbose('reading property file "%s"', pl) @@ -21,16 +21,18 @@ export function parsePlist (p: string, property: string): Promise { throw notInstalledErr('', msg) } - return fs - .readFile(pl, 'utf8') - .then(plist.parse) - .then((val) => val[property]) - .then(String) // explicitly convert value to String type - .catch(failed) // to make TS compiler happy + try { + const file = await fs.readFile(pl, 'utf8') + const val = plist.parse(file) + + return String(val[property]) // explicitly convert value to String type + } catch (err) { + return failed(err) // to make TS compiler happy + } } /** uses mdfind to find app using Ma app id like 'com.google.Chrome.canary' */ -export function mdfind (id: string): Promise { +export async function mdfind (id: string): Promise { const cmd = `mdfind 'kMDItemCFBundleIdentifier=="${id}"' | head -1` debugVerbose('looking for bundle id %s using command: %s', id, cmd) @@ -46,16 +48,15 @@ export function mdfind (id: string): Promise { throw notInstalledErr(id) } - return utils.execa(cmd) - .then((val) => { - return val.stdout - }) - .then((val) => { - logFound(val) + try { + const val = await execa(cmd) - return val - }) - .catch(failedToFind) + logFound(val.stdout) + + return val.stdout + } catch (err) { + return failedToFind() + } } export type AppInfo = { @@ -79,22 +80,24 @@ function formApplicationPath (appName: string) { } /** finds an application and its version */ -export function findApp ({ appName, executable, bundleId, versionProperty }: FindAppParams): Promise { +export async function findApp ({ appName, executable, bundleId, versionProperty }: FindAppParams): Promise { debugVerbose('looking for app %s bundle id %s', executable, bundleId) - const findVersion = (foundPath: string) => { - return parsePlist(foundPath, versionProperty).then((version) => { - debugVerbose('got plist: %o', { foundPath, version }) + const findVersion = async (foundPath: string) => { + const version = await parsePlist(foundPath, versionProperty) + + debugVerbose('got plist: %o', { foundPath, version }) - return { - path: path.join(foundPath, executable), - version, - } - }) + return { + path: path.join(foundPath, executable), + version, + } } - const tryMdFind = () => { - return mdfind(bundleId).then(findVersion) + const tryMdFind = async () => { + const foundPath = await mdfind(bundleId) + + return findVersion(foundPath) } const tryFullApplicationFind = () => { @@ -105,5 +108,11 @@ export function findApp ({ appName, executable, bundleId, versionProperty }: Fin return findVersion(applicationPath) } - return tryMdFind().catch(tryFullApplicationFind) + try { + const val = await tryMdFind() + + return val + } catch (err) { + return tryFullApplicationFind() + } } diff --git a/packages/launcher/lib/detect.ts b/packages/launcher/lib/detect.ts index cba982d644f..929f6633238 100644 --- a/packages/launcher/lib/detect.ts +++ b/packages/launcher/lib/detect.ts @@ -1,4 +1,3 @@ -import Bluebird from 'bluebird' import _, { compact, extend, find } from 'lodash' import os from 'os' import { removeDuplicateBrowsers } from '@packages/data-context/src/sources/BrowserDataSource' @@ -94,17 +93,19 @@ function lookup ( * one for each binary. If Windows is detected, only one `checkOneBrowser` will be called, because * we don't use the `binary` field on Windows. */ -function checkBrowser (browser: Browser): Bluebird<(boolean | HasVersion)[]> { +async function checkBrowser (browser: Browser): Promise<(boolean | HasVersion)[]> { if (Array.isArray(browser.binary) && os.platform() !== 'win32') { - return Bluebird.map(browser.binary, (binary: string) => { - return checkOneBrowser(extend({}, browser, { binary })) - }) + const checkedBrowsers = await Promise.all(browser.binary.map((binary) => checkOneBrowser(extend({}, browser, { binary })))) + + return checkedBrowsers } - return Bluebird.map([browser], checkOneBrowser) + const checkedBrowsers = await checkOneBrowser(browser) + + return [checkedBrowsers] } -function checkOneBrowser (browser: Browser): Promise { +async function checkOneBrowser (browser: Browser): Promise { const platform = os.platform() const pickBrowserProps = [ 'name', @@ -131,21 +132,25 @@ function checkOneBrowser (browser: Browser): Promise { throw err } - return lookup(platform, browser) - .then((val) => ({ ...browser, ...val })) - .then((val) => _.pick(val, pickBrowserProps) as FoundBrowser) - .then((foundBrowser) => { + try { + const detectedBrowser = await lookup(platform, browser) + + const browserWithDetected = { ...browser, ...detectedBrowser } + + const foundBrowser = _.pick(browserWithDetected, pickBrowserProps) as FoundBrowser + foundBrowser.majorVersion = getMajorVersion(foundBrowser.version) validateCypressSupport(browser.validator, foundBrowser, platform) return foundBrowser - }) - .catch(failed) + } catch (error) { + return failed(error as NotInstalledError) + } } /** returns list of detected browsers */ -export const detect = (goalBrowsers?: Browser[]): Bluebird => { +export const detect = async (goalBrowsers?: Browser[]): Promise => { // we can detect same browser under different aliases // tell them apart by the name and the version property if (!goalBrowsers) { @@ -158,13 +163,27 @@ export const detect = (goalBrowsers?: Browser[]): Bluebird => { debug('detecting if the following browsers are present %o', goalBrowsers) - return Bluebird.mapSeries(goalBrowsers, checkBrowser) - .then((val) => _.flatten(val)) - .then(compactFalse) - .then(removeDuplicateBrowsers) + let foundBrowsers: FoundBrowser[] = [] + + { + const hasVersionOrFalse: (boolean | HasVersion)[][] = [] + + for (const browser of goalBrowsers) { + const browserOrFalse = await checkBrowser(browser) + + hasVersionOrFalse.push(browserOrFalse) + } + + const flattenedFoundBrowsers = _.flatten(hasVersionOrFalse) + const compactedFoundBrowsers = compactFalse(flattenedFoundBrowsers) + + foundBrowsers = removeDuplicateBrowsers(compactedFoundBrowsers) + } + + return foundBrowsers } -export const detectByPath = ( +export const detectByPath = async ( path: string, goalBrowsers?: Browser[], ): Promise => { @@ -210,8 +229,9 @@ export const detectByPath = ( const pathData = helper.getPathData(path) - return helper.getVersionString(pathData.path) - .then((version) => { + try { + const version = await helper.getVersionString(pathData.path) + let browser if (pathData.browserKey) { @@ -227,12 +247,11 @@ export const detectByPath = ( } return setCustomBrowserData(browser, pathData.path, version) - }) - .catch((err: NotDetectedAtPathError) => { - if (err.notDetectedAtPath) { - throw err + } catch (error: any) { + if (error.notDetectedAtPath) { + throw error as NotDetectedAtPathError } - throw notDetectedAtPathErr(err.message) - }) + throw notDetectedAtPathErr(error.message) + } } diff --git a/packages/launcher/lib/linux/index.ts b/packages/launcher/lib/linux/index.ts index c0602d71797..9c053b97fb1 100644 --- a/packages/launcher/lib/linux/index.ts +++ b/packages/launcher/lib/linux/index.ts @@ -2,33 +2,37 @@ import Debug from 'debug' import type { FoundBrowser, Browser } from '@packages/types' import type { PathData } from '../types' import { notInstalledErr } from '../errors' -import { utils } from '../utils' +import { getOutput } from '../utils' import os from 'os' import { promises as fs } from 'fs' import path from 'path' -import Bluebird from 'bluebird' import which from 'which' const debug = Debug('cypress:launcher:linux') const debugVerbose = Debug('cypress-verbose:launcher:linux') +const createTimeoutPromise = (timeout: number = 30000, message: string = `Timed out after ${timeout} seconds`) => { + return new Promise((_, reject) => { + setTimeout(() => { + reject(new Error(message)) + }, timeout) + }) +} + async function isFirefoxSnap (binary: string): Promise { try { - return await Bluebird.resolve((async () => { - const binaryPath = await which(binary) + const binaryPath = await which(binary) - // if the bin path or what it's symlinked to start with `/snap/bin`, it's a snap - if (binaryPath.startsWith('/snap/bin/') || (await fs.realpath(binaryPath)).startsWith('/snap/bin')) return true + // if the bin path or what it's symlinked to start with `/snap/bin`, it's a snap + if (binaryPath.startsWith('/snap/bin/') || (await fs.realpath(binaryPath)).startsWith('/snap/bin')) return true - // read the first 16kb, don't read the entire file into memory in case it is a binary - const fd = await fs.open(binaryPath, 'r') - const { buffer, bytesRead } = await fd.read({ length: 16384 }) + // read the first 16kb, don't read the entire file into memory in case it is a binary + const fd = await fs.open(binaryPath, 'r') + const { buffer, bytesRead } = await fd.read({ length: 16384 }) - await fd.close() + await fd.close() - return buffer.slice(0, bytesRead).toString('utf8').includes('exec /snap/bin/firefox') - })()) - .timeout(30000) + return buffer.slice(0, bytesRead).toString('utf8').includes('exec /snap/bin/firefox') } catch (err) { debug('failed to check if Firefox is a snap, assuming it isn\'t %o', { err, binary }) @@ -36,7 +40,7 @@ async function isFirefoxSnap (binary: string): Promise { } } -function getLinuxBrowser ( +async function getLinuxBrowser ( name: string, binary: string, versionRegex: RegExp, @@ -46,7 +50,7 @@ function getLinuxBrowser ( path: binary, } - const getVersion = (stdout: string) => { + const getVersion = async (stdout: string) => { const m = versionRegex.exec(stdout) if (m) { @@ -85,7 +89,7 @@ function getLinuxBrowser ( return } - if (name === 'firefox' && (await isFirefoxSnap(binary))) { + if (name === 'firefox' && (await Promise.race([isFirefoxSnap(binary), createTimeoutPromise(30000, 'Timed out after 30 seconds checking if Firefox is a snap')]))) { // if the binary in the path points to a script that calls the snap, set a snap-specific profile path // @see https://github.com/cypress-io/cypress/issues/19793 debug('firefox is running as a snap, changing profile path') @@ -95,29 +99,30 @@ function getLinuxBrowser ( } } - return getVersionString(binary) - .tap(maybeSetSnapProfilePath) - .then(getVersion) - .then((version?: string): FoundBrowser => { + try { + const versionString = await getVersionString(binary) + + await maybeSetSnapProfilePath(versionString) + const version = await getVersion(versionString) + foundBrowser.version = version - return foundBrowser - }) - .catch(logAndThrowError) + return foundBrowser as FoundBrowser + } catch (err) { + return logAndThrowError(err) + } } -export function getVersionString (path: string) { +export async function getVersionString (path: string) { debugVerbose('finding version string using command "%s --version"', path) - return Bluebird.resolve(utils.getOutput(path, ['--version'])) - .timeout(30000, `Timed out after 30 seconds getting browser version for ${path}`) - .then((val) => val.stdout) - .then((val) => val.trim()) - .then((val) => { - debugVerbose('stdout for "%s --version": %s', path, val) + const timeoutPromise = createTimeoutPromise(30000, `Timed out after 30 seconds getting browser version for ${path}`) + const { stdout } = await Promise.race([getOutput(path, ['--version']), timeoutPromise]) as { stdout: string } + const trimmedStdout = stdout.trim() - return val - }) + debugVerbose('stdout for "%s --version": %s', path, trimmedStdout) + + return trimmedStdout } export function getVersionNumber (version: string, browser: Browser) { diff --git a/packages/launcher/lib/types.ts b/packages/launcher/lib/types.ts index bc08c1fdbb4..54f69ceeebf 100644 --- a/packages/launcher/lib/types.ts +++ b/packages/launcher/lib/types.ts @@ -1,5 +1,4 @@ import type { ChildProcess } from 'child_process' -import type Bluebird from 'bluebird' import type { Browser, FoundBrowser } from '@packages/types' export type NotInstalledError = Error & { notInstalled: boolean } @@ -7,7 +6,7 @@ export type NotInstalledError = Error & { notInstalled: boolean } export type NotDetectedAtPathError = Error & { notDetectedAtPath: boolean } export type LauncherApi = { - detect: (goalBrowsers?: Browser[]) => Bluebird + detect: (goalBrowsers?: Browser[]) => Promise detectByPath: ( path: string, goalBrowsers?: Browser[] diff --git a/packages/launcher/lib/utils.ts b/packages/launcher/lib/utils.ts index c2e68a94836..4f58378250c 100644 --- a/packages/launcher/lib/utils.ts +++ b/packages/launcher/lib/utils.ts @@ -1,72 +1,67 @@ -import execa from 'execa' import cp from 'child_process' import os from 'os' -import Bluebird from 'bluebird' -// export an object for easy method stubbing -export const utils = { - execa, - spawnWithArch: >(cmd: string, args: string[], opts: T) => { - if (os.platform() === 'darwin' && os.arch() === 'arm64') { - // On macOS, browsers are distributed as "universal apps" which have both arm64 and x86_64 binaries - // in the same file. The OS decides which architecture to use based on heuristics. If Cypress was - // launched from an x86_64 process on arm64 macOS (like if an x64 version of Node.js is being used), - // even though the Cypress CLI will correctly spawn the arm64 version of Cypress, when we spawn the - // browser macOS will decide to use the x86_64 version, not the arm64 version. This is problematic - // because the Rosetta translation is painfully slow. To work around this, we wrap the spawn with - // the `arch` utility, which will launch the correct architecture (arm64) if it is available in the - // universal app, otherwise falling back to x86_64. - return cp.spawn( - 'arch', - [cmd, ...args], - { - ...opts, - env: { - ARCHPREFERENCE: 'arm64,x86_64', - ...opts.env, - }, - } as T, - ) - } +export const spawnWithArch = >(cmd: string, args: string[], opts: T) => { + if (os.platform() === 'darwin' && os.arch() === 'arm64') { + // On macOS, browsers are distributed as "universal apps" which have both arm64 and x86_64 binaries + // in the same file. The OS decides which architecture to use based on heuristics. If Cypress was + // launched from an x86_64 process on arm64 macOS (like if an x64 version of Node.js is being used), + // even though the Cypress CLI will correctly spawn the arm64 version of Cypress, when we spawn the + // browser macOS will decide to use the x86_64 version, not the arm64 version. This is problematic + // because the Rosetta translation is painfully slow. To work around this, we wrap the spawn with + // the `arch` utility, which will launch the correct architecture (arm64) if it is available in the + // universal app, otherwise falling back to x86_64. + return cp.spawn( + 'arch', + [cmd, ...args], + { + ...opts, + env: { + ARCHPREFERENCE: 'arm64,x86_64', + ...opts.env, + }, + } as T, + ) + } - // Outside of darwin-arm64, we can rely on the OS to launch the correct architecture of the browser. - return cp.spawn(cmd, args, opts) - }, - getOutput: (cmd: string, args: string[]): Bluebird<{ stdout: string, stderr?: string }> => { - if (os.platform() === 'win32') { - // execa has better support for windows spawning conventions - throw new Error('getOutput should not be used on Windows - use execa instead') - } + // Outside of darwin-arm64, we can rely on the OS to launch the correct architecture of the browser. + return cp.spawn(cmd, args, opts) +} - return new Bluebird((resolve, reject) => { - let stdout = '' - let stderr = '' +export const getOutput = (cmd: string, args: string[]): Promise<{ stdout: string, stderr?: string }> => { + if (os.platform() === 'win32') { + // execa has better support for windows spawning conventions + throw new Error('getOutput should not be used on Windows - use execa instead') + } - const proc = utils.spawnWithArch(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], env: process.env }) + return new Promise((resolve, reject) => { + let stdout = '' + let stderr = '' - const finish = () => { - proc.kill() - resolve({ stderr, stdout }) - } + const proc = spawnWithArch(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], env: process.env }) - // the "exit" event might happen before - // the child streams are finished, thus we use - // the "close" event - // https://github.com/cypress-io/cypress/issues/8611 - proc.on('close', finish) + const finish = () => { + proc.kill() + resolve({ stderr, stdout }) + } - proc.stdout.on('data', (chunk) => { - stdout += chunk - }) + // the "exit" event might happen before + // the child streams are finished, thus we use + // the "close" event + // https://github.com/cypress-io/cypress/issues/8611 + proc.on('close', finish) - proc.stderr.on('data', (chunk) => { - stderr += chunk - }) + proc.stdout.on('data', (chunk) => { + stdout += chunk + }) + + proc.stderr.on('data', (chunk) => { + stderr += chunk + }) - proc.on('error', (err) => { - proc.kill() - reject(err) - }) + proc.on('error', (err) => { + proc.kill() + reject(err) }) - }, + }) } diff --git a/packages/launcher/lib/windows/index.ts b/packages/launcher/lib/windows/index.ts index 69c1986b503..0046a20685d 100644 --- a/packages/launcher/lib/windows/index.ts +++ b/packages/launcher/lib/windows/index.ts @@ -144,8 +144,9 @@ function getWindowsBrowser (browser: Browser): Promise { let path = doubleEscape(exePath) - return fs.pathExists(path) - .then((exists) => { + try { + const exists = await fs.pathExists(path) + debugVerbose('found %s ? %o', path, { exists }) if (!exists) { @@ -154,21 +155,20 @@ function getWindowsBrowser (browser: Browser): Promise { // Use module.exports.getVersionString here, rather than our local reference // to that variable so that the tests can easily mock it - return getVersionString(path).then((version) => { - debug('got version string for %s: %o', browser.name, { exePath, version }) - - return { - name: browser.name, - version, - path: exePath, - } as FoundBrowser - }) - }) - .catch((err) => { + const version = await getVersionString(path) + + debug('got version string for %s: %o', browser.name, { exePath, version }) + + return { + name: browser.name, + version, + path: exePath, + } as FoundBrowser + } catch (err) { debug('error while looking up exe, trying next exePath %o', { exePath, exePaths, err }) return tryNextExePath() - }) + } } return tryNextExePath() diff --git a/packages/launcher/package.json b/packages/launcher/package.json index 1c4f334f1ce..5d73a2a2b97 100644 --- a/packages/launcher/package.json +++ b/packages/launcher/package.json @@ -16,7 +16,6 @@ "tslint": "tslint --config ../ts/tslint.json --project ." }, "dependencies": { - "bluebird": "3.5.3", "debug": "^4.3.4", "execa": "4.1.0", "fs-extra": "9.1.0", From cbdcae7b9fa9511592ad9bd9f72d87092318c37c Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Mon, 6 Oct 2025 21:37:25 -0400 Subject: [PATCH 10/16] chore: start refactoring launcher code in server fix type issues --- packages/server/lib/browsers/utils.ts | 13 ++-- packages/server/lib/cypress.ts | 80 ++++++++++++-------- packages/server/lib/modes/exit.ts | 8 -- packages/server/lib/modes/info.ts | 11 ++- packages/server/lib/modes/pkg.ts | 6 -- packages/server/test/unit/modes/info_spec.js | 2 +- 6 files changed, 60 insertions(+), 60 deletions(-) delete mode 100644 packages/server/lib/modes/exit.ts delete mode 100644 packages/server/lib/modes/pkg.ts diff --git a/packages/server/lib/browsers/utils.ts b/packages/server/lib/browsers/utils.ts index 30b021d7077..0f0617b6205 100644 --- a/packages/server/lib/browsers/utils.ts +++ b/packages/server/lib/browsers/utils.ts @@ -344,20 +344,21 @@ async function ensureAndGetByNameOrPath (nameOrPath: string, returnAll = false, // did the user give a bad name, or is this actually a path? if (isValidPathToBrowser(nameOrPath)) { // looks like a path - try to resolve it to a FoundBrowser - return launcher.detectByPath(nameOrPath) - .then((browser) => { + try { + const browser = await launcher.detectByPath(nameOrPath) + if (returnAll) { return [browser].concat(browsers) } return browser - }).catch((err) => { - errors.throwErr('BROWSER_NOT_FOUND_BY_PATH', nameOrPath, err.message) - }) + } catch (err) { + return errors.throwErr('BROWSER_NOT_FOUND_BY_PATH', nameOrPath, err.message) + } } // not a path, not found by name - throwBrowserNotFound(nameOrPath, browsers) + return throwBrowserNotFound(nameOrPath, browsers) } const formatBrowsersToOptions = (browsers) => { diff --git a/packages/server/lib/cypress.ts b/packages/server/lib/cypress.ts index a6d06a0b6aa..dacbdbb62d9 100644 --- a/packages/server/lib/cypress.ts +++ b/packages/server/lib/cypress.ts @@ -16,6 +16,9 @@ import argsUtils from './util/args' import { telemetry } from '@packages/telemetry' import { getCtx, hasCtx } from '@packages/data-context' import { warning as errorsWarning } from './errors' +import pkg from '@packages/root' +import { info } from './modes/info' +import { toNumber } from 'lodash' const debug = Debug('cypress:server:cypress') @@ -206,55 +209,66 @@ export = { }) }, - startInMode (mode: Mode, options: any) { + async startInMode (mode: Mode, options: any) { debug('starting in mode %s with options %o', mode, options) switch (mode) { case 'version': - return require('./modes/pkg')(options) - .get('version') - .then((version: any) => { - return console.log(version) // eslint-disable-line no-console - }).then(exit0) - .catch(exitErr) + try { + console.log(pkg.version)// eslint-disable-line no-console + return exit0() + } catch (err) { + return exitErr(err) + } case 'info': - return require('./modes/info')(options) - .then(exit0) - .catch(exitErr) + try { + await info() + return exit0() + } catch (err) { + return exitErr(err) + } case 'smokeTest': - return this.runElectron(mode, options) - .then((pong: any) => { + try { + const pong = await this.runElectron(mode, options) + if (!this.isCurrentlyRunningElectron()) { - return pong + return exit(pong) } if (pong === options.ping) { - return 0 + return exit(0) } - return 1 - }).then(exit) - .catch(exitErr) + return exit(1) + } catch (err) { + return exitErr(err) + } case 'returnPkg': - return require('./modes/pkg')(options) - .then((pkg: any) => { - return console.log(JSON.stringify(pkg)) // eslint-disable-line no-console - }).then(exit0) - .catch(exitErr) + try { + console.log(JSON.stringify(pkg)) // eslint-disable-line no-console + + return exit0() + } catch (err) { + return exitErr(err) + } case 'exitWithCode': - return require('./modes/exit')(options) - .then(exit) - .catch(exitErr) + try { + const exitCode = toNumber(options.exitWithCode) + return exit(exitCode) + } catch (err) { + return exitErr(err) + } case 'run': // run headlessly and exit // with num of totalFailed - return this.runElectron(mode, options) - .then((results: any) => { + try { + const results = await this.runElectron(mode, options) + if (results.runs) { const isCanceled = results.runs.filter((run) => run.skippedSpec).length @@ -262,18 +276,18 @@ export = { // eslint-disable-next-line no-console console.log(require('chalk').magenta('\n Exiting with non-zero exit code because the run was canceled.')) - return 1 + return exit(1) } } if (options.posixExitCodes) { - return results.totalFailed ? 1 : 0 + return exit(results.totalFailed ? 1 : 0) } - return results.totalFailed - }) - .then(exit) - .catch(exitErr) + return exit(results.totalFailed) + } catch (err) { + return exitErr(err) + } case 'interactive': return this.runElectron(mode, options) diff --git a/packages/server/lib/modes/exit.ts b/packages/server/lib/modes/exit.ts deleted file mode 100644 index 426ccb0bcc9..00000000000 --- a/packages/server/lib/modes/exit.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { toNumber } from 'lodash' -import Promise from 'bluebird' - -export = (options) => { - return Promise.try(() => { - return toNumber(options.exitWithCode) - }) -} diff --git a/packages/server/lib/modes/info.ts b/packages/server/lib/modes/info.ts index dbfc55655c7..fd3c847b7bb 100644 --- a/packages/server/lib/modes/info.ts +++ b/packages/server/lib/modes/info.ts @@ -127,10 +127,9 @@ const print = (browsers: FoundBrowser[] = []) => { } } -const info = () => { - return launcherDetect() - .then(addProfilePath) - .then(print) -} +export const info = async () => { + const browsers = await launcherDetect() + const browsersWithProfilePath = await addProfilePath(browsers) -module.exports = info + print(browsersWithProfilePath) +} diff --git a/packages/server/lib/modes/pkg.ts b/packages/server/lib/modes/pkg.ts deleted file mode 100644 index 2e781081247..00000000000 --- a/packages/server/lib/modes/pkg.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Promise from 'bluebird' -import pkg from '@packages/root' - -export = () => { - return Promise.resolve(pkg) -} diff --git a/packages/server/test/unit/modes/info_spec.js b/packages/server/test/unit/modes/info_spec.js index f77de4c9c50..3225cb4bc7c 100644 --- a/packages/server/test/unit/modes/info_spec.js +++ b/packages/server/test/unit/modes/info_spec.js @@ -1,6 +1,6 @@ require('../../spec_helper') -const info = require(`../../../lib/modes/info`) +const { info } = require(`../../../lib/modes/info`) const capture = require(`../../../lib/capture`) const browserUtils = require(`../../../lib/browsers/utils`) const { fs } = require(`../../../lib/util/fs`) From 0a2a58669a414f0e058f020fc1dcf07ba193189b Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Tue, 7 Oct 2025 12:39:23 -0400 Subject: [PATCH 11/16] chore: fix browser path system test --- .../__snapshots__/browser_path_spec.js | 8 +-- system-tests/test/browser_path_spec.js | 59 +++++++++---------- 2 files changed, 29 insertions(+), 38 deletions(-) diff --git a/system-tests/__snapshots__/browser_path_spec.js b/system-tests/__snapshots__/browser_path_spec.js index 90b5285e782..60e3890a8e7 100644 --- a/system-tests/__snapshots__/browser_path_spec.js +++ b/system-tests/__snapshots__/browser_path_spec.js @@ -31,18 +31,12 @@ exports['e2e launching browsers by path works with an installed browser path 1'] │ Pending: 0 │ │ Skipped: 0 │ │ Screenshots: 0 │ - │ Video: true │ + │ Video: false │ │ Duration: X seconds │ │ Spec Ran: simple.cy.js │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ - (Video) - - - Started compressing: Compressing to 32 CRF - - Finished compressing: /XXX/XXX/XXX/cypress/videos/simple.cy.js.mp4 (X second) - - ==================================================================================================== (Run Finished) diff --git a/system-tests/test/browser_path_spec.js b/system-tests/test/browser_path_spec.js index f2bc161d4d9..cda4a4ef8f4 100644 --- a/system-tests/test/browser_path_spec.js +++ b/system-tests/test/browser_path_spec.js @@ -23,40 +23,37 @@ const absPath = (pathStr) => { describe('e2e launching browsers by path', () => { systemTests.setup() - it('fails with bad browser path', function () { - return systemTests.exec(this, { - project: 'e2e', - spec: 'simple.cy.js', - browser: '/this/aint/gonna/be/found', - expectedExitCode: 1, - }) - .then((res) => { - expect(res.stdout).to.contain('We could not identify a known browser at the path you provided: `/this/aint/gonna/be/found`') - - expect(res.code).to.eq(1) - }) - }) - - it('works with an installed browser path', function () { - return launcher.detect().then((browsers) => { - return browsers.find((browser) => { - return browser.family === 'chromium' - }) - }).tap((browser) => { - if (!browser) { - throw new Error('A \'chromium\' family browser must be installed for this test') - } - }).get('path') - // turn binary browser names ("google-chrome") into their absolute paths - // so that server recognizes them as a path, not as a browser name - .then((absPath)) - .then((foundPath) => { - return systemTests.exec(this, { + it('fails with bad browser path', async function () { + try { + await systemTests.exec(this, { project: 'e2e', spec: 'simple.cy.js', - browser: foundPath, - snapshot: true, + browser: '/this/aint/gonna/be/found', + expectedExitCode: 1, }) + } catch (err) { + expect(err.message).to.contain('We could not identify a known browser at the path you provided: `/this/aint/gonna/be/found`') + + expect(err.code).to.eq(1) + } + }) + + it('works with an installed browser path', async function () { + const browsers = await launcher.detect() + const browser = browsers.find((browser) => browser.family === 'chromium') + + if (!browser) { + throw new Error('A \'chromium\' family browser must be installed for this test') + } + + const absolutePath = await absPath(browser.path) + + return systemTests.exec(this, { + project: 'e2e', + spec: 'simple.cy.js', + browser: absolutePath, + snapshot: true, + video: false, }) }) }) From 400b56fd74460ff5673924ef66fb47b047afc8a5 Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Tue, 7 Oct 2025 12:40:04 -0400 Subject: [PATCH 12/16] chore: update docs --- guides/esm-migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/esm-migration.md b/guides/esm-migration.md index e0ce58c6380..71cd4a7ca41 100644 --- a/guides/esm-migration.md +++ b/guides/esm-migration.md @@ -50,7 +50,7 @@ When migrating some of these projects away from the `ts-node` entry [see `@packa - [x] packages/electron ✅ **COMPLETED** - [ ] packages/https-proxy **PARTIAL** - entry point is JS - [x] packages/icons ✅ **COMPLETED** -- [x] packages/launcher ✅ **COMPLETED** +- [x] packages/launcher ✅ **COMPLETED** (needs independent bundle but lower priority) - [x] packages/launchpad ✅ **COMPLETED** - [x] packages/net-stubbing ✅ **COMPLETED** - [ ] packages/network **PARTIAL** - entry point is JS From e533990a8b088a3061f442de01421a58ae05ca07 Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Tue, 7 Oct 2025 12:56:23 -0400 Subject: [PATCH 13/16] throw err --- packages/server/lib/browsers/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/lib/browsers/utils.ts b/packages/server/lib/browsers/utils.ts index 0f0617b6205..7673586d0ca 100644 --- a/packages/server/lib/browsers/utils.ts +++ b/packages/server/lib/browsers/utils.ts @@ -353,12 +353,12 @@ async function ensureAndGetByNameOrPath (nameOrPath: string, returnAll = false, return browser } catch (err) { - return errors.throwErr('BROWSER_NOT_FOUND_BY_PATH', nameOrPath, err.message) + errors.throwErr('BROWSER_NOT_FOUND_BY_PATH', nameOrPath, err.message) } } // not a path, not found by name - return throwBrowserNotFound(nameOrPath, browsers) + throwBrowserNotFound(nameOrPath, browsers) } const formatBrowsersToOptions = (browsers) => { From 893e7e8c070d334cea6dd2203760281ba1ced969 Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Tue, 7 Oct 2025 13:11:38 -0400 Subject: [PATCH 14/16] fix --- packages/launcher/lib/linux/index.ts | 30 +++++++++++++++++----------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/launcher/lib/linux/index.ts b/packages/launcher/lib/linux/index.ts index 9c053b97fb1..c18e72b75ae 100644 --- a/packages/launcher/lib/linux/index.ts +++ b/packages/launcher/lib/linux/index.ts @@ -21,18 +21,9 @@ const createTimeoutPromise = (timeout: number = 30000, message: string = `Timed async function isFirefoxSnap (binary: string): Promise { try { - const binaryPath = await which(binary) + const result = await Promise.race([getFirefoxSnap(binary), createTimeoutPromise(30000, 'Timed out after 30 seconds checking if Firefox is a snap')]) as Promise - // if the bin path or what it's symlinked to start with `/snap/bin`, it's a snap - if (binaryPath.startsWith('/snap/bin/') || (await fs.realpath(binaryPath)).startsWith('/snap/bin')) return true - - // read the first 16kb, don't read the entire file into memory in case it is a binary - const fd = await fs.open(binaryPath, 'r') - const { buffer, bytesRead } = await fd.read({ length: 16384 }) - - await fd.close() - - return buffer.slice(0, bytesRead).toString('utf8').includes('exec /snap/bin/firefox') + return result } catch (err) { debug('failed to check if Firefox is a snap, assuming it isn\'t %o', { err, binary }) @@ -40,6 +31,21 @@ async function isFirefoxSnap (binary: string): Promise { } } +async function getFirefoxSnap (binary: string): Promise { + const binaryPath = await which(binary) + + // if the bin path or what it's symlinked to start with `/snap/bin`, it's a snap + if (binaryPath.startsWith('/snap/bin/') || (await fs.realpath(binaryPath)).startsWith('/snap/bin')) return true + + // read the first 16kb, don't read the entire file into memory in case it is a binary + const fd = await fs.open(binaryPath, 'r') + const { buffer, bytesRead } = await fd.read({ length: 16384 }) + + await fd.close() + + return buffer.slice(0, bytesRead).toString('utf8').includes('exec /snap/bin/firefox') +} + async function getLinuxBrowser ( name: string, binary: string, @@ -89,7 +95,7 @@ async function getLinuxBrowser ( return } - if (name === 'firefox' && (await Promise.race([isFirefoxSnap(binary), createTimeoutPromise(30000, 'Timed out after 30 seconds checking if Firefox is a snap')]))) { + if (name === 'firefox' && await isFirefoxSnap(binary)) { // if the binary in the path points to a script that calls the snap, set a snap-specific profile path // @see https://github.com/cypress-io/cypress/issues/19793 debug('firefox is running as a snap, changing profile path') From f82d283af48eee63e3a12d4a61bb0f00c791c159 Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Tue, 7 Oct 2025 13:44:46 -0400 Subject: [PATCH 15/16] chore: make launcher an independent package --- guides/esm-migration.md | 2 +- packages/launcher/.eslintignore | 3 ++- packages/launcher/.gitignore | 2 ++ packages/launcher/index.ts | 11 ----------- packages/launcher/lib/darwin/util.ts | 5 +++-- packages/launcher/lib/detect.ts | 15 +++++++++++---- packages/launcher/lib/index.ts | 9 +++++++++ packages/launcher/lib/linux/index.ts | 2 +- packages/launcher/lib/windows/index.ts | 14 ++++++++++---- packages/launcher/package.json | 19 ++++++++++++------- packages/launcher/tsconfig.cjs.json | 10 ++++++++++ packages/launcher/tsconfig.esm.json | 11 +++++++++++ packages/launcher/tsconfig.json | 25 +++++++++++++++---------- packages/server/lib/browsers/utils.ts | 4 ++-- yarn.lock | 13 +++++++++---- 15 files changed, 98 insertions(+), 47 deletions(-) create mode 100644 packages/launcher/.gitignore delete mode 100644 packages/launcher/index.ts create mode 100644 packages/launcher/lib/index.ts create mode 100644 packages/launcher/tsconfig.cjs.json create mode 100644 packages/launcher/tsconfig.esm.json diff --git a/guides/esm-migration.md b/guides/esm-migration.md index 71cd4a7ca41..e0ce58c6380 100644 --- a/guides/esm-migration.md +++ b/guides/esm-migration.md @@ -50,7 +50,7 @@ When migrating some of these projects away from the `ts-node` entry [see `@packa - [x] packages/electron ✅ **COMPLETED** - [ ] packages/https-proxy **PARTIAL** - entry point is JS - [x] packages/icons ✅ **COMPLETED** -- [x] packages/launcher ✅ **COMPLETED** (needs independent bundle but lower priority) +- [x] packages/launcher ✅ **COMPLETED** - [x] packages/launchpad ✅ **COMPLETED** - [x] packages/net-stubbing ✅ **COMPLETED** - [ ] packages/network **PARTIAL** - entry point is JS diff --git a/packages/launcher/.eslintignore b/packages/launcher/.eslintignore index 15730005ffd..b68d920cde2 100644 --- a/packages/launcher/.eslintignore +++ b/packages/launcher/.eslintignore @@ -1,4 +1,5 @@ -**/dist +**/cjs +**/esm **/*.d.ts **/package-lock.json **/tsconfig.json diff --git a/packages/launcher/.gitignore b/packages/launcher/.gitignore new file mode 100644 index 00000000000..929bed061c1 --- /dev/null +++ b/packages/launcher/.gitignore @@ -0,0 +1,2 @@ +cjs/ +esm/ \ No newline at end of file diff --git a/packages/launcher/index.ts b/packages/launcher/index.ts deleted file mode 100644 index 2a98e6ff42f..00000000000 --- a/packages/launcher/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { detect, detectByPath } from './lib/detect' - -import { launch } from './lib/browsers' - -export { - detect, - detectByPath, - launch, -} - -export * from './lib/types' diff --git a/packages/launcher/lib/darwin/util.ts b/packages/launcher/lib/darwin/util.ts index 096ecd8846b..d64af65e94e 100644 --- a/packages/launcher/lib/darwin/util.ts +++ b/packages/launcher/lib/darwin/util.ts @@ -4,6 +4,7 @@ import execa from 'execa' import fs from 'fs-extra' import path from 'path' import plist from 'plist' +import type { PlistValue } from 'plist' const debugVerbose = Debug('cypress-verbose:launcher:darwin:util') @@ -25,9 +26,9 @@ export async function parsePlist (p: string, property: string): Promise const file = await fs.readFile(pl, 'utf8') const val = plist.parse(file) - return String(val[property]) // explicitly convert value to String type + return String(val[property as keyof PlistValue]) // explicitly convert value to String type } catch (err) { - return failed(err) // to make TS compiler happy + return failed(err as Error) // to make TS compiler happy } } diff --git a/packages/launcher/lib/detect.ts b/packages/launcher/lib/detect.ts index 929f6633238..a3b68c846f3 100644 --- a/packages/launcher/lib/detect.ts +++ b/packages/launcher/lib/detect.ts @@ -1,6 +1,5 @@ -import _, { compact, extend, find } from 'lodash' +import _, { compact, extend, find, uniqBy } from 'lodash' import os from 'os' -import { removeDuplicateBrowsers } from '@packages/data-context/src/sources/BrowserDataSource' import { knownBrowsers } from './known-browsers' import * as darwinHelper from './darwin' import { notDetectedAtPathErr } from './errors' @@ -26,8 +25,16 @@ type HasVersion = Omit, 'version' | 'name'> & { name: string } +function getBrowserKey (browser: T) { + return `${browser.name}-${browser.version}` +} + +function removeDuplicateBrowsers (browsers: FoundBrowser[]) { + return uniqBy(browsers, getBrowserKey) +} + export const getMajorVersion = (version: string): string => { - return version.split('.')[0] + return version.split('.')[0] as string } // Determines if found browser is supported by Cypress. If found to be @@ -199,7 +206,7 @@ export const detectByPath = async ( }) } - const detectBrowserFromKey = (browserKey): Browser | undefined => { + const detectBrowserFromKey = (browserKey: string): Browser | undefined => { return find(goalBrowsers, (goalBrowser) => { return ( goalBrowser.name === browserKey || diff --git a/packages/launcher/lib/index.ts b/packages/launcher/lib/index.ts new file mode 100644 index 00000000000..7c2739000cf --- /dev/null +++ b/packages/launcher/lib/index.ts @@ -0,0 +1,9 @@ +import { detect, detectByPath } from './detect' + +import { launch } from './browsers' + +export { + detect, + detectByPath, + launch, +} diff --git a/packages/launcher/lib/linux/index.ts b/packages/launcher/lib/linux/index.ts index c18e72b75ae..9baddf54a2f 100644 --- a/packages/launcher/lib/linux/index.ts +++ b/packages/launcher/lib/linux/index.ts @@ -115,7 +115,7 @@ async function getLinuxBrowser ( return foundBrowser as FoundBrowser } catch (err) { - return logAndThrowError(err) + return logAndThrowError(err as Error) } } diff --git a/packages/launcher/lib/windows/index.ts b/packages/launcher/lib/windows/index.ts index 0046a20685d..ef1a2c77820 100644 --- a/packages/launcher/lib/windows/index.ts +++ b/packages/launcher/lib/windows/index.ts @@ -56,7 +56,7 @@ function formChromeForTestingAppPath () { ].map(normalize) } -function getFirefoxPaths (editionFolder) { +function getFirefoxPaths (editionFolder: string) { return () => { return (['Program Files', 'Program Files (x86)']) .map((programFiles) => { @@ -134,7 +134,7 @@ function getWindowsBrowser (browser: Browser): Promise { debugVerbose('looking at possible paths... %o', { browser, exePaths }) // shift and try paths 1-by-1 until we find one that works - const tryNextExePath = async () => { + const tryNextExePath = async (): Promise => { const exePath = exePaths.shift() if (!exePath) { @@ -180,12 +180,18 @@ export function doubleEscape (s: string) { return win32.join(...s.split(win32.sep)).replace(/\\/g, '\\\\') } -export function getVersionString (path: string) { +export function getVersionString (path: string): Promise { // on Windows using "--version" seems to always start the full // browser, no matter what one does. try { - return Promise.resolve(winVersionInfo(path).FileVersion) + const fileVersion = winVersionInfo(path).FileVersion + + if (!fileVersion) { + throw new Error(`Failed to get version string for ${path}`) + } + + return Promise.resolve(fileVersion) } catch (err) { return Promise.reject(err) } diff --git a/packages/launcher/package.json b/packages/launcher/package.json index 5d73a2a2b97..b6fb654ae44 100644 --- a/packages/launcher/package.json +++ b/packages/launcher/package.json @@ -2,12 +2,14 @@ "name": "@packages/launcher", "version": "0.0.0-development", "private": true, + "main": "./cjs/index.js", "scripts": { - "build-prod": "tsc --project .", - "check-ts": "tsc --noEmit && yarn -s tslint", - "clean": "rimraf --glob 'lib/*.js' && rimraf --glob 'lib/**/*.js' || true", + "build": "yarn build:cjs && yarn build:esm", + "build-prod": "yarn build", + "build:cjs": "tsc -p tsconfig.cjs.json", + "build:esm": "tsc -p tsconfig.esm.json", + "check-ts": "tsc --noEmit -p tsconfig.cjs.json && tsc --noEmit -p tsconfig.esm.json && yarn -s tslint -p tsconfig.cjs.json", "clean-deps": "rimraf node_modules", - "clean-js": "yarn clean", "lint": "eslint --ext .js,.jsx,.ts,.tsx,.json, .", "size": "t=\"cypress-v0.0.0.tgz\"; yarn pack --filename \"${t}\"; wc -c \"cli/${t}\"; tar tvf \"${t}\"; rm \"${t}\";", "test": "yarn test-unit", @@ -29,15 +31,18 @@ "@packages/data-context": "0.0.0-development", "@packages/ts": "0.0.0-development", "@packages/types": "0.0.0-development", + "@types/plist": "^3.0.5", + "@types/win-version-info": "^3.1.3", "mock-fs": "5.4.0", "typescript": "~5.4.5", "vitest": "^3.2.4" }, "files": [ - "index.js", - "lib" + "cjs/*", + "esm/*" ], - "types": "index.ts", + "types": "./cjs/index.d.ts", + "module": "./esm/index.js", "nx": { "implicitDependencies": [ "@packages/data-context" diff --git a/packages/launcher/tsconfig.cjs.json b/packages/launcher/tsconfig.cjs.json new file mode 100644 index 00000000000..0a511b9a029 --- /dev/null +++ b/packages/launcher/tsconfig.cjs.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./lib", + "outDir": "./cjs", + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "node" + } +} \ No newline at end of file diff --git a/packages/launcher/tsconfig.esm.json b/packages/launcher/tsconfig.esm.json new file mode 100644 index 00000000000..9fd03f42bfd --- /dev/null +++ b/packages/launcher/tsconfig.esm.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./lib", + "outDir": "./esm", + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "node", + "noEmit": true + } +} \ No newline at end of file diff --git a/packages/launcher/tsconfig.json b/packages/launcher/tsconfig.json index 41c9480108b..ca491e190c3 100644 --- a/packages/launcher/tsconfig.json +++ b/packages/launcher/tsconfig.json @@ -1,15 +1,20 @@ { - "extends": "./../ts/tsconfig.json", "include": [ - "**/*.ts", - "./index.ts" - ], - "compilerOptions": { - }, - "files": [ - "./../ts/index.d.ts" + "lib" ], "exclude": [ "test" - ] -} + ], + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "types": [ + "node" + ], + "declaration": true, + } +} \ No newline at end of file diff --git a/packages/server/lib/browsers/utils.ts b/packages/server/lib/browsers/utils.ts index 7673586d0ca..0f0617b6205 100644 --- a/packages/server/lib/browsers/utils.ts +++ b/packages/server/lib/browsers/utils.ts @@ -353,12 +353,12 @@ async function ensureAndGetByNameOrPath (nameOrPath: string, returnAll = false, return browser } catch (err) { - errors.throwErr('BROWSER_NOT_FOUND_BY_PATH', nameOrPath, err.message) + return errors.throwErr('BROWSER_NOT_FOUND_BY_PATH', nameOrPath, err.message) } } // not a path, not found by name - throwBrowserNotFound(nameOrPath, browsers) + return throwBrowserNotFound(nameOrPath, browsers) } const formatBrowsersToOptions = (browsers) => { diff --git a/yarn.lock b/yarn.lock index 930aebcae07..72f9bef7b63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8671,10 +8671,10 @@ resolved "https://registry.yarnpkg.com/@types/picomatch/-/picomatch-2.3.0.tgz#75db5e75a713c5a83d5b76780c3da84a82806003" integrity sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g== -"@types/plist@^3.0.1": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/plist/-/plist-3.0.2.tgz#61b3727bba0f5c462fe333542534a0c3e19ccb01" - integrity sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw== +"@types/plist@^3.0.1", "@types/plist@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/plist/-/plist-3.0.5.tgz#9a0c49c0f9886c8c8696a7904dd703f6284036e0" + integrity sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA== dependencies: "@types/node" "*" xmlbuilder ">=11.0.1" @@ -9053,6 +9053,11 @@ resolved "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz#54541d02d6b1daee5ec01ac0d1b37cecf37db1ae" integrity sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw== +"@types/win-version-info@^3.1.3": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/win-version-info/-/win-version-info-3.1.3.tgz#54e741be330c467f50f066b1a257172609446e01" + integrity sha512-hP+TAbSWhXBpbHhp+AYG/airCfbuMSh8FMA/VtAXpOD6SnAeP+gY5kKXBm5o2ahagNEuWe85URDc12iHphO7dw== + "@types/ws@^7.4.7": version "7.4.7" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" From ac1db0a111488d3335297376aae2b8f34dbd3ec5 Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Tue, 7 Oct 2025 13:51:03 -0400 Subject: [PATCH 16/16] fix typing cast issue --- packages/launcher/lib/linux/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/launcher/lib/linux/index.ts b/packages/launcher/lib/linux/index.ts index 9baddf54a2f..d1651357d86 100644 --- a/packages/launcher/lib/linux/index.ts +++ b/packages/launcher/lib/linux/index.ts @@ -21,7 +21,7 @@ const createTimeoutPromise = (timeout: number = 30000, message: string = `Timed async function isFirefoxSnap (binary: string): Promise { try { - const result = await Promise.race([getFirefoxSnap(binary), createTimeoutPromise(30000, 'Timed out after 30 seconds checking if Firefox is a snap')]) as Promise + const result = await Promise.race([getFirefoxSnap(binary), createTimeoutPromise(30000, 'Timed out after 30 seconds checking if Firefox is a snap')]) as boolean return result } catch (err) {