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>
41 changes: 41 additions & 0 deletions test/functional/fixtures/regression/gh-8391/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const path = require('path');
const { expect } = require('chai');
const createTestCafe = require('../../../../../lib');
const config = require('../../../config');

const needSkip = !config.hasBrowser('firefox');

function getFirefoxBrowserName () {
const firefoxSettings = config.currentEnvironment.browsers.find(browser => browser.alias.includes('firefox'));

return firefoxSettings ? firefoxSettings.browserName : 'firefox';
}

async function runFirefoxProxyModeCryptoTest () {
// NOTE: host is intentionally unset to exercise the hostname calculation path.
const testCafe = await createTestCafe({ port1: 1335, port2: 1336 });

try {
const failedCount = await testCafe
.createRunner()
.src(path.join(__dirname, 'testcafe-fixtures/index.js'))
.browsers(getFirefoxBrowserName())
.run({
disableNativeAutomation: true,
pageLoadTimeout: 45000,
selectorTimeout: 5000,
testExecutionTimeout: 120000,
});

expect(failedCount).eql(0);
}
finally {
await testCafe.close();
}
}

(needSkip ? describe.skip : describe)('[Regression](GH-8391)', function () {
it('Should keep WebCrypto available in Firefox proxy mode when all browsers are local', function () {
return runFirefoxProxyModeCryptoTest();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ClientFunction } from 'testcafe';

fixture('GH-8391 - Firefox proxy mode should preserve WebCrypto')
.page`http://localhost:3000/fixtures/regression/gh-8391/pages/index.html`;

const getCryptoState = ClientFunction(() => ({
hasSubtle: !!(window.crypto && window.crypto.subtle),
isSecureContext: window.isSecureContext,
}));

test('Should expose window.crypto.subtle in Firefox proxy mode', async t => {
const cryptoState = await getCryptoState();

await t.expect(cryptoState.isSecureContext).ok('Expected secure context in proxy mode');
await t.expect(cryptoState.hasSubtle).ok('Expected WebCrypto API to be available in proxy mode');
});
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