Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion src/configuration/testcafe-configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,10 +330,12 @@ export default class TestCafeConfiguration extends Configuration {
this.mergeOptions({ hostname });
}

public async calculateHostname ({ nativeAutomation } = { nativeAutomation: false }): Promise<void> {
public async calculateHostname ({ nativeAutomation, allBrowsersLocal } = { nativeAutomation: false, allBrowsersLocal: false }): Promise<void> {
await this.ensureHostname(async hostname => {
if (nativeAutomation)
hostname = LOCALHOST_NAMES.LOCALHOST;
else if (!hostname && allBrowsersLocal)
hostname = LOCALHOST_NAMES.LOCALHOST;
else
hostname = await getValidHostname(hostname);

Expand Down
17 changes: 13 additions & 4 deletions src/runner/bootstrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,16 @@ export default class Bootstrapper {
};
}

private async _setupProxy (): Promise<void> {
private async _setupProxy (browserInfo: BrowserInfoSource[]): Promise<void> {
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);
}
Expand Down Expand Up @@ -209,7 +214,7 @@ export default class Bootstrapper {

this._validateUserProfileOptionInNativeAutomation(automated);

await this._setupProxy();
await this._setupProxy(browserInfo);

let browserConnections = this._createAutomatedConnections(automated);

Expand Down Expand Up @@ -349,13 +354,17 @@ export default class Bootstrapper {
return testedApp;
}

private async _canUseParallelBootstrapping (browserInfo: BrowserInfoSource[]): Promise<boolean> {
private async _isAllBrowsersLocal (browserInfo: BrowserInfoSource[]): Promise<boolean> {
const isLocalPromises = browserInfo.map(browser => browser.provider.isLocalBrowser(void 0, Bootstrapper._getBrowserName(browser)));
const isLocalBrowsers = await Promise.all(isLocalPromises);

return isLocalBrowsers.every(result => result);
}
Comment on lines +357 to 362
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_isAllBrowsersLocal calls browser.provider.isLocalBrowser(void 0, ...) for every BrowserInfoSource. When the entry is a BrowserConnection, an id is available (browser.id) and some providers rely on it to determine locality; passing undefined risks misclassifying remote connections as local, which can incorrectly force hostname to localhost in proxy mode. Pass the connection id for BrowserConnection instances (and keep undefined for plain BrowserInfo).

Copilot uses AI. Check for mistakes.

private async _canUseParallelBootstrapping (browserInfo: BrowserInfoSource[]): Promise<boolean> {
return this._isAllBrowsersLocal(browserInfo);
}

private async _bootstrapSequence (browserInfo: BrowserInfoSource[], id: string): Promise<BasicRuntimeResources> {
const tests = await this._getTests(id);
const testedApp = await this._startTestedApp();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<script>
document.getElementById('nested-second-page-btn').addEventListener('click', function () {
window.nestedSecondPageBtnClickCount = (window.nestedSecondPageBtnClickCount || 0) + 1;
this.textContent = 'clicked';
});
</script>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<script>
document.getElementById('second-page-btn').addEventListener('click', function () {
window.secondPageBtnClickCount = (window.secondPageBtnClickCount || 0) + 1;
this.textContent = 'clicked';
});
</script>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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()
Expand All @@ -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 => {
Expand Down
10 changes: 10 additions & 0 deletions test/functional/fixtures/regression/gh-8391/pages/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>GH-8391</title>
</head>
<body>
<div id="status">ok</div>
</body>
</html>
5 changes: 5 additions & 0 deletions test/functional/fixtures/regression/gh-8391/test.js
Original file line number Diff line number Diff line change
@@ -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' });
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This regression test is intended to verify Firefox behavior specifically in proxy mode (--disable-native-automation), but it doesn’t enforce that mode. Since the functional test harness can run with NATIVE_AUTOMATION=true, this test may execute in native automation and fail to cover the reported scenario. Please skip/guard this test when config.nativeAutomation is true (or explicitly run with disableNativeAutomation: true in this test).

Suggested change
return runTests('testcafe-fixtures/index.js', 'Should run a simple assertion in Firefox proxy mode', { only: 'firefox' });
return runTests('testcafe-fixtures/index.js', 'Should run a simple assertion in Firefox proxy mode', { only: 'firefox', disableNativeAutomation: true });

Copilot uses AI. Check for mistakes.
});
});
Original file line number Diff line number Diff line change
@@ -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');
});
34 changes: 34 additions & 0 deletions test/server/bootstrapper-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
});
});
});
31 changes: 27 additions & 4 deletions test/server/configuration-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`);
Expand Down Expand Up @@ -63,7 +72,7 @@ describe('TestCafeConfiguration', function () {
});

afterEach(async () => {
await del(testCafeConfiguration.defaultPaths);
await removeDefaultConfigFiles(testCafeConfiguration);

consoleWrapper.unwrap();
consoleWrapper.messages.clear();
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -505,6 +514,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');
});
});
});

Expand Down Expand Up @@ -654,7 +677,7 @@ describe('TestCafeConfiguration', function () {
});

after(async () => {
await del(configuration.defaultPaths);
await removeDefaultConfigFiles(configuration);
});

it('Should success create configuration with incorrect browser value', () => {
Expand Down Expand Up @@ -922,7 +945,7 @@ describe('TypeScriptConfiguration', function () {
let configuration;

afterEach(async () => {
await del(configuration.defaultPaths);
await removeDefaultConfigFiles(configuration);
});

it('Custom config path is used', () => {
Expand Down