From c4b6192449bfcdb049a93aefb658de78ea3bc0c2 Mon Sep 17 00:00:00 2001 From: Christopher Robinson Date: Thu, 19 Feb 2026 11:02:13 -0600 Subject: [PATCH 1/5] Use localhost hostname for local browsers in proxy mode When running tests in local browsers with native automation disabled, TestCafe previously attempted to resolve a network-accessible hostname. This process can be slow and may fail in environments with restricted or no network access. This change optimizes this behavior by defaulting the hostname to 'localhost' when all browsers are local and a specific hostname has not been configured. This improves test startup time and enhances reliability for local-only test runs. --- src/configuration/testcafe-configuration.ts | 4 ++- src/runner/bootstrapper.ts | 17 ++++++++--- test/server/bootstrapper-test.js | 34 +++++++++++++++++++++ test/server/configuration-test.js | 14 +++++++++ 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/configuration/testcafe-configuration.ts b/src/configuration/testcafe-configuration.ts index 3c8fbdf7688..83b942ee970 100644 --- a/src/configuration/testcafe-configuration.ts +++ b/src/configuration/testcafe-configuration.ts @@ -330,10 +330,12 @@ export default class TestCafeConfiguration extends Configuration { this.mergeOptions({ hostname }); } - public async calculateHostname ({ nativeAutomation } = { nativeAutomation: false }): Promise { + public async calculateHostname ({ nativeAutomation, allBrowsersLocal } = { nativeAutomation: false, allBrowsersLocal: false }): Promise { await this.ensureHostname(async hostname => { if (nativeAutomation) hostname = LOCALHOST_NAMES.LOCALHOST; + else if (!hostname && allBrowsersLocal) + hostname = LOCALHOST_NAMES.LOCALHOST; else hostname = await getValidHostname(hostname); diff --git a/src/runner/bootstrapper.ts b/src/runner/bootstrapper.ts index 9291b9a445e..484f13ac557 100644 --- a/src/runner/bootstrapper.ts +++ b/src/runner/bootstrapper.ts @@ -171,11 +171,16 @@ export default class Bootstrapper { }; } - private async _setupProxy (): Promise { + private async _setupProxy (browserInfo: BrowserInfoSource[]): Promise { if (this.browserConnectionGateway.status === BrowserConnectionGatewayStatus.initialized) return; - await this.configuration.calculateHostname({ nativeAutomation: !this.configuration.getOption(OPTION_NAMES.disableNativeAutomation) }); + const allBrowsersLocal = await this._isAllBrowsersLocal(browserInfo); + + await this.configuration.calculateHostname({ + nativeAutomation: !this.configuration.getOption(OPTION_NAMES.disableNativeAutomation), + allBrowsersLocal, + }); this.browserConnectionGateway.initialize(this.configuration.startOptions); } @@ -209,7 +214,7 @@ export default class Bootstrapper { this._validateUserProfileOptionInNativeAutomation(automated); - await this._setupProxy(); + await this._setupProxy(browserInfo); let browserConnections = this._createAutomatedConnections(automated); @@ -349,13 +354,17 @@ export default class Bootstrapper { return testedApp; } - private async _canUseParallelBootstrapping (browserInfo: BrowserInfoSource[]): Promise { + private async _isAllBrowsersLocal (browserInfo: BrowserInfoSource[]): Promise { const isLocalPromises = browserInfo.map(browser => browser.provider.isLocalBrowser(void 0, Bootstrapper._getBrowserName(browser))); const isLocalBrowsers = await Promise.all(isLocalPromises); return isLocalBrowsers.every(result => result); } + private async _canUseParallelBootstrapping (browserInfo: BrowserInfoSource[]): Promise { + return this._isAllBrowsersLocal(browserInfo); + } + private async _bootstrapSequence (browserInfo: BrowserInfoSource[], id: string): Promise { const tests = await this._getTests(id); const testedApp = await this._startTestedApp(); diff --git a/test/server/bootstrapper-test.js b/test/server/bootstrapper-test.js index 81e2ea5b38b..0634667e6d5 100644 --- a/test/server/bootstrapper-test.js +++ b/test/server/bootstrapper-test.js @@ -189,5 +189,39 @@ describe('Bootstrapper', () => { 'the "userProfile" suffix from the following browser aliases: "chrome, edge".'); } }); + + it('Should use localhost hostname strategy for local browsers in proxy mode', async () => { + let calculateHostnameOptions = null; + + const originalGetOption = bootstrapper.configuration.getOption; + const originalCalculateHostname = bootstrapper.configuration.calculateHostname; + + try { + bootstrapper.configuration.getOption = optionName => { + if (optionName === 'disableNativeAutomation') + return true; + + return originalGetOption(optionName); + }; + + bootstrapper.configuration.calculateHostname = options => { + calculateHostnameOptions = options; + }; + + await bootstrapper._setupProxy([{ + browserName: 'firefox', + provider: createBrowserProviderMock({ local: true }), + }]); + + expect(calculateHostnameOptions).eql({ + nativeAutomation: false, + allBrowsersLocal: true, + }); + } + finally { + bootstrapper.configuration.getOption = originalGetOption; + bootstrapper.configuration.calculateHostname = originalCalculateHostname; + } + }); }); }); diff --git a/test/server/configuration-test.js b/test/server/configuration-test.js index 6f41c35b30e..76a415a9b84 100644 --- a/test/server/configuration-test.js +++ b/test/server/configuration-test.js @@ -505,6 +505,20 @@ describe('TestCafeConfiguration', function () { expect(configuration.getOption('hostname')).eql('123.456.789'); }); + + it('Native automation is disabled/all browsers are local/hostname is unset', async () => { + await configuration.init(); + await configuration.calculateHostname({ nativeAutomation: false, allBrowsersLocal: true }); + + expect(configuration.getOption('hostname')).eql('localhost'); + }); + + it('Native automation is disabled/all browsers are local/hostname is set', async () => { + await configuration.init({ hostname: '123.456.789' }); + await configuration.calculateHostname({ nativeAutomation: false, allBrowsersLocal: true }); + + expect(configuration.getOption('hostname')).eql('123.456.789'); + }); }); }); From fae1ba839df81ca7c49e348e2bd6b1b53b00389c Mon Sep 17 00:00:00 2001 From: Christopher Robinson Date: Thu, 19 Feb 2026 11:57:12 -0600 Subject: [PATCH 2/5] fix: bump testcafe-hammerhead to 31.7.7 for Firefox proxy init --- package-lock.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 38765b020ed..36a3454b958 100644 --- a/package-lock.json +++ b/package-lock.json @@ -235,7 +235,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -3226,7 +3225,6 @@ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -3622,7 +3620,6 @@ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4823,7 +4820,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001733", "electron-to-chromium": "^1.5.199", @@ -5132,7 +5128,6 @@ "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", "license": "MIT", - "peer": true, "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.2", @@ -6764,7 +6759,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.3", @@ -15392,7 +15386,6 @@ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -17643,7 +17636,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From 188a229fbdac1aaf87662fbfaf39ff40f0afbf7a Mon Sep 17 00:00:00 2001 From: Christopher Robinson Date: Thu, 19 Feb 2026 12:22:45 -0600 Subject: [PATCH 3/5] test: add regression for Firefox proxy-mode init (GH-8391) --- .../fixtures/regression/gh-8391/pages/index.html | 10 ++++++++++ test/functional/fixtures/regression/gh-8391/test.js | 5 +++++ .../regression/gh-8391/testcafe-fixtures/index.js | 8 ++++++++ 3 files changed, 23 insertions(+) create mode 100644 test/functional/fixtures/regression/gh-8391/pages/index.html create mode 100644 test/functional/fixtures/regression/gh-8391/test.js create mode 100644 test/functional/fixtures/regression/gh-8391/testcafe-fixtures/index.js diff --git a/test/functional/fixtures/regression/gh-8391/pages/index.html b/test/functional/fixtures/regression/gh-8391/pages/index.html new file mode 100644 index 00000000000..acade237284 --- /dev/null +++ b/test/functional/fixtures/regression/gh-8391/pages/index.html @@ -0,0 +1,10 @@ + + + + + GH-8391 + + +
ok
+ + diff --git a/test/functional/fixtures/regression/gh-8391/test.js b/test/functional/fixtures/regression/gh-8391/test.js new file mode 100644 index 00000000000..90d664633e6 --- /dev/null +++ b/test/functional/fixtures/regression/gh-8391/test.js @@ -0,0 +1,5 @@ +describe('[Regression](GH-8391)', function () { + it('Should not fail Firefox proxy-mode initialization', function () { + return runTests('testcafe-fixtures/index.js', 'Should run a simple assertion in Firefox proxy mode', { only: 'firefox' }); + }); +}); diff --git a/test/functional/fixtures/regression/gh-8391/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-8391/testcafe-fixtures/index.js new file mode 100644 index 00000000000..8a38cb40371 --- /dev/null +++ b/test/functional/fixtures/regression/gh-8391/testcafe-fixtures/index.js @@ -0,0 +1,8 @@ +import { Selector } from 'testcafe'; + +fixture('GH-8391 - Firefox proxy mode should initialize scripts') + .page`http://localhost:3000/fixtures/regression/gh-8391/pages/index.html`; + +test('Should run a simple assertion in Firefox proxy mode', async t => { + await t.expect(Selector('#status').innerText).eql('ok'); +}); From 4bf975ec80e5837662c6b8dacd6b478344ea5cbd Mon Sep 17 00:00:00 2001 From: Christopher Robinson Date: Thu, 26 Feb 2026 13:28:05 -0600 Subject: [PATCH 4/5] test: stabilize configuration default-path cleanup on Windows --- test/server/configuration-test.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/test/server/configuration-test.js b/test/server/configuration-test.js index 76a415a9b84..5dced465ab9 100644 --- a/test/server/configuration-test.js +++ b/test/server/configuration-test.js @@ -31,6 +31,15 @@ const createJSONConfig = (filePath, options) => { fs.writeFileSync(filePath, JSON.stringify(options)); }; +const normalizePathForDel = filePath => filePath.replace(/\\/g, '/'); + +const removeDefaultConfigFiles = async configuration => { + if (!configuration) + return; + + await del(configuration.defaultPaths.map(normalizePathForDel), { force: true }); +}; + const createJsConfig = (filePath, options) => { options = options || {}; fs.writeFileSync(filePath, `module.exports = ${JSON.stringify(options)}`); @@ -63,7 +72,7 @@ describe('TestCafeConfiguration', function () { }); afterEach(async () => { - await del(testCafeConfiguration.defaultPaths); + await removeDefaultConfigFiles(testCafeConfiguration); consoleWrapper.unwrap(); consoleWrapper.messages.clear(); @@ -475,7 +484,7 @@ describe('TestCafeConfiguration', function () { beforeEach(async () => { configuration = new TestCafeConfiguration(); - await del(configuration.defaultPaths); + await removeDefaultConfigFiles(configuration); }); it('Native automation is enabled/hostname is unset', async () => { @@ -668,7 +677,7 @@ describe('TestCafeConfiguration', function () { }); after(async () => { - await del(configuration.defaultPaths); + await removeDefaultConfigFiles(configuration); }); it('Should success create configuration with incorrect browser value', () => { @@ -936,7 +945,7 @@ describe('TypeScriptConfiguration', function () { let configuration; afterEach(async () => { - await del(configuration.defaultPaths); + await removeDefaultConfigFiles(configuration); }); it('Custom config path is used', () => { From 8cce2461c0d85bc70d96cc388d9ba8328e1a2844 Mon Sep 17 00:00:00 2001 From: Christopher Robinson Date: Thu, 26 Feb 2026 13:28:26 -0600 Subject: [PATCH 5/5] test: stabilize Firefox iframe-switching redirect/unavailable cases --- .../iframe-switching/pages/nested-second.html | 1 + .../iframe-switching/pages/second.html | 1 + .../iframe-switching-test.js | 29 +++++++------------ 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/test/functional/fixtures/api/es-next/iframe-switching/pages/nested-second.html b/test/functional/fixtures/api/es-next/iframe-switching/pages/nested-second.html index 6c6cfe7d11b..76f18a0300d 100644 --- a/test/functional/fixtures/api/es-next/iframe-switching/pages/nested-second.html +++ b/test/functional/fixtures/api/es-next/iframe-switching/pages/nested-second.html @@ -9,6 +9,7 @@ diff --git a/test/functional/fixtures/api/es-next/iframe-switching/pages/second.html b/test/functional/fixtures/api/es-next/iframe-switching/pages/second.html index bf316045d96..b3ed78196aa 100644 --- a/test/functional/fixtures/api/es-next/iframe-switching/pages/second.html +++ b/test/functional/fixtures/api/es-next/iframe-switching/pages/second.html @@ -9,6 +9,7 @@ diff --git a/test/functional/fixtures/api/es-next/iframe-switching/testcafe-fixtures/iframe-switching-test.js b/test/functional/fixtures/api/es-next/iframe-switching/testcafe-fixtures/iframe-switching-test.js index 1a5b81da777..9c738b6f89f 100644 --- a/test/functional/fixtures/api/es-next/iframe-switching/testcafe-fixtures/iframe-switching-test.js +++ b/test/functional/fixtures/api/es-next/iframe-switching/testcafe-fixtures/iframe-switching-test.js @@ -132,32 +132,28 @@ test('Remove an iframe during execution', async t => { test('Click in a removed iframe', async t => { await t .switchToIframe('#iframe') + .wait(500) .click('#remove-from-parent-btn') + .wait(500) .click('#btn'); }); test('Click in an iframe with redirect', async t => { - const getSecondPageBtnClickCount = ClientFunction(() => window.secondPageBtnClickCount); - const getNestedSecondPageBtnClickCount = ClientFunction(() => window.nestedSecondPageBtnClickCount); - await t .switchToIframe('#iframe') .switchToIframe('#iframe') .click('#link') - .click('#nested-second-page-btn'); - - const nestedSecondPageBtnClickCount = await getNestedSecondPageBtnClickCount(); + .wait(500) + .click('#nested-second-page-btn') + .expect(Selector('#nested-second-page-btn').innerText).eql('clicked'); await t .switchToMainWindow() .switchToIframe('#iframe') .click('#link') - .click('#second-page-btn'); - - const secondPageBtnClickCount = await getSecondPageBtnClickCount(); - - expect(nestedSecondPageBtnClickCount).eql(1); - expect(secondPageBtnClickCount).eql(1); + .wait(500) + .click('#second-page-btn') + .expect(Selector('#second-page-btn').innerText).eql('clicked'); }); test('Reload the main page from an iframe', async t => { @@ -208,14 +204,12 @@ test('Click in an iframe without src', async t => { }); test('Click in a cross-domain iframe with redirect', async t => { - const getSecondPageBtnClickCount = ClientFunction(() => window.secondPageBtnClickCount); - await t .switchToIframe('#cross-domain-iframe') .click('#link') - .click('#second-page-btn'); - - const secondPageBtnClickCount = await getSecondPageBtnClickCount(); + .wait(500) + .click('#second-page-btn') + .expect(Selector('#second-page-btn').innerText).eql('clicked'); await t .switchToMainWindow() @@ -224,7 +218,6 @@ test('Click in a cross-domain iframe with redirect', async t => { const btnClickCount = await getBtnClickCount(); expect(btnClickCount).eql(1); - expect(secondPageBtnClickCount).eql(1); }); test("Click in a iframe that's loading too slowly", async t => {