From 1c7f2c6a802e666a04eb5f6b5a133348e0d87c47 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Tue, 27 Oct 2026 14:55:37 +0400 Subject: [PATCH] CI: patch TestCafe to take the Chrome CDP port from DevToolsActivePort instead of a pre-picked free port --- patches/testcafe@3.7.5.patch | 127 +++++++++++++++++++++++++++++++++++ pnpm-lock.yaml | 25 +++---- pnpm-workspace.yaml | 1 + 3 files changed, 141 insertions(+), 12 deletions(-) create mode 100644 patches/testcafe@3.7.5.patch diff --git a/patches/testcafe@3.7.5.patch b/patches/testcafe@3.7.5.patch new file mode 100644 index 000000000000..85309a498322 --- /dev/null +++ b/patches/testcafe@3.7.5.patch @@ -0,0 +1,127 @@ +diff --git a/lib/browser/provider/built-in/dedicated/chrome/build-chrome-args.js b/lib/browser/provider/built-in/dedicated/chrome/build-chrome-args.js +index 9b9f577..591cb7c 100644 +--- a/lib/browser/provider/built-in/dedicated/chrome/build-chrome-args.js ++++ b/lib/browser/provider/built-in/dedicated/chrome/build-chrome-args.js +@@ -27,8 +27,9 @@ function buildChromeArgs({ config, cdpPort, platformArgs, tempProfileDir, isCont + '--metrics-recording-only', + '--no-first-run', + ]; ++ const hasCdpPort = cdpPort === 0 || !!cdpPort; + let chromeArgs = [] +- .concat(cdpPort ? [`--remote-debugging-port=${cdpPort}`] : [], !config.userProfile ? [`--user-data-dir=${tempProfileDir.path}`] : [], config.headless ? [headlessMode] : [], config.userArgs ? [config.userArgs] : [], ++ .concat(hasCdpPort ? [`--remote-debugging-port=${cdpPort}`] : [], !config.userProfile ? [`--user-data-dir=${tempProfileDir.path}`] : [], config.headless ? [headlessMode] : [], config.userArgs ? [config.userArgs] : [], + // NOTE: we need to prevent new window blocking for multiple windows in Native Automation + isNativeAutomation ? ['--disable-popup-blocking'] : [], platformArgs ? [platformArgs] : [], defaultArgs) + .join(' '); +diff --git a/lib/browser/provider/built-in/dedicated/chrome/index.js b/lib/browser/provider/built-in/dedicated/chrome/index.js +index b12bb9e..fc4e63a 100644 +--- a/lib/browser/provider/built-in/dedicated/chrome/index.js ++++ b/lib/browser/provider/built-in/dedicated/chrome/index.js +@@ -49,9 +49,8 @@ exports.default = Object.assign(Object.assign({}, base_1.default), { getConfig(n + }, + async _startChrome(startOptions, pageUrl) { + if (startOptions.isContainerized) +- await (0, local_chrome_1.startOnDocker)(pageUrl, startOptions); +- else +- await (0, local_chrome_1.start)(pageUrl, startOptions); ++ return (0, local_chrome_1.startOnDocker)(pageUrl, startOptions); ++ return (0, local_chrome_1.start)(pageUrl, startOptions); + }, + async openBrowser(browserId, pageUrl, config, additionalOptions) { + var _a; +@@ -64,7 +63,7 @@ exports.default = Object.assign(Object.assign({}, base_1.default), { getConfig(n + reportWarning: (...args) => this.reportWarning(browserId, ...args), + }; + //NOTE: A not-working tab is opened when the browser start in the docker so we should create a new tab. +- await this._startChrome(Object.assign({ isNativeAutomation: additionalOptions.nativeAutomation }, runtimeInfo), pageUrl); ++ runtimeInfo.cdpPort = await this._startChrome(Object.assign({ isNativeAutomation: additionalOptions.nativeAutomation }, runtimeInfo), pageUrl); + await this.waitForConnectionReady(browserId); + runtimeInfo.viewportSize = await this.runInitScript(browserId, client_functions_1.GET_WINDOW_DIMENSIONS_INFO_SCRIPT); + runtimeInfo.activeWindowId = null; +diff --git a/lib/browser/provider/built-in/dedicated/chrome/local-chrome.js b/lib/browser/provider/built-in/dedicated/chrome/local-chrome.js +index 4f22b00..714a195 100644 +--- a/lib/browser/provider/built-in/dedicated/chrome/local-chrome.js ++++ b/lib/browser/provider/built-in/dedicated/chrome/local-chrome.js +@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) { + }; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.stop = exports.startOnDocker = exports.start = void 0; ++const fs_1 = require("fs"); ++const path_1 = require("path"); + const testcafe_browser_tools_1 = __importDefault(require("testcafe-browser-tools")); + const process_1 = require("../../../../../utils/process"); + const browser_starter_1 = __importDefault(require("../../../utils/browser-starter")); +@@ -14,11 +16,33 @@ const delay_1 = __importDefault(require("../../../../../utils/delay")); + const browserStarter = new browser_starter_1.default(); + const LIST_TABS_TIMEOUT = 10000; + const LIST_TABS_DELAY = 500; ++const DEVTOOLS_ACTIVE_PORT_FILE = 'DevToolsActivePort'; ++const DEVTOOLS_ACTIVE_PORT_TIMEOUT = 30000; ++const DEVTOOLS_ACTIVE_PORT_DELAY = 100; ++async function readDevToolsActivePort(tempProfileDir) { ++ const filePath = path_1.join(tempProfileDir.path, DEVTOOLS_ACTIVE_PORT_FILE); ++ const timer = new timer_1.default(DEVTOOLS_ACTIVE_PORT_TIMEOUT); ++ while (!timer.expired) { ++ try { ++ const port = parseInt(fs_1.readFileSync(filePath, 'utf8'), 10); ++ if (port > 0) ++ return port; ++ } ++ catch (error) { ++ // The file appears once Chrome has bound its DevTools port. ++ } ++ await (0, delay_1.default)(DEVTOOLS_ACTIVE_PORT_DELAY); ++ } ++ throw new Error(`Chrome did not write ${filePath} within ${DEVTOOLS_ACTIVE_PORT_TIMEOUT} ms`); ++} + async function start(pageUrl, { browserName, config, cdpPort, tempProfileDir, isContainerized, isNativeAutomation }) { + const chromeInfo = await testcafe_browser_tools_1.default.getBrowserInfo(config.path || browserName); + const chromeOpenParameters = Object.assign({}, chromeInfo); + chromeOpenParameters.cmd = (0, build_chrome_args_1.buildChromeArgs)({ config, cdpPort, platformArgs: chromeOpenParameters.cmd, tempProfileDir, isContainerized, isNativeAutomation, browserName }); + await browserStarter.startBrowser(chromeOpenParameters, pageUrl); ++ if (cdpPort === 0 && tempProfileDir) ++ return readDevToolsActivePort(tempProfileDir); ++ return cdpPort; + } + exports.start = start; + async function tryListTabs(cdpPort) { +@@ -30,7 +54,7 @@ async function tryListTabs(cdpPort) { + } + } + async function startOnDocker(pageUrl, { browserName, config, cdpPort, tempProfileDir, isContainerized }) { +- await start('', { browserName, config, cdpPort, tempProfileDir, isContainerized }); ++ cdpPort = await start('', { browserName, config, cdpPort, tempProfileDir, isContainerized }); + let { tabs, error } = await tryListTabs(cdpPort); + const timer = new timer_1.default(LIST_TABS_TIMEOUT); + //NOTE: We should repeat getting 'List' after a while because we can get an error if the browser isn't ready. +@@ -44,6 +68,7 @@ async function startOnDocker(pageUrl, { browserName, config, cdpPort, tempProfil + const { Target } = await (0, chrome_remote_interface_1.default)({ target, port: cdpPort }); + await Target.createTarget({ url: pageUrl }); + await chrome_remote_interface_1.default.Close({ id: target.id, port: cdpPort }); ++ return cdpPort; + } + exports.startOnDocker = startOnDocker; + async function stop({ browserId }) { +diff --git a/lib/browser/provider/built-in/dedicated/chrome/runtime-info.js b/lib/browser/provider/built-in/dedicated/chrome/runtime-info.js +index 0de3fac..9f9852e 100644 +--- a/lib/browser/provider/built-in/dedicated/chrome/runtime-info.js ++++ b/lib/browser/provider/built-in/dedicated/chrome/runtime-info.js +@@ -3,7 +3,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; + }; + Object.defineProperty(exports, "__esModule", { value: true }); +-const endpoint_utils_1 = require("../../../../../utils/endpoint-utils"); + const create_temp_profile_1 = __importDefault(require("./create-temp-profile")); + const is_docker_1 = __importDefault(require("is-docker")); + const is_podman_1 = __importDefault(require("is-podman")); +@@ -21,8 +20,9 @@ class ChromeRuntimeInfo { + const runtimeInfo = new this(config); + if (!runtimeInfo.config.userProfile) + runtimeInfo.tempProfileDir = await runtimeInfo.createTempProfile(proxyHostName, disableMultipleWindows); ++ // Port 0 lets Chrome choose a free port; `start` reads the real one from DevToolsActivePort. + if (!runtimeInfo.cdpPort && !runtimeInfo.config.userProfile) +- runtimeInfo.cdpPort = await (0, endpoint_utils_1.getFreePort)(); ++ runtimeInfo.cdpPort = 0; + return runtimeInfo; + } + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0f19ca6f284..9f1b07676461 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -223,6 +223,7 @@ overrides: patchedDependencies: testcafe-hammerhead@31.7.8: 8655c07786177d3b611a05abf6b0ea87d32796eb2601f92a800ef041095f264c + testcafe@3.7.5: 1f6217b64591e208454404583c8f1f020156d76fd9f7fc131827d5f22c9fc921 importers: @@ -610,7 +611,7 @@ importers: version: 5.10.0(eslint@9.39.4(jiti@2.6.1)(supports-color@7.2.0)) '@testcafe-community/axe': specifier: 3.5.0 - version: 3.5.0(axe-core@4.13.0)(testcafe@3.7.5) + version: 3.5.0(axe-core@4.13.0)(testcafe@3.7.5(patch_hash=1f6217b64591e208454404583c8f1f020156d76fd9f7fc131827d5f22c9fc921)) '@types/eslint': specifier: 8.56.12 version: 8.56.12 @@ -658,7 +659,7 @@ importers: version: 7.0.3 devextreme-screenshot-comparer: specifier: 2.0.17 - version: 2.0.17(testcafe@3.7.5) + version: 2.0.17(testcafe@3.7.5(patch_hash=1f6217b64591e208454404583c8f1f020156d76fd9f7fc131827d5f22c9fc921)) eslint: specifier: 'catalog:' version: 9.39.4(jiti@2.6.1)(supports-color@7.2.0) @@ -748,7 +749,7 @@ importers: version: 38.0.0(stylelint@16.22.0(typescript@6.0.3)) testcafe: specifier: 'catalog:' - version: 3.7.5(supports-color@7.2.0) + version: 3.7.5(patch_hash=1f6217b64591e208454404583c8f1f020156d76fd9f7fc131827d5f22c9fc921)(supports-color@7.2.0) testcafe-reporter-spec-time: specifier: 4.0.0 version: 4.0.0 @@ -1138,7 +1139,7 @@ importers: version: 5.10.0(eslint@9.39.4(jiti@2.6.1)) '@testcafe-community/axe': specifier: 3.5.0 - version: 3.5.0(axe-core@4.13.0)(testcafe@3.7.5) + version: 3.5.0(axe-core@4.13.0)(testcafe@3.7.5(patch_hash=1f6217b64591e208454404583c8f1f020156d76fd9f7fc131827d5f22c9fc921)) '@types/jquery': specifier: 'catalog:' version: 4.0.1 @@ -1156,7 +1157,7 @@ importers: version: link:../../packages/devextreme/artifacts/npm/devextreme devextreme-screenshot-comparer: specifier: 2.0.17 - version: 2.0.17(testcafe@3.7.5) + version: 2.0.17(testcafe@3.7.5(patch_hash=1f6217b64591e208454404583c8f1f020156d76fd9f7fc131827d5f22c9fc921)) devextreme-testcafe-models: specifier: workspace:* version: link:../../packages/testcafe-models @@ -1192,7 +1193,7 @@ importers: version: 0.12.1 testcafe: specifier: 'catalog:' - version: 3.7.5(supports-color@7.2.0) + version: 3.7.5(patch_hash=1f6217b64591e208454404583c8f1f020156d76fd9f7fc131827d5f22c9fc921)(supports-color@7.2.0) testcafe-reporter-spec-time: specifier: 4.0.0 version: 4.0.0 @@ -2212,7 +2213,7 @@ importers: version: link:../devextreme/artifacts/npm/devextreme testcafe: specifier: 'catalog:' - version: 3.7.5(supports-color@7.2.0) + version: 3.7.5(patch_hash=1f6217b64591e208454404583c8f1f020156d76fd9f7fc131827d5f22c9fc921)(supports-color@7.2.0) tools/flaky-test-reporter: dependencies: @@ -24574,11 +24575,11 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@testcafe-community/axe@3.5.0(axe-core@4.13.0)(testcafe@3.7.5)': + '@testcafe-community/axe@3.5.0(axe-core@4.13.0)(testcafe@3.7.5(patch_hash=1f6217b64591e208454404583c8f1f020156d76fd9f7fc131827d5f22c9fc921))': dependencies: axe-core: 4.13.0 chalk: 2.4.2 - testcafe: 3.7.5(supports-color@7.2.0) + testcafe: 3.7.5(patch_hash=1f6217b64591e208454404583c8f1f020156d76fd9f7fc131827d5f22c9fc921)(supports-color@7.2.0) '@testing-library/dom@10.4.1': dependencies: @@ -28130,12 +28131,12 @@ snapshots: transitivePeerDependencies: - chokidar - devextreme-screenshot-comparer@2.0.17(testcafe@3.7.5): + devextreme-screenshot-comparer@2.0.17(testcafe@3.7.5(patch_hash=1f6217b64591e208454404583c8f1f020156d76fd9f7fc131827d5f22c9fc921)): dependencies: color-diff: 1.3.0 looks-same: 7.3.0 pngjs: 6.0.0 - testcafe: 3.7.5(supports-color@7.2.0) + testcafe: 3.7.5(patch_hash=1f6217b64591e208454404583c8f1f020156d76fd9f7fc131827d5f22c9fc921)(supports-color@7.2.0) tslib: 2.8.1 device-specs@1.0.1: {} @@ -36295,7 +36296,7 @@ snapshots: testcafe-selector-generator@0.1.0: {} - testcafe@3.7.5(supports-color@7.2.0): + testcafe@3.7.5(patch_hash=1f6217b64591e208454404583c8f1f020156d76fd9f7fc131827d5f22c9fc921)(supports-color@7.2.0): dependencies: '@babel/core': 7.29.7 '@babel/plugin-proposal-decorators': 7.29.7(@babel/core@7.29.7) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 657998218cd5..d5bcd5ce8952 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -145,3 +145,4 @@ allowBuilds: patchedDependencies: testcafe-hammerhead@31.7.8: patches/testcafe-hammerhead@31.7.8.patch + testcafe@3.7.5: patches/testcafe@3.7.5.patch