Repository: DevExpress/testcafe
Pull request #8080 (2023-11-14 12:51:32)

feat: experimental multiple windows in native automation

AlexKamaev: > Decided to check [here](https://github.com/DevExpress/testcafe/pull/8080/files#diff-0e08fb5580229c71846f72feac202a23dbead04d5d816fed8e95b691b4e5baedR11) if multiple mode enabled

I've checked it. In general, we cannot pass the `experimentalMultipleWindows` option into the `openBrowser` function, since browser can be opened before the `experimentalMultipleWindows` options is set (as in our tests).

So, I'll merge it as is. If it would be neccessary, I'll check the possible fixes for this in near future.

Patch:
diff --git a/Gulpfile.js b/Gulpfile.js
index 54096a5d8..7302f5881 100644
--- a/Gulpfile.js
+++ b/Gulpfile.js
@@ -472,6 +472,12 @@ gulp.step('test-functional-local-multiple-windows-run', () => {
 
 gulp.task('test-functional-local-multiple-windows', gulp.series('prepare-tests', 'test-functional-local-multiple-windows-run'));
 
+gulp.step('test-functional-local-multiple-windows-na-run', () => {
+    return testFunctional(MULTIPLE_WINDOWS_TESTS_GLOB, functionalTestConfig.testingEnvironmentNames.localChrome, { nativeAutomation: true });
+});
+
+gulp.task('test-functional-local-multiple-windows-na', gulp.series('prepare-tests', 'test-functional-local-multiple-windows-na-run'));
+
 gulp.step('test-functional-local-chrome-firefox-headed-run', () => {
     return testFunctional(HEADED_CHROME_FIREFOX_TESTS_GLOB, functionalTestConfig.testingEnvironmentNames.localBrowsersChromeFirefox);
 });
diff --git a/package-lock.json b/package-lock.json
index e7f968bbb..6c77a34a0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,7 +20,7 @@
         "@babel/plugin-transform-async-to-generator": "^7.22.5",
         "@babel/plugin-transform-exponentiation-operator": "^7.22.5",
         "@babel/plugin-transform-for-of": "^7.22.15",
-        "@babel/plugin-transform-runtime": "^7.23.2",
+        "@babel/plugin-transform-runtime": "7.23.3",
         "@babel/preset-env": "^7.23.2",
         "@babel/preset-flow": "^7.22.15",
         "@babel/preset-react": "^7.22.15",
@@ -90,7 +90,7 @@
         "source-map-support": "^0.5.16",
         "strip-bom": "^2.0.0",
         "testcafe-browser-tools": "2.0.26",
-        "testcafe-hammerhead": "31.6.4",
+        "testcafe-hammerhead": "31.7.0",
         "testcafe-legacy-api": "5.1.6",
         "testcafe-reporter-json": "^2.1.0",
         "testcafe-reporter-list": "^2.2.0",
@@ -7098,9 +7098,9 @@
       }
     },
     "node_modules/esotope-hammerhead": {
-      "version": "0.6.6",
-      "resolved": "https://registry.npmjs.org/esotope-hammerhead/-/esotope-hammerhead-0.6.6.tgz",
-      "integrity": "sha512-pHTz6hKpCotF/VDMK6UlMo6A1Y8nQsYKuZPtNKWJSKWgvJvB35m91wqeSVz4TlkxTHyIfoeIwMiffdv7Z0JCRA==",
+      "version": "0.6.7",
+      "resolved": "https://registry.npmjs.org/esotope-hammerhead/-/esotope-hammerhead-0.6.7.tgz",
+      "integrity": "sha512-nejJRHWvdoymlWnAXJGm8qfaK1hQ7NiMnTQzMSHPUzBrY7Nogu8O0Q6/HcY8AvY58pkkq2loto7oDDZ0zXYQcg==",
       "dependencies": {
         "@types/estree": "0.0.46"
       }
@@ -17567,9 +17567,10 @@
       }
     },
     "node_modules/testcafe-hammerhead": {
-      "version": "31.6.4",
-      "resolved": "https://registry.npmjs.org/testcafe-hammerhead/-/testcafe-hammerhead-31.6.4.tgz",
-      "integrity": "sha512-Pkr2ybw58KNUgArpj7GuV41dzHGgcFm3/uauWTKMPzhZt5k7Etat98lR+J1IPUb64FhlOxC4aADKR4VocIyqxw==",
+      "version": "31.7.0",
+      "resolved": "https://github.com/AlexKamaev/issues/raw/master/testcafe-hammerhead-31.7.0.tgz",
+      "integrity": "sha512-ovVj9/wPlStItl/ACzHwQRZxLQBj7kTVN+wbKGs9f+2gEEcCwhWuAVoH33Sj68OCi7RuTcZGZ1BP6WaEwJ1hjA==",
+      "license": "MIT",
       "dependencies": {
         "@adobe/css-tools": "^4.3.0-rc.1",
         "@electron/asar": "^3.2.3",
@@ -17577,7 +17578,7 @@
         "bowser": "1.6.0",
         "crypto-md5": "^1.0.0",
         "debug": "4.3.1",
-        "esotope-hammerhead": "0.6.6",
+        "esotope-hammerhead": "0.6.7",
         "http-cache-semantics": "^4.1.0",
         "httpntlm": "^1.8.10",
         "iconv-lite": "0.5.1",
diff --git a/package.json b/package.json
index 9cef782a3..4b3b5c9bc 100644
--- a/package.json
+++ b/package.json
@@ -142,7 +142,7 @@
     "source-map-support": "^0.5.16",
     "strip-bom": "^2.0.0",
     "testcafe-browser-tools": "2.0.26",
-    "testcafe-hammerhead": "31.6.4",
+    "testcafe-hammerhead": "31.7.0",
     "testcafe-legacy-api": "5.1.6",
     "testcafe-reporter-json": "^2.1.0",
     "testcafe-reporter-list": "^2.2.0",
diff --git a/src/browser/connection/gateway/index.ts b/src/browser/connection/gateway/index.ts
index 0e96b6cd5..dce64a17b 100644
--- a/src/browser/connection/gateway/index.ts
+++ b/src/browser/connection/gateway/index.ts
@@ -84,6 +84,7 @@ export default class BrowserConnectionGateway extends EventEmitter {
         this._dispatch(`${SERVICE_ROUTES.initScript}/{id}`, proxy, BrowserConnectionGateway._onInitScriptResponse, 'POST');
         this._dispatch(`${SERVICE_ROUTES.activeWindowId}/{id}`, proxy, BrowserConnectionGateway._onGetActiveWindowIdRequest, 'GET');
         this._dispatch(`${SERVICE_ROUTES.activeWindowId}/{id}`, proxy, BrowserConnectionGateway._onSetActiveWindowIdRequest, 'POST');
+        this._dispatch(`${SERVICE_ROUTES.ensureWindowInNativeAutomation}/{id}`, proxy, BrowserConnectionGateway._ensureWindowInNativeAutomation, 'POST');
         this._dispatch(`${SERVICE_ROUTES.closeWindow}/{id}`, proxy, BrowserConnectionGateway._onCloseWindowRequest, 'POST');
         this._dispatch(`${SERVICE_ROUTES.openFileProtocol}/{id}`, proxy, BrowserConnectionGateway._onOpenFileProtocolRequest, 'POST');
         this._dispatch(`${SERVICE_ROUTES.dispatchNativeAutomationEvent}/{id}`, proxy, BrowserConnectionGateway._onDispatchNativeAutomationEvent, 'POST');
@@ -202,6 +203,16 @@ export default class BrowserConnectionGateway extends EventEmitter {
         }
     }
 
+    private static async _ensureWindowInNativeAutomation (req: IncomingMessage, res: ServerResponse, connection: BrowserConnection): Promise<void> {
+        if (BrowserConnectionGateway._ensureConnectionReady(res, connection)) {
+            BrowserConnectionGateway._fetchRequestData(req, async data => {
+                const windowId = await connection.getNewWindowIdInNativeAutomation(JSON.parse(data).windowId);
+
+                respondWithJSON(res, { windowId });
+            });
+        }
+    }
+
     private static _onSetActiveWindowIdRequest (req: IncomingMessage, res: ServerResponse, connection: BrowserConnection): void {
         if (BrowserConnectionGateway._ensureConnectionReady(res, connection)) {
             BrowserConnectionGateway._fetchRequestData(req, data => {
diff --git a/src/browser/connection/index.ts b/src/browser/connection/index.ts
index fd78e9490..4047abf28 100644
--- a/src/browser/connection/index.ts
+++ b/src/browser/connection/index.ts
@@ -35,7 +35,7 @@ import { TestRun as LegacyTestRun } from 'testcafe-legacy-api';
 import { Proxy } from 'testcafe-hammerhead';
 import { NextTestRunInfo, OpenBrowserAdditionalOptions } from '../../shared/types';
 import { EventType } from '../../native-automation/types';
-import NativeAutomation from '../../native-automation';
+import { NativeAutomationBase } from '../../native-automation';
 
 const getBrowserConnectionDebugScope = (id: string): string => `testcafe:browser:connection:${id}`;
 
@@ -119,6 +119,7 @@ export default class BrowserConnection extends EventEmitter {
     public heartbeatUrl = '';
     public statusUrl = '';
     public activeWindowIdUrl = '';
+    public ensureWindowInNativeAutomationUrl = '';
     public closeWindowUrl = '';
     public statusDoneUrl = '';
     public heartbeatRelativeUrl = '';
@@ -202,6 +203,7 @@ export default class BrowserConnection extends EventEmitter {
         this.statusDoneRelativeUrl                     = `${SERVICE_ROUTES.statusDone}/${this.id}`;
         this.idleRelativeUrl                           = `${SERVICE_ROUTES.idle}/${this.id}`;
         this.activeWindowIdUrl                         = `${SERVICE_ROUTES.activeWindowId}/${this.id}`;
+        this.ensureWindowInNativeAutomationUrl         = `${SERVICE_ROUTES.ensureWindowInNativeAutomation}/${this.id}`;
         this.closeWindowUrl                            = `${SERVICE_ROUTES.closeWindow}/${this.id}`;
         this.openFileProtocolRelativeUrl               = `${SERVICE_ROUTES.openFileProtocol}/${this.id}`;
         this.dispatchNativeAutomationEventRelativeUrl         = `${SERVICE_ROUTES.dispatchNativeAutomationEvent}/${this.id}`;
@@ -646,11 +648,19 @@ export default class BrowserConnection extends EventEmitter {
         return this.provider.supportNativeAutomation();
     }
 
-    public getNativeAutomation (): NativeAutomation {
+    public getNativeAutomation (): NativeAutomationBase {
         return this.provider.getNativeAutomation(this.id);
     }
 
     public isNativeAutomationEnabled (): boolean {
         return this._options.nativeAutomation;
     }
+
+    async getNewWindowIdInNativeAutomation (windowId: string): Promise<void> {
+        return this.provider.getNewWindowIdInNativeAutomation(this.id, windowId);
+    }
+
+    public resetActiveWindowId (): void {
+        this.provider.resetActiveWindowId(this.id);
+    }
 }
diff --git a/src/browser/connection/service-routes.ts b/src/browser/connection/service-routes.ts
index 09968b335..d601acbd9 100644
--- a/src/browser/connection/service-routes.ts
+++ b/src/browser/connection/service-routes.ts
@@ -8,6 +8,7 @@ export default {
     idle:                                  '/browser/idle',
     idleForced:                            '/browser/idle-forced',
     activeWindowId:                        '/browser/active-window-id',
+    ensureWindowInNativeAutomation:        '/browser/ensure-window-in-native-automation',
     closeWindow:                           '/browser/close-window',
     serviceWorker:                         '/service-worker.js',
     openFileProtocol:                      '/browser/open-file-protocol',
diff --git a/src/browser/provider/built-in/dedicated/base.js b/src/browser/provider/built-in/dedicated/base.js
index bc4594c0e..6083d201a 100644
--- a/src/browser/provider/built-in/dedicated/base.js
+++ b/src/browser/provider/built-in/dedicated/base.js
@@ -14,6 +14,10 @@ export default {
         return this.openedBrowsers[browserId].activeWindowId;
     },
 
+    resetActiveWindowId (browserId) {
+        this.openedBrowsers[browserId].activeWindowId = this.openedBrowsers[browserId].nativeAutomation?.windowId;
+    },
+
     setActiveWindowId (browserId, val) {
         this.openedBrowsers[browserId].activeWindowId = val;
     },
@@ -101,10 +105,10 @@ export default {
         await this.resizeWindow(browserId, maximumSize.width, maximumSize.height, maximumSize.width, maximumSize.height);
     },
 
-    async closeBrowserChildWindow (browserId) {
+    async closeBrowserChildWindow (browserId, windowId) {
         const runtimeInfo   = this.openedBrowsers[browserId];
         const browserClient = this._getBrowserProtocolClient(runtimeInfo);
 
-        return browserClient.closeBrowserChildWindow();
+        return browserClient.closeBrowserChildWindow(windowId);
     },
 };
diff --git a/src/browser/provider/built-in/dedicated/chrome/build-chrome-args.js b/src/browser/provider/built-in/dedicated/chrome/build-chrome-args.js
index f438d5b0b..d3b53b236 100644
--- a/src/browser/provider/built-in/dedicated/chrome/build-chrome-args.js
+++ b/src/browser/provider/built-in/dedicated/chrome/build-chrome-args.js
@@ -1,12 +1,14 @@
 export const CONTAINERIZED_CHROME_FLAGS = ['--no-sandbox', '--disable-dev-shm-usage'];
 
-export function buildChromeArgs ({ config, cdpPort, platformArgs, tempProfileDir, isContainerized }) {
+export function buildChromeArgs ({ config, cdpPort, platformArgs, tempProfileDir, isContainerized, isNativeAutomation }) {
     let chromeArgs = []
         .concat(
             cdpPort ? [`--remote-debugging-port=${cdpPort}`] : [],
             !config.userProfile ? [`--user-data-dir=${tempProfileDir.path}`] : [],
             config.headless ? ['--headless'] : [],
             config.userArgs ? [config.userArgs] : [],
+            // NOTE: we need to prevent new window blocking for multiple windows in Native Automation
+            isNativeAutomation ? ['--disable-popup-blocking'] : [],
             platformArgs ? [platformArgs] : []
         )
         .join(' ');
diff --git a/src/browser/provider/built-in/dedicated/chrome/cdp-client/index.ts b/src/browser/provider/built-in/dedicated/chrome/cdp-client/index.ts
index 69c0301d4..ca3a88f97 100644
--- a/src/browser/provider/built-in/dedicated/chrome/cdp-client/index.ts
+++ b/src/browser/provider/built-in/dedicated/chrome/cdp-client/index.ts
@@ -2,7 +2,7 @@ import { Dictionary } from '../../../../../../configuration/interfaces';
 import Protocol from 'devtools-protocol';
 import path from 'path';
 import os from 'os';
-import remoteChrome from 'chrome-remote-interface';
+import remoteChrome, { TargetInfo } from 'chrome-remote-interface';
 import debug from 'debug';
 import { GET_WINDOW_DIMENSIONS_INFO_SCRIPT } from '../../../../utils/client-functions';
 import WARNING_MESSAGE from '../../../../../../notifications/warning-message';
@@ -20,12 +20,25 @@ import delay from '../../../../../../utils/delay';
 
 import StartScreencastRequest = Protocol.Page.StartScreencastRequest;
 import ScreencastFrameEvent = Protocol.Page.ScreencastFrameEvent;
+import { NativeAutomationInitOptions } from '../../../../../../shared/types';
+
+import {
+    NativeAutomationBase,
+    NativeAutomationChildWindow,
+    NativeAutomationMainWindow,
+} from '../../../../../../native-automation';
+
+import {
+    getActiveTab,
+    getFirstTab,
+    getTabById,
+    getTabs,
+}
+    from './utils';
 
 const DEBUG_SCOPE = (id: string): string => `testcafe:browser:provider:built-in:chrome:browser-client:${id}`;
 const DOWNLOADS_DIR = path.join(os.homedir(), 'Downloads');
 
-const DEVTOOLS_TAB_URL_REGEX = /^devtools:\/\/devtools/;
-
 const debugLog = debug('testcafe:browser:provider:built-in:dedicated:chrome');
 
 class ProtocolApiInfo {
@@ -48,6 +61,8 @@ interface VideoFrameData {
     sessionId: number;
 }
 
+export const NEW_WINDOW_OPENED_IN_NATIVE_AUTOMATION = 'new-window-opened-in-native-automation';
+
 export class BrowserClient {
     private _clients: Dictionary<ProtocolApiInfo> = {};
     private readonly _runtimeInfo: RuntimeInfo;
@@ -67,6 +82,10 @@ export class BrowserClient {
         this._lastFrame         = null;
     }
 
+    private get _port (): number {
+        return this._runtimeInfo.cdpPort;
+    }
+
     private get _clientKey (): string {
         return this._runtimeInfo.activeWindowId || this._runtimeInfo.browserId;
     }
@@ -75,21 +94,6 @@ export class BrowserClient {
         return this._runtimeInfo.config;
     }
 
-    private async _getTabs (): Promise<remoteChrome.TargetInfo[]> {
-        const tabs = await remoteChrome.List({ port: this._runtimeInfo.cdpPort });
-
-        return tabs.filter(t => t.type === 'page' && !DEVTOOLS_TAB_URL_REGEX.test(t.url));
-    }
-
-    private async _getActiveTab (): Promise<remoteChrome.TargetInfo> {
-        let tabs = await this._getTabs();
-
-        if (this._runtimeInfo.activeWindowId)
-            tabs = tabs.filter(t => t.title.includes(this._runtimeInfo.activeWindowId));
-
-        return tabs[0];
-    }
-
     private _checkDropOfPerformance (method: CheckedCDPMethod, elapsedTime: [number, number]): void {
         this.debugLogger(`CDP method '${method}' took ${prettyTime(elapsedTime)}`);
 
@@ -103,12 +107,12 @@ export class BrowserClient {
         }
     }
 
-    private async _createClient (): Promise<remoteChrome.ProtocolApi> {
-        const target                     = await this._getActiveTab();
-        const client                     = await remoteChrome({ target, port: this._runtimeInfo.cdpPort });
+    private async _createClient (target: TargetInfo, cacheKey: string = this._clientKey): Promise<remoteChrome.ProtocolApi> {
+        const client = await remoteChrome({ target, port: this._port });
+
         const { Page, Network, Runtime } = client;
 
-        this._clients[this._clientKey] = new ProtocolApiInfo(client);
+        this._clients[cacheKey] = new ProtocolApiInfo(client);
 
         await guardTimeExecution(
             async () => await Page.enable(),
@@ -232,11 +236,8 @@ export class BrowserClient {
         return !!this._parentTarget && this._config.headless;
     }
 
-    public async setClientInactive (): Promise<void> {
-        // NOTE: ensure client exists
-        await this.getActiveClient();
-
-        const client = this._clients[this._clientKey];
+    public async setClientInactive (windowId: string): Promise<void> {
+        const client = this._clients[windowId];
 
         if (client)
             client.inactive = true;
@@ -244,8 +245,11 @@ export class BrowserClient {
 
     public async getActiveClient (): Promise<remoteChrome.ProtocolApi | void> {
         try {
-            if (!this._clients[this._clientKey])
-                await this._createClient();
+            if (!this._clients[this._clientKey]) {
+                const target = await getActiveTab(this._port, this._runtimeInfo.activeWindowId);
+
+                await this._createClient(target);
+            }
         }
         catch (err) {
             debugLog(err);
@@ -261,9 +265,9 @@ export class BrowserClient {
         return info.client;
     }
 
-    public async init (): Promise<void> {
+    public async initMainWindowCdpClient (): Promise<void> {
         try {
-            const tabs = await this._getTabs();
+            const tabs = await getTabs(this._port);
 
             this._parentTarget = tabs.find(t => t.url.includes(this._runtimeInfo.browserId));
 
@@ -311,7 +315,7 @@ export class BrowserClient {
 
     public async closeTab (): Promise<void> {
         if (this._parentTarget)
-            await remoteChrome.Close({ id: this._parentTarget.id, port: this._runtimeInfo.cdpPort });
+            await remoteChrome.Close({ id: this._parentTarget.id, port: this._port });
     }
 
     public async updateMobileViewportSize (): Promise<void> {
@@ -328,8 +332,8 @@ export class BrowserClient {
         this._runtimeInfo.viewportSize.height = windowDimensions.outerHeight;
     }
 
-    public async closeBrowserChildWindow (): Promise<void> {
-        await this.setClientInactive();
+    public async closeBrowserChildWindow (windowId: string): Promise<void> {
+        await this.setClientInactive(windowId);
 
         // NOTE: delay browser window closing
         await delay(100);
@@ -387,4 +391,37 @@ export class BrowserClient {
 
         return Buffer.from(currentVideoFrame.data, 'base64');
     }
+
+    private async _onTargetCreatedHandler (targetInfo: remoteChrome.TargetInfo, options: NativeAutomationInitOptions): Promise<string | null> {
+        const target = await getTabById(this._port, targetInfo.targetId);
+
+        if (!target)
+            return null;
+
+        try {
+            const client           = await this._createClient(target, targetInfo.targetId);
+            const nativeAutomation = new NativeAutomationChildWindow(this._runtimeInfo.browserId, target.id, client, options);
+
+            await nativeAutomation.start();
+
+            return target.id;
+        }
+        catch (err) {
+            debugLog(err);
+
+            return null;
+        }
+    }
+
+    public async createMainWindowNativeAutomation (options: NativeAutomationInitOptions): Promise<NativeAutomationBase | null> {
+        const target = await getFirstTab(this._port);
+        const client = await this._createClient(target);
+
+        const nativeAutomation = new NativeAutomationMainWindow(this._runtimeInfo.browserId, target.id, client, options);
+
+        nativeAutomation.on(NEW_WINDOW_OPENED_IN_NATIVE_AUTOMATION, async targetInfo => this._onTargetCreatedHandler(targetInfo, options));
+
+        return nativeAutomation;
+    }
 }
+
diff --git a/src/browser/provider/built-in/dedicated/chrome/cdp-client/utils.ts b/src/browser/provider/built-in/dedicated/chrome/cdp-client/utils.ts
new file mode 100644
index 000000000..092ab8331
--- /dev/null
+++ b/src/browser/provider/built-in/dedicated/chrome/cdp-client/utils.ts
@@ -0,0 +1,30 @@
+import remoteChrome, { TargetInfo } from 'chrome-remote-interface';
+
+const DEVTOOLS_TAB_URL_REGEX = /^devtools:\/\/devtools/;
+
+export async function getTabs (port: number): Promise<TargetInfo[]> {
+    const tabs = await remoteChrome.List({ port });
+
+    return tabs.filter(t => t.type === 'page' && !DEVTOOLS_TAB_URL_REGEX.test(t.url));
+}
+
+export async function getTabById (port: number, id: string): Promise<TargetInfo> {
+    const tabs = await getTabs(port);
+
+    return tabs.find(tab => tab.id === id) as TargetInfo;
+}
+
+export async function getFirstTab (port: number): Promise<TargetInfo> {
+    const tabs = await getTabs(port);
+
+    return tabs[0];
+}
+
+export async function getActiveTab (port: number, activeWindowId: string): Promise<TargetInfo> {
+    const tabs = await getTabs(port);
+
+    if (activeWindowId)
+        return tabs.find(t => t.title.includes(activeWindowId)) as TargetInfo;
+
+    return tabs[0];
+}
diff --git a/src/browser/provider/built-in/dedicated/chrome/index.js b/src/browser/provider/built-in/dedicated/chrome/index.js
index 68aa97876..b64cd60e7 100644
--- a/src/browser/provider/built-in/dedicated/chrome/index.js
+++ b/src/browser/provider/built-in/dedicated/chrome/index.js
@@ -11,7 +11,6 @@ import {
 import { GET_WINDOW_DIMENSIONS_INFO_SCRIPT } from '../../../utils/client-functions';
 import { BrowserClient } from './cdp-client';
 import { dispatchEvent as dispatchNativeAutomationEvent, navigateTo } from '../../../../../native-automation/utils/cdp';
-import NativeAutomation from '../../../../../native-automation';
 import { chromeBrowserProviderLogger } from '../../../../../utils/debug-loggers';
 import { EventType } from '../../../../../native-automation/types';
 import delay from '../../../../../utils/delay';
@@ -56,20 +55,19 @@ export default {
         this.setUserAgentMetaInfo(browserId, metaInfo, options);
     },
 
-    async _setupNativeAutomation ({ browserId, browserClient, runtimeInfo, nativeAutomationOptions }) {
-        const cdpClient = await browserClient.getActiveClient();
-        const nativeAutomation = new NativeAutomation(browserId, cdpClient, nativeAutomationOptions);
+    async _setupNativeAutomation ({ browserClient, runtimeInfo, nativeAutomationOptions }) {
+        const nativeAutomation = await browserClient.createMainWindowNativeAutomation(nativeAutomationOptions);
 
         await nativeAutomation.start();
 
         runtimeInfo.nativeAutomation = nativeAutomation;
     },
 
-    async _startChrome (runtimeInfo, pageUrl) {
-        if (runtimeInfo.isContainerized)
-            await startLocalChromeOnDocker(pageUrl, runtimeInfo);
+    async _startChrome (startOptions, pageUrl) {
+        if (startOptions.isContainerized)
+            await startLocalChromeOnDocker(pageUrl, startOptions);
         else
-            await startLocalChrome(pageUrl, runtimeInfo);
+            await startLocalChrome(pageUrl, startOptions);
     },
 
     async openBrowser (browserId, pageUrl, config, additionalOptions) {
@@ -85,29 +83,29 @@ export default {
         };
 
         //NOTE: A not-working tab is opened when the browser start in the docker so we should create a new tab.
-        await this._startChrome(runtimeInfo, pageUrl);
+        await this._startChrome(Object.assign({ isNativeAutomation: additionalOptions.nativeAutomation }, runtimeInfo), pageUrl);
         await this.waitForConnectionReady(browserId);
 
         runtimeInfo.viewportSize      = await this.runInitScript(browserId, GET_WINDOW_DIMENSIONS_INFO_SCRIPT);
         runtimeInfo.activeWindowId    = null;
         runtimeInfo.windowDescriptors = {};
 
-        if (!additionalOptions.disableMultipleWindows)
-            runtimeInfo.activeWindowId = this.calculateWindowId();
-
         const browserClient = new BrowserClient(runtimeInfo);
 
         this.openedBrowsers[browserId] = runtimeInfo;
 
-        await browserClient.init();
+        if (additionalOptions.nativeAutomation)
+            await this._setupNativeAutomation({ browserId, browserClient, runtimeInfo, nativeAutomationOptions: toNativeAutomationSetupOptions(additionalOptions, config.headless) });
+
+        if (!additionalOptions.disableMultipleWindows)
+            runtimeInfo.activeWindowId = runtimeInfo?.nativeAutomation?.windowId || this.calculateWindowId();
+
+        await browserClient.initMainWindowCdpClient();
 
         await this._ensureWindowIsExpanded(browserId, runtimeInfo.viewportSize);
 
         this._setUserAgentMetaInfoForEmulatingDevice(browserId, runtimeInfo.config);
 
-        if (additionalOptions.nativeAutomation)
-            await this._setupNativeAutomation({ browserId, browserClient, runtimeInfo, nativeAutomationOptions: toNativeAutomationSetupOptions(additionalOptions, config.headless) });
-
         chromeBrowserProviderLogger('browser opened %s', browserId);
     },
 
@@ -220,4 +218,10 @@ export default {
 
         return runtimeInfo.nativeAutomation;
     },
+
+    async getNewWindowIdInNativeAutomation (browserId) {
+        const runtimeInfo = this.openedBrowsers[browserId];
+
+        return runtimeInfo.nativeAutomation.getNewWindowIdInNativeAutomation();
+    },
 };
diff --git a/src/browser/provider/built-in/dedicated/chrome/local-chrome.js b/src/browser/provider/built-in/dedicated/chrome/local-chrome.js
index 677f6f400..15ee404c6 100644
--- a/src/browser/provider/built-in/dedicated/chrome/local-chrome.js
+++ b/src/browser/provider/built-in/dedicated/chrome/local-chrome.js
@@ -11,11 +11,11 @@ const browserStarter = new BrowserStarter();
 const LIST_TABS_TIMEOUT = 10000;
 const LIST_TABS_DELAY   = 500;
 
-export async function start (pageUrl, { browserName, config, cdpPort, tempProfileDir, isContainerized }) {
+export async function start (pageUrl, { browserName, config, cdpPort, tempProfileDir, isContainerized, isNativeAutomation }) {
     const chromeInfo           = await browserTools.getBrowserInfo(config.path || browserName);
     const chromeOpenParameters = Object.assign({}, chromeInfo);
 
-    chromeOpenParameters.cmd = buildChromeArgs({ config, cdpPort, platformArgs: chromeOpenParameters.cmd, tempProfileDir, isContainerized });
+    chromeOpenParameters.cmd = buildChromeArgs({ config, cdpPort, platformArgs: chromeOpenParameters.cmd, tempProfileDir, isContainerized, isNativeAutomation });
 
     await browserStarter.startBrowser(chromeOpenParameters, pageUrl);
 }
diff --git a/src/browser/provider/index.ts b/src/browser/provider/index.ts
index e7526e163..0b71bd385 100644
--- a/src/browser/provider/index.ts
+++ b/src/browser/provider/index.ts
@@ -16,7 +16,7 @@ import { WindowDimentionsInfo } from '../interfaces';
 import getLocalOSInfo, { OSInfo } from 'get-os-info';
 import { OpenBrowserAdditionalOptions } from '../../shared/types';
 import { EventType } from '../../native-automation/types';
-import NativeAutomation from '../../native-automation';
+import { NativeAutomationBase } from '../../native-automation';
 
 
 const DEBUG_LOGGER = debug('testcafe:browser:provider');
@@ -440,6 +440,17 @@ export default class BrowserProvider {
         return this.plugin.getActiveWindowId(browserId);
     }
 
+    public resetActiveWindowId (browserId: string): string | null {
+        if (!this.plugin.supportMultipleWindows)
+            return null;
+
+        return this.plugin.resetActiveWindowId(browserId);
+    }
+
+    public getNewActiveWindowId (browserId: string): string | null {
+        return this.plugin.getNewActiveWindowId(browserId);
+    }
+
     public setActiveWindowId (browserId: string, val: string): void {
         this.plugin.setActiveWindowId(browserId, val);
     }
@@ -448,8 +459,8 @@ export default class BrowserProvider {
         await this.plugin.openFileProtocol(browserId, url);
     }
 
-    public async closeBrowserChildWindow (browserId: string): Promise<void> {
-        await this.plugin.closeBrowserChildWindow(browserId);
+    public async closeBrowserChildWindow (browserId: string, windowId: string): Promise<void> {
+        await this.plugin.closeBrowserChildWindow(browserId, windowId);
     }
 
     public async dispatchNativeAutomationEvent (browserId: string, type: EventType, options: any): Promise<void> {
@@ -464,7 +475,11 @@ export default class BrowserProvider {
         return this.plugin.supportNativeAutomation();
     }
 
-    public getNativeAutomation (browserId: string): NativeAutomation {
+    public getNativeAutomation (browserId: string): NativeAutomationBase {
         return this.plugin.getNativeAutomation(browserId);
     }
+
+    public getNewWindowIdInNativeAutomation (browserId: string, windowId: string): Promise<void> {
+        return this.plugin.getNewWindowIdInNativeAutomation(browserId, windowId);
+    }
 }
diff --git a/src/browser/provider/plugin-host.js b/src/browser/provider/plugin-host.js
index 66e3f81b4..18c563507 100644
--- a/src/browser/provider/plugin-host.js
+++ b/src/browser/provider/plugin-host.js
@@ -159,7 +159,7 @@ export default class BrowserProviderPluginHost {
         return value;
     }
 
-    async closeBrowserChildWindow (/*browserId*/) {
+    async closeBrowserChildWindow (/*browserId, windowId*/) {
         return Promise.resolve();
     }
 
@@ -174,4 +174,8 @@ export default class BrowserProviderPluginHost {
     getNativeAutomation (/*browserId*/) {
         return null;
     }
+
+    getNewWindowIdInNativeAutomation (/*browserId, windowId*/) {
+        return Promise.resolve();
+    }
 }
diff --git a/src/cli/argument-parser/index.ts b/src/cli/argument-parser/index.ts
index a34daaabf..5118d802a 100644
--- a/src/cli/argument-parser/index.ts
+++ b/src/cli/argument-parser/index.ts
@@ -194,6 +194,7 @@ export default class CLIArgumentParser {
             .option('--screenshots-full-page', 'enable full-page screenshots')
             .option('--compiler-options <option=value[,...]>', 'specify test compilation settings')
             .option('--disable-multiple-windows', 'disable the multi-window mode')
+            .option('--experimental-multiple-windows', 'Enable experimental support for multiple windows in Native Automation mode')
             .option('--disable-http2', 'force the proxy to issue HTTP/1.1 requests')
             .option('--cache', 'cache web assets between test runs')
             .option('--base-url <url>', 'set the base url for the test run')
diff --git a/src/client/browser/index.js b/src/client/browser/index.js
index c66de6282..d9fec45f8 100644
--- a/src/client/browser/index.js
+++ b/src/client/browser/index.js
@@ -204,6 +204,13 @@ export function setActiveWindowId (activeWindowIdUrl, createXHR, windowId) {
     });
 }
 
+export function ensureWindowInNativeAutomation (ensureWindowInNativeAutomationUrl, createXHR, windowId) {
+    return sendXHR(ensureWindowInNativeAutomationUrl, createXHR, {
+        method: 'POST',
+        data:   JSON.stringify({ windowId }), //eslint-disable-line no-restricted-globals
+    });
+}
+
 export function closeWindow (closeWindowUrl, createXHR, windowId) {
     return sendXHR(closeWindowUrl, createXHR, {
         method: 'POST',
diff --git a/src/client/driver/driver.js b/src/client/driver/driver.js
index 703f3f501..fad4bf07d 100644
--- a/src/client/driver/driver.js
+++ b/src/client/driver/driver.js
@@ -266,7 +266,7 @@ export default class Driver extends serviceUtils.EventEmitter {
 
     _getCurrentWindowId () {
         if (this.options.nativeAutomation)
-            return this.runInfo.activeWindowId;
+            return this.runInfo.windowId;
 
         const currentUrl     = window.location.toString();
         const parsedProxyUrl = hammerhead.utils.url.parseProxyUrl(currentUrl);
@@ -349,8 +349,8 @@ export default class Driver extends serviceUtils.EventEmitter {
             this.contextStorage.setItem(PENDING_PAGE_ERROR, error);
     }
 
-    _addChildWindowDriverLink (e) {
-        const childWindowDriverLink = new ChildWindowDriverLink(e.window, e.windowId);
+    _addChildWindowDriverLink (wnd, windowId) {
+        const childWindowDriverLink = new ChildWindowDriverLink(wnd, windowId);
 
         this.childWindowDriverLinks.push(childWindowDriverLink);
         this._ensureClosedChildWindowWatcher();
@@ -417,6 +417,9 @@ export default class Driver extends serviceUtils.EventEmitter {
         if (!this.options.nativeAutomation)
             return;
 
+        if (this.options.experimentalMultipleWindows)
+            return;
+
         this._onReady(new DriverStatus({
             isCommandResult: true,
             executionError:  new MultipleWindowsModeIsNotSupportedInNativeAutomationModeError(),
@@ -425,9 +428,37 @@ export default class Driver extends serviceUtils.EventEmitter {
         e.isPrevented = true;
     }
 
-    _onChildWindowOpened (e) {
-        this._addChildWindowDriverLink(e);
-        this._switchToChildWindow(e.windowId);
+    async _ensureNewWindowOpenedInNativeAutomation (e) {
+        const result = await browser.ensureWindowInNativeAutomation(
+            this.communicationUrls.ensureWindowInNativeAutomationUrl,
+            hammerhead.createNativeXHR,
+        );
+
+        if (!result.windowId)
+            return null;
+
+        if (e.form)
+            nativeMethods.formSubmit.apply(e.form);
+        else
+            nativeMethods.windowOpen.call(window, e.pageUrl, e.windowName);
+
+        return result.windowId;
+    }
+
+    async _onChildWindowOpened (e, isOpeningInIframe) {
+        let windowId = e.windowId;
+
+        if (this.options.nativeAutomation && !isOpeningInIframe) {
+            this._waitChildWindowOpenedInNativeAutomation = this._ensureNewWindowOpenedInNativeAutomation(e);
+
+            windowId = await this._waitChildWindowOpenedInNativeAutomation;
+
+            if (!windowId)
+                return;
+        }
+
+        this._addChildWindowDriverLink(e.window, windowId);
+        this._switchToChildWindow(windowId);
     }
 
     _sendStartToRestoreCommand () {
@@ -594,12 +625,18 @@ export default class Driver extends serviceUtils.EventEmitter {
     }
 
     _getWindowInfo () {
-        const parsedUrl = hammerhead.utils.url.parseProxyUrl(window.location.toString());
+        let url = window.location.toString();
+
+        if (!this.options.nativeAutomation) {
+            const { destUrl } = hammerhead.utils.url.parseProxyUrl(url);
+
+            url = destUrl;
+        }
 
         return {
             id:    this.windowId,
             title: document.title,
-            url:   parsedUrl.destUrl,
+            url,
         };
     }
 
@@ -833,7 +870,7 @@ export default class Driver extends serviceUtils.EventEmitter {
         if (this._stopRespondToChildren)
             return;
 
-        this._addChildWindowDriverLink({ window: wnd, windowId: msg.windowId });
+        this._addChildWindowDriverLink(wnd, msg.windowId);
 
         const allChildWindowLinksRestored = this.childWindowDriverLinks.length === this.contextStorage.getItem(PENDING_CHILD_WINDOW_COUNT);
 
@@ -870,7 +907,7 @@ export default class Driver extends serviceUtils.EventEmitter {
         const childWindowDriverLinkExists = !!this.childWindowDriverLinks.find(link => link.windowId === msg.windowId);
 
         if (!childWindowDriverLinkExists)
-            this._onChildWindowOpened({ window: wnd, windowId: msg.windowId });
+            this._onChildWindowOpened({ window: wnd, windowId: msg.windowId }, true);
     }
 
     _handleStopInternalFromFrame (msg, wnd) {
@@ -1242,6 +1279,13 @@ export default class Driver extends serviceUtils.EventEmitter {
             .then(driverStatus => {
                 this.contextStorage.setItem(this.COMMAND_EXECUTING_FLAG, false);
                 this.contextStorage.setItem(EXECUTING_SKIP_JS_ERRORS_FUNCTION_FLAG, false);
+
+                if (this._waitChildWindowOpenedInNativeAutomation)
+                    return this._waitChildWindowOpenedInNativeAutomation.then(() => driverStatus);
+
+                return driverStatus;
+            })
+            .then(driverStatus => {
                 this._onReady(driverStatus);
             });
     }
@@ -1438,7 +1482,9 @@ export default class Driver extends serviceUtils.EventEmitter {
     }
 
     async _restoreChildWindowLinks () {
-        if (!this.contextStorage.getItem(PENDING_CHILD_WINDOW_COUNT))
+        const pendingChildWindowCount = this.contextStorage.getItem(PENDING_CHILD_WINDOW_COUNT);
+
+        if (!pendingChildWindowCount || pendingChildWindowCount === this.childWindowDriverLinks.length)
             return;
 
         const restoreChildWindowsPromise = new Promise(resolve => {
@@ -1563,6 +1609,8 @@ export default class Driver extends serviceUtils.EventEmitter {
             return Promise.resolve();
 
         return Promise.all(this.childWindowDriverLinks.map(childWindowDriverLink => {
+            childWindowDriverLink.ignoreMasterSwitching = true;
+
             return childWindowDriverLink.closeAllChildWindows();
         }))
             .then(() => {
@@ -1876,7 +1924,7 @@ export default class Driver extends serviceUtils.EventEmitter {
     }
 
     async _getDriverRole () {
-        if (!this.windowId)
+        if (!this.windowId && !this.options.nativeAutomation)
             return DriverRole.master;
 
         const { activeWindowId } = await browser.getActiveWindowId(this.communicationUrls.activeWindowId, hammerhead.createNativeXHR);
diff --git a/src/client/driver/generate-id.js b/src/client/driver/generate-id.js
index 42784dd62..81b9e6e47 100644
--- a/src/client/driver/generate-id.js
+++ b/src/client/driver/generate-id.js
@@ -1,5 +1,7 @@
 import { nativeMethods } from './deps/hammerhead';
 
+// NOTE: We need the additional 'Math.random' part to ensure that the
+// method does not generate identical IDs when executed within an array
 export default function () {
-    return nativeMethods.performanceNow().toString();
+    return `${nativeMethods.performanceNow()}.${Math.floor(Math.random() * 100000)}`;
 }
diff --git a/src/client/driver/iframe-driver.js b/src/client/driver/iframe-driver.js
index 39bf07cf4..9a839339b 100644
--- a/src/client/driver/iframe-driver.js
+++ b/src/client/driver/iframe-driver.js
@@ -21,8 +21,8 @@ import {
 const messageSandbox = eventSandbox.message;
 
 export default class IframeDriver extends Driver {
-    constructor (testRunId, options) {
-        super(testRunId, {}, {}, options);
+    constructor (testRunId, communicationsUrls, options) {
+        super(testRunId, communicationsUrls, {}, options);
 
         this.lastParentDriverMessageId = null;
         this.parentDriverLink          = new ParentIframeDriverLink(window.parent);
@@ -39,9 +39,16 @@ export default class IframeDriver extends Driver {
         // NOTE: do nothing because hammerhead sends console messages to the top window directly
     }
 
-    // NOTE: when the new page is opened in the iframe we send a message to the top window
-    // to start waiting for the new page is loaded
-    _onChildWindowOpened () {
+    async _onChildWindowOpened (e) {
+        if (this.options.nativeAutomation) {
+            const windowId = await this._ensureNewWindowOpenedInNativeAutomation(e);
+
+            if (!windowId)
+                return;
+        }
+
+        // NOTE: when the new page is opened in the iframe we send a message to the top window
+        // to start waiting for the new page is loaded
         messageSandbox.sendServiceMsg(new ChildWindowIsOpenedInFrameMessage(), window.top);
     }
 
diff --git a/src/configuration/default-values.ts b/src/configuration/default-values.ts
index 454aaa0c8..f6f5963e3 100644
--- a/src/configuration/default-values.ts
+++ b/src/configuration/default-values.ts
@@ -15,13 +15,14 @@ export const DEFAULT_CONCURRENCY_VALUE = 1;
 
 export const DEFAULT_SOURCE_DIRECTORIES = ['tests', 'test'];
 
-export const DEFAULT_DEVELOPMENT_MODE          = false;
-export const DEFAULT_RETRY_TEST_PAGES          = false;
-export const DEFAULT_DISABLE_HTTP2             = false;
-export const DEFAULT_DISABLE_NATIVE_AUTOMATION = false;
-export const DEFAULT_SCREENSHOT_THUMBNAILS     = true;
-export const DEFAULT_FILTER_FN                 = null;
-export const DEFAULT_DISABLE_CROSS_DOMAIN     = false;
+export const DEFAULT_DEVELOPMENT_MODE              = false;
+export const DEFAULT_RETRY_TEST_PAGES              = false;
+export const DEFAULT_DISABLE_HTTP2                 = false;
+export const DEFAULT_DISABLE_NATIVE_AUTOMATION     = false;
+export const DEFAULT_EXPERIMENTAL_MULTIPLE_WINDOWS = false;
+export const DEFAULT_SCREENSHOT_THUMBNAILS         = true;
+export const DEFAULT_FILTER_FN                     = null;
+export const DEFAULT_DISABLE_CROSS_DOMAIN          = false;
 
 export const DEFAULT_TYPESCRIPT_COMPILER_OPTIONS: Dictionary<boolean | number> = {
     experimentalDecorators:  true,
diff --git a/src/configuration/option-names.ts b/src/configuration/option-names.ts
index dc62b9918..08bf9401f 100644
--- a/src/configuration/option-names.ts
+++ b/src/configuration/option-names.ts
@@ -43,6 +43,7 @@ enum OptionNames {
     disableScreenshots = 'disableScreenshots',
     debugLogger = 'debugLogger',
     disableMultipleWindows = 'disableMultipleWindows',
+    experimentalMultipleWindows = 'experimentalMultipleWindows',
     disableHttp2 = 'disableHttp2',
     disableNativeAutomation = 'disableNativeAutomation',
     compilerOptions = 'compilerOptions',
diff --git a/src/configuration/run-option-names.ts b/src/configuration/run-option-names.ts
index 61e075e43..a4779d37b 100644
--- a/src/configuration/run-option-names.ts
+++ b/src/configuration/run-option-names.ts
@@ -22,5 +22,6 @@ export default [
     OPTION_NAMES.ajaxRequestTimeout,
     OPTION_NAMES.retryTestPages,
     OPTION_NAMES.disableNativeAutomation,
+    OPTION_NAMES.experimentalMultipleWindows,
     OPTION_NAMES.baseUrl,
 ];
diff --git a/src/native-automation/index.ts b/src/native-automation/index.ts
index 6dcb1a4c7..adb991c7a 100644
--- a/src/native-automation/index.ts
+++ b/src/native-automation/index.ts
@@ -5,17 +5,23 @@ import { NativeAutomationInitOptions } from '../shared/types';
 import { nativeAutomationLogger } from '../utils/debug-loggers';
 import SessionStorage from './session-storage';
 import NativeAutomationApiBase from './api-base';
+import AsyncEventEmitter from '../utils/async-event-emitter';
+import { NEW_WINDOW_OPENED_IN_NATIVE_AUTOMATION } from '../browser/provider/built-in/dedicated/chrome/cdp-client';
 
-export default class NativeAutomation {
-    private readonly _client: ProtocolApi;
+export class NativeAutomationBase extends AsyncEventEmitter {
+    protected readonly _client: ProtocolApi;
     public readonly requestPipeline;
     public readonly sessionStorage: SessionStorage;
     private readonly options: NativeAutomationInitOptions;
+    protected readonly windowId: string;
 
-    public constructor (browserId: string, client: ProtocolApi, options: NativeAutomationInitOptions) {
+    public constructor (browserId: string, windowId: string, client: ProtocolApi, options: NativeAutomationInitOptions, isMainWindow: boolean) {
+        super();
+
+        this.windowId        = windowId;
         this._client         = client;
         this.options         = options;
-        this.requestPipeline = new NativeAutomationRequestPipeline(browserId, client, options);
+        this.requestPipeline = new NativeAutomationRequestPipeline(browserId, windowId, client, isMainWindow, options);
         this.sessionStorage  = new SessionStorage(browserId, client, options);
 
         addCustomDebugFormatters();
@@ -65,3 +71,43 @@ export default class NativeAutomation {
         ];
     }
 }
+
+export class NativeAutomationMainWindow extends NativeAutomationBase {
+    private _resolveNewWindowOpeningPromise: Promise<any> | undefined;
+
+    public constructor (browserId: string, windowId: string, client: ProtocolApi, options: NativeAutomationInitOptions) {
+        super(browserId, windowId, client, options, true);
+    }
+
+    async start (): Promise<void> {
+        await super.start();
+
+        await this._client.Target.setDiscoverTargets({ discover: true });
+
+        this._client.Target.on('targetCreated', async ({ targetInfo }) => {
+            if (targetInfo.type !== 'page' || targetInfo.targetId === this.windowId)
+                return;
+
+            this._resolveNewWindowOpeningPromise = this.emit(NEW_WINDOW_OPENED_IN_NATIVE_AUTOMATION, targetInfo);
+        });
+    }
+
+    public async getNewWindowIdInNativeAutomation (): Promise<string> {
+        if (!this._resolveNewWindowOpeningPromise)
+            throw new Error('Cannot get new window id');
+
+        return this._resolveNewWindowOpeningPromise
+            .then(res => {
+                const windowId = res[0];
+
+                return windowId;
+            });
+    }
+}
+
+export class NativeAutomationChildWindow extends NativeAutomationBase {
+    public constructor (browserId: string, windowId: string, client: ProtocolApi, options: NativeAutomationInitOptions) {
+        super(browserId, windowId, client, options, false);
+    }
+}
+
diff --git a/src/native-automation/request-pipeline/index.ts b/src/native-automation/request-pipeline/index.ts
index b30ed06a8..2bd5d5a57 100644
--- a/src/native-automation/request-pipeline/index.ts
+++ b/src/native-automation/request-pipeline/index.ts
@@ -59,7 +59,6 @@ import TestRunBridge from './test-run-bridge';
 import NativeAutomationRequestContextInfo from './context-info';
 import { failedToFindDNSError, sslCertificateError } from '../errors';
 
-
 const ALL_REQUEST_RESPONSES = { requestStage: 'Request' } as RequestPattern;
 const ALL_REQUEST_REQUESTS  = { requestStage: 'Response' } as RequestPattern;
 
@@ -72,6 +71,8 @@ const TARGET_INFO_TYPE = {
 };
 
 export default class NativeAutomationRequestPipeline extends NativeAutomationApiBase {
+    private readonly _windowId: string;
+    private readonly _isMainWindow: boolean;
     private readonly _testRunBridge: TestRunBridge;
     private readonly _contextInfo: NativeAutomationRequestContextInfo;
     public readonly requestHookEventProvider: NativeAutomationRequestHookEventProvider;
@@ -84,10 +85,12 @@ export default class NativeAutomationRequestPipeline extends NativeAutomationApi
     private readonly _failedRequestIds: string[];
     private _pendingCertificateError: CertificateErrorEvent | null;
 
-    public constructor (browserId: string, client: ProtocolApi, options: NativeAutomationInitOptions) {
+    public constructor (browserId: string, windowId: string, client: ProtocolApi, isMainWindow: boolean, options: NativeAutomationInitOptions) {
         super(browserId, client, options);
 
-        this._testRunBridge           = new TestRunBridge(browserId);
+        this._testRunBridge           = new TestRunBridge(browserId, windowId);
+        this._windowId                = windowId;
+        this._isMainWindow            = isMainWindow;
         this._contextInfo             = new NativeAutomationRequestContextInfo(this._testRunBridge);
         this._specialServiceRoutes    = this._getSpecialServiceRoutes();
         this.requestHookEventProvider = new NativeAutomationRequestHookEventProvider();
@@ -430,7 +433,7 @@ export default class NativeAutomationRequestPipeline extends NativeAutomationApi
             const specialRequestHandler = getSpecialRequestHandler(event, this.options, this._specialServiceRoutes);
 
             if (specialRequestHandler)
-                await specialRequestHandler(event, this._client, this.options, sessionId);
+                await specialRequestHandler(event, this._client, this._isMainWindow, this.options, sessionId);
             else
                 await this._handleOtherRequests(event, sessionId);
         });
diff --git a/src/native-automation/request-pipeline/special-handlers.ts b/src/native-automation/request-pipeline/special-handlers.ts
index 00b7f7921..b20eafcb1 100644
--- a/src/native-automation/request-pipeline/special-handlers.ts
+++ b/src/native-automation/request-pipeline/special-handlers.ts
@@ -34,7 +34,7 @@ async function handleRequestPauseEvent (event: RequestPausedEvent, client: Proto
 
 const internalRequest = {
     condition: (event: RequestPausedEvent): boolean => !event.networkId && event.resourceType !== 'Document' && !event.request.url,
-    handler:   async (event: RequestPausedEvent, client: ProtocolApi, options: NativeAutomationInitOptions, sessionId: SessionId): Promise<void> => {
+    handler:   async (event: RequestPausedEvent, client: ProtocolApi, isMainWindow: boolean, options: NativeAutomationInitOptions, sessionId: SessionId): Promise<void> => {
         requestPipelineInternalRequestLogger('%r', event);
 
         await handleRequestPauseEvent(event, client, sessionId);
@@ -52,10 +52,25 @@ const serviceRequest = {
 
         return options.serviceDomains.some(domain => url.startsWith(domain));
     },
-    handler: async (event: RequestPausedEvent, client: ProtocolApi, options: NativeAutomationInitOptions, sessionId: SessionId): Promise<void> => {
+    handler: async (event: RequestPausedEvent, client: ProtocolApi, isMainWindow: boolean, options: NativeAutomationInitOptions, sessionId: SessionId): Promise<void> => {
         requestPipelineServiceRequestLogger('%r', event);
 
-        await handleRequestPauseEvent(event, client, sessionId);
+        try {
+            await handleRequestPauseEvent(event, client, sessionId);
+        }
+        catch (err) {
+            if (isMainWindow) {
+                requestPipelineServiceRequestLogger('Failed to process request in main window: %s', event.request.url);
+
+                throw err;
+            }
+            else {
+                // NOTE: Sometimes, a child window sends a heartbeat request and then immediately closes.
+                // In these situations, we need to catch errors because we can't handle this request correctly
+                // when the cdpClient has already closed.
+                requestPipelineServiceRequestLogger('Failed to process request in child window: %s', event.request.url);
+            }
+        }
     },
 } as RequestHandler;
 
@@ -65,7 +80,7 @@ const defaultFaviconRequest = {
 
         return parsedUrl.pathname === DEFAULT_FAVICON_PATH;
     },
-    handler: async (event: RequestPausedEvent, client: ProtocolApi, options: NativeAutomationInitOptions, sessionId: SessionId): Promise<void> => {
+    handler: async (event: RequestPausedEvent, client: ProtocolApi, isMainWindow: boolean, options: NativeAutomationInitOptions, sessionId: SessionId): Promise<void> => {
         requestPipelineLogger('%r', event);
 
         if (isRequest(event))
@@ -93,7 +108,7 @@ const SPECIAL_REQUEST_HANDLERS = [
     defaultFaviconRequest,
 ];
 
-export default function getSpecialRequestHandler (event: RequestPausedEvent, options?: NativeAutomationInitOptions, serviceRoutes?: SpecialServiceRoutes): any {
+export default function getSpecialRequestHandler (event: RequestPausedEvent, options?: NativeAutomationInitOptions, serviceRoutes?: SpecialServiceRoutes): RequestHandler['handler'] | null {
     const specialRequestHandler = SPECIAL_REQUEST_HANDLERS.find(h => h.condition(event, options, serviceRoutes));
 
     return specialRequestHandler ? specialRequestHandler.handler : null;
diff --git a/src/native-automation/types.ts b/src/native-automation/types.ts
index f5d864ea3..2aa0fffb9 100644
--- a/src/native-automation/types.ts
+++ b/src/native-automation/types.ts
@@ -19,7 +19,7 @@ export interface DocumentResourceInfo {
 
 export interface RequestHandler {
     condition: (event: RequestPausedEvent, options?: NativeAutomationInitOptions, serviceRoutes?: SpecialServiceRoutes) => boolean;
-    handler: (event: RequestPausedEvent, client: ProtocolApi, options?: NativeAutomationInitOptions) => Promise<void>;
+    handler: (event: RequestPausedEvent, client: ProtocolApi, isMainWindow: boolean, options?: NativeAutomationInitOptions, sessionId?: SessionId) => Promise<void>;
 }
 
 export interface InjectableResourcesOptions {


Test patch:
diff --git a/.github/workflows/test-functional-local-multiple-windows-na.yml b/.github/workflows/test-functional-local-multiple-windows-na.yml
new file mode 100644
index 000000000..4820675cb
--- /dev/null
+++ b/.github/workflows/test-functional-local-multiple-windows-na.yml
@@ -0,0 +1,19 @@
+name: Test Functional (Multiple Windows in Native Automation mode)
+
+on:
+  workflow_dispatch:
+    inputs:
+      sha:
+        description: 'The test commit SHA or ref'
+        required: true
+        default: 'master'
+      merged_sha:
+        description: 'The merge commit SHA'
+      deploy_run_id:
+        description: 'The ID of a deployment workspace run with artifacts'
+jobs:
+  test:
+    uses: ./.github/workflows/test-functional.yml
+    with:
+      test-script: 'npx gulp test-functional-local-multiple-windows-na-run --steps-as-tasks'
+      display: ':99.0'
diff --git a/src/api/test-controller/index.js b/src/api/test-controller/index.js
index ba84d564f..a92269d87 100644
--- a/src/api/test-controller/index.js
+++ b/src/api/test-controller/index.js
@@ -185,7 +185,7 @@ export default class TestController {
     _validateMultipleWindowCommand (apiMethodName) {
         const { disableMultipleWindows, activeWindowId } = this.testRun;
 
-        if (this.testRun.isNativeAutomation)
+        if (this.testRun.isNativeAutomation && !this.testRun.isExperimentalMultipleWindows)
             throw new MultipleWindowsModeIsNotSupportedInNativeAutomationModeError(apiMethodName);
 
         if (disableMultipleWindows)
diff --git a/src/client/test-run/iframe.js.mustache b/src/client/test-run/iframe.js.mustache
index 84432d17c..2be0d02f8 100644
--- a/src/client/test-run/iframe.js.mustache
+++ b/src/client/test-run/iframe.js.mustache
@@ -1,12 +1,22 @@
 (function () {
+    var origin                      = location.origin;
+    var experimentalMultipleWindows = {{{experimentalMultipleWindows}}}
+    var nativeAutomation            = {{{nativeAutomation}}};
+
+    if (nativeAutomation)
+        origin = {{{domain}}};
+
     var IframeDriver = window['%testCafeIframeDriver%'];
     var driver       = new IframeDriver({{{testRunId}}}, {
+        ensureWindowInNativeAutomationUrl: origin + {{{browserEnsureWindowInNativeAutomationUrl}}},
+    }, {
         selectorTimeout:  {{{selectorTimeout}}},
         pageLoadTimeout:  {{{pageLoadTimeout}}},
         dialogHandler:    {{{dialogHandler}}},
         retryTestPages:   {{{retryTestPages}}},
         speed:            {{{speed}}},
-        nativeAutomation: {{{nativeAutomation}}},
+        nativeAutomation,
+        experimentalMultipleWindows,
     });
 
     driver.start();
diff --git a/src/client/test-run/index.js.mustache b/src/client/test-run/index.js.mustache
index 794b3a83a..3cbf0d1d7 100644
--- a/src/client/test-run/index.js.mustache
+++ b/src/client/test-run/index.js.mustache
@@ -11,6 +11,8 @@
     var testRunId                                       = {{{testRunId}}};
     var browserId                                       = {{{browserId}}};
     var activeWindowId                                  = {{{activeWindowId}}};
+    var windowId                                        = {{{windowId}}};
+    var experimentalMultipleWindows                     = {{{experimentalMultipleWindows}}};
     var selectorTimeout                                 = {{{selectorTimeout}}};
     var pageLoadTimeout                                 = {{{pageLoadTimeout}}};
     var childWindowReadyTimeout                         = {{{childWindowReadyTimeout}}};
@@ -22,6 +24,8 @@
     var browserIdleUrl                                  = origin + {{{browserIdleRelativeUrl}}};
     var browserOpenFileProtocolUrl                      = origin + {{{browserOpenFileProtocolRelativeUrl}}};
     var browserActiveWindowIdUrl                        = origin + {{{browserActiveWindowIdUrl}}};
+    var browserActiveWindowIdUrl                        = origin + {{{browserActiveWindowIdUrl}}};
+    var browserEnsureWindowInNativeAutomationUrl        = origin + {{{browserEnsureWindowInNativeAutomationUrl}}};
     var browserCloseWindowUrl                           = origin + {{{browserCloseWindowUrl}}};
     var browserDispatchNativeAutomationEventUrl         = origin + {{{browserDispatchNativeAutomationEventRelativeUrl}}};
     var browserDispatchNativeAutomationEventSequenceUrl = origin + {{{browserDispatchNativeAutomationEventSequenceRelativeUrl}}};
@@ -36,33 +40,36 @@
     var ClientDriver = window['%testCafeDriver%'];
     var driver       = new ClientDriver(testRunId,
         {
-            heartbeat:                      browserHeartbeatUrl,
-            status:                         browserStatusUrl,
-            statusDone:                     browserStatusDoneUrl,
-            idle:                           browserIdleUrl,
-            activeWindowId:                 browserActiveWindowIdUrl,
-            closeWindow:                    browserCloseWindowUrl,
-            openFileProtocolUrl:            browserOpenFileProtocolUrl,
-            dispatchNativeAutomationEvent:  browserDispatchNativeAutomationEventUrl,
+            heartbeat:                             browserHeartbeatUrl,
+            status:                                browserStatusUrl,
+            statusDone:                            browserStatusDoneUrl,
+            idle:                                  browserIdleUrl,
+            activeWindowId:                        browserActiveWindowIdUrl,
+            ensureWindowInNativeAutomationUrl:     browserEnsureWindowInNativeAutomationUrl,
+            closeWindow:                           browserCloseWindowUrl,
+            openFileProtocolUrl:                   browserOpenFileProtocolUrl,
+            dispatchNativeAutomationEvent:         browserDispatchNativeAutomationEventUrl,
             dispatchNativeAutomationEventSequence: browserDispatchNativeAutomationEventSequenceUrl,
-            parseSelector:                  browserParseSelectorUrl,
+            parseSelector:                         browserParseSelectorUrl,
         },
         {
             userAgent:      userAgent,
             fixtureName:    fixtureName,
             testName:       testName,
             activeWindowId: activeWindowId,
+            windowId:       windowId,
         },
         {
-            selectorTimeout:            selectorTimeout,
-            pageLoadTimeout:            pageLoadTimeout,
-            childWindowReadyTimeout:    childWindowReadyTimeout,
-            skipJsErrors:               skipJsErrors,
-            dialogHandler:              dialogHandler,
-            retryTestPages:             retryTestPages,
-            speed:                      speed,
-            canUseDefaultWindowActions: canUseDefaultWindowActions,
-            nativeAutomation:                  nativeAutomation,
+            selectorTimeout:             selectorTimeout,
+            pageLoadTimeout:             pageLoadTimeout,
+            childWindowReadyTimeout:     childWindowReadyTimeout,
+            skipJsErrors:                skipJsErrors,
+            dialogHandler:               dialogHandler,
+            retryTestPages:              retryTestPages,
+            speed:                       speed,
+            canUseDefaultWindowActions:  canUseDefaultWindowActions,
+            nativeAutomation:            nativeAutomation,
+            experimentalMultipleWindows: experimentalMultipleWindows,
         }
     );
 
diff --git a/src/configuration/testcafe-configuration.ts b/src/configuration/testcafe-configuration.ts
index 46a0d41a7..3c8fbdf76 100644
--- a/src/configuration/testcafe-configuration.ts
+++ b/src/configuration/testcafe-configuration.ts
@@ -22,6 +22,7 @@ import {
     DEFAULT_SOURCE_DIRECTORIES,
     DEFAULT_SPEED_VALUE,
     DEFAULT_TIMEOUT,
+    DEFAULT_EXPERIMENTAL_MULTIPLE_WINDOWS,
     getDefaultCompilerOptions,
 } from './default-values';
 
@@ -69,6 +70,7 @@ const OPTION_INIT_FLAG_NAMES = [
     OPTION_NAMES.disableHttp2,
     OPTION_NAMES.disableNativeAutomation,
     OPTION_NAMES.disableCrossDomain,
+    OPTION_NAMES.experimentalMultipleWindows,
 ];
 
 export interface TestCafeStartOptions {
@@ -270,6 +272,7 @@ export default class TestCafeConfiguration extends Configuration {
         this._ensureOptionWithValue(OPTION_NAMES.retryTestPages, DEFAULT_RETRY_TEST_PAGES, OptionSource.Configuration);
         this._ensureOptionWithValue(OPTION_NAMES.disableHttp2, DEFAULT_DISABLE_HTTP2, OptionSource.Configuration);
         this._ensureOptionWithValue(OPTION_NAMES.disableNativeAutomation, DEFAULT_DISABLE_NATIVE_AUTOMATION, OptionSource.Configuration);
+        this._ensureOptionWithValue(OPTION_NAMES.experimentalMultipleWindows, DEFAULT_EXPERIMENTAL_MULTIPLE_WINDOWS, OptionSource.Configuration);
         this._ensureOptionWithValue(OPTION_NAMES.disableCrossDomain, DEFAULT_DISABLE_CROSS_DOMAIN, OptionSource.Configuration);
 
         this._ensureScreenshotOptions();
diff --git a/src/native-automation/request-pipeline/test-run-bridge.ts b/src/native-automation/request-pipeline/test-run-bridge.ts
index b6d49a2f2..b96d03185 100644
--- a/src/native-automation/request-pipeline/test-run-bridge.ts
+++ b/src/native-automation/request-pipeline/test-run-bridge.ts
@@ -5,8 +5,10 @@ import { InjectableResourcesOptions } from '../types';
 
 export default class TestRunBridge {
     private readonly _browserId: string;
-    public constructor (browserId: string) {
+    private readonly _windowId: string;
+    public constructor (browserId: string, windowId: string) {
         this._browserId = browserId;
+        this._windowId = windowId;
     }
 
     public getBrowserConnection (): BrowserConnection {
@@ -53,7 +55,7 @@ export default class TestRunBridge {
     public async getTaskScript ({ isIframe }: InjectableResourcesOptions): Promise<string> {
         const browserConnection = this.getBrowserConnection();
         const proxy             = browserConnection.browserConnectionGateway.proxy;
-        const windowId          = browserConnection.activeWindowId;
+        const windowId          = this._windowId;
 
         // @ts-ignore
         return await this.getCurrentTestRun().session.getTaskScript({
diff --git a/src/runner/test-run-controller.ts b/src/runner/test-run-controller.ts
index 7dbff378a..c18d1c539 100644
--- a/src/runner/test-run-controller.ts
+++ b/src/runner/test-run-controller.ts
@@ -102,6 +102,9 @@ export default class TestRunController extends AsyncEventEmitter {
             folderName:       this.testRun.id,
         });
 
+        if (this.isNativeAutomation)
+            connection.resetActiveWindowId();
+
         await this.testRun.initialize();
 
         this._screenshots.addTestRun(this.test, this.testRun);
diff --git a/src/test-run/index.ts b/src/test-run/index.ts
index 24b80893a..45bd0d0b6 100644
--- a/src/test-run/index.ts
+++ b/src/test-run/index.ts
@@ -133,7 +133,7 @@ import {
 } from './role-provider';
 
 import NativeAutomationRequestPipeline from '../native-automation/request-pipeline';
-import NativeAutomation from '../native-automation';
+import { NativeAutomationBase } from '../native-automation';
 import ReportDataLog from '../reporter/report-data-log';
 
 const lazyRequire                 = require('import-lazy')(require);
@@ -269,6 +269,7 @@ export default class TestRun extends AsyncEventEmitter {
     private readonly _requestHookEventProvider: RequestHookEventProvider;
     private readonly _roleProvider: RoleProvider;
     public readonly isNativeAutomation: boolean;
+    public readonly isExperimentalMultipleWindows: boolean;
 
     public constructor ({ test, browserConnection, screenshotCapturer, globalWarningLog, opts, messageBus, startRunExecutionTime, nativeAutomation }: TestRunInit) {
         super();
@@ -298,7 +299,8 @@ export default class TestRun extends AsyncEventEmitter {
         this.disablePageCaching = test.disablePageCaching || opts.disablePageCaching as boolean;
         this.isNativeAutomation = nativeAutomation;
 
-        this.disableMultipleWindows = opts.disableMultipleWindows as boolean;
+        this.disableMultipleWindows        = opts.disableMultipleWindows as boolean;
+        this.isExperimentalMultipleWindows = opts.experimentalMultipleWindows as boolean;
 
         this.requestTimeout = this._getRequestTimeout(test, opts);
 
@@ -377,7 +379,7 @@ export default class TestRun extends AsyncEventEmitter {
         return this._nativeAutomation.requestPipeline;
     }
 
-    private get _nativeAutomation (): NativeAutomation {
+    private get _nativeAutomation (): NativeAutomationBase {
         return this.browserConnection.getNativeAutomation();
     }
 
@@ -549,7 +551,7 @@ export default class TestRun extends AsyncEventEmitter {
     }
 
     // Hammerhead payload
-    public async getPayloadScript (): Promise<string> {
+    public async getPayloadScript (windowId?: string): Promise<string> {
         this.fileDownloadingHandled               = false;
         this.resolveWaitForFileDownloadingPromise = null;
 
@@ -559,11 +561,13 @@ export default class TestRun extends AsyncEventEmitter {
             testRunId:                                               JSON.stringify(this.session.id),
             browserId:                                               JSON.stringify(this.browserConnection.id),
             activeWindowId:                                          JSON.stringify(this.activeWindowId),
+            windowId:                                                JSON.stringify(windowId || ''),
             browserHeartbeatRelativeUrl:                             JSON.stringify(this.browserConnection.heartbeatRelativeUrl),
             browserStatusRelativeUrl:                                JSON.stringify(this.browserConnection.statusRelativeUrl),
             browserStatusDoneRelativeUrl:                            JSON.stringify(this.browserConnection.statusDoneRelativeUrl),
             browserIdleRelativeUrl:                                  JSON.stringify(this.browserConnection.idleRelativeUrl),
             browserActiveWindowIdUrl:                                JSON.stringify(this.browserConnection.activeWindowIdUrl),
+            browserEnsureWindowInNativeAutomationUrl:                JSON.stringify(this.browserConnection.ensureWindowInNativeAutomationUrl),
             browserCloseWindowUrl:                                   JSON.stringify(this.browserConnection.closeWindowUrl),
             browserOpenFileProtocolRelativeUrl:                      JSON.stringify(this.browserConnection.openFileProtocolRelativeUrl),
             browserDispatchNativeAutomationEventRelativeUrl:         JSON.stringify(this.browserConnection.dispatchNativeAutomationEventRelativeUrl),
@@ -581,11 +585,15 @@ export default class TestRun extends AsyncEventEmitter {
             dialogHandler:                                           JSON.stringify(this.activeDialogHandler),
             canUseDefaultWindowActions:                              JSON.stringify(await this.browserConnection.canUseDefaultWindowActions()),
             nativeAutomation:                                        this.isNativeAutomation,
+            experimentalMultipleWindows:                             this.isExperimentalMultipleWindows,
             domain:                                                  JSON.stringify(this.browserConnection.browserConnectionGateway.proxy.server1Info.domain),
         });
     }
 
     public async getIframePayloadScript (): Promise<string> {
+        const browserEnsureWindowInNativeAutomationUrl = JSON.stringify(this.browserConnection.ensureWindowInNativeAutomationUrl);
+        const experimentalMultipleWindows = this.isExperimentalMultipleWindows;
+
         return Mustache.render(IFRAME_TEST_RUN_TEMPLATE, {
             testRunId:        JSON.stringify(this.session.id),
             selectorTimeout:  this.opts.selectorTimeout,
@@ -594,6 +602,9 @@ export default class TestRun extends AsyncEventEmitter {
             speed:            this.speed,
             dialogHandler:    JSON.stringify(this.activeDialogHandler),
             nativeAutomation: JSON.stringify(this.isNativeAutomation),
+            domain:           JSON.stringify(this.browserConnection.browserConnectionGateway.proxy.server1Info.domain),
+            browserEnsureWindowInNativeAutomationUrl,
+            experimentalMultipleWindows,
         });
     }
 
diff --git a/src/test-run/session-controller.js b/src/test-run/session-controller.js
index 9734b4d14..37dad02b0 100644
--- a/src/test-run/session-controller.js
+++ b/src/test-run/session-controller.js
@@ -26,8 +26,8 @@ export default class SessionController extends Session {
     }
 
     // Hammerhead payload
-    async getPayloadScript () {
-        return this.currentTestRun.getPayloadScript();
+    async getPayloadScript (windowId) {
+        return this.currentTestRun.getPayloadScript(windowId);
     }
 
     async getIframePayloadScript () {
diff --git a/test/functional/fixtures/multiple-windows/pages/i4760/child.html b/test/functional/fixtures/multiple-windows/pages/i4760/child.html
index 949d66190..c178241a3 100644
--- a/test/functional/fixtures/multiple-windows/pages/i4760/child.html
+++ b/test/functional/fixtures/multiple-windows/pages/i4760/child.html
@@ -13,7 +13,7 @@
         window.opener.postMessage('message', '*');
         window.setTimeout(function () {
             window.close();
-        }, 100);
+        }, 10);
     });
 </script>
 </body>
diff --git a/test/functional/fixtures/multiple-windows/test.js b/test/functional/fixtures/multiple-windows/test.js
index 118939b35..524c009b0 100644
--- a/test/functional/fixtures/multiple-windows/test.js
+++ b/test/functional/fixtures/multiple-windows/test.js
@@ -6,6 +6,7 @@ const { GREEN_PIXEL, RED_PIXEL } = require('../../assertion-helper');
 const { readPngFile }            = require('../../../../lib/utils/promisified-functions');
 const config                     = require('../../config');
 const { createWarningReporter }  = require('../../utils/warning-reporter');
+const { skipInNativeAutomation } = require('../../utils/skip-in');
 
 const SCREENSHOTS_PATH = config.testScreenshotsDir;
 
@@ -21,6 +22,22 @@ async function assertScreenshotColor (fileName, pixel) {
 }
 
 describe('Multiple windows', () => {
+    let origRunTests = null;
+
+    before(() => {
+        origRunTests = global.runTests;
+
+        global.runTests = (fixture, testName, opts = {}) => {
+            opts.experimentalMultipleWindows = true;
+
+            return origRunTests.call(this, fixture, testName, opts);
+        };
+    });
+
+    after(() => {
+        global.runTests = origRunTests;
+    });
+
     describe('Switch to the child window', () => {
         it('Click on link', () => {
             return runTests('testcafe-fixtures/switching-to-child/click-on-link.js', null, { only: 'chrome' });
@@ -81,7 +98,7 @@ describe('Multiple windows', () => {
                 return testCafeInstance.createRunner()
                     .browsers(`chrome:headless`)
                     .src(fullTestPath)
-                    .run({ disableNativeAutomation: true });
+                    .run({ disableNativeAutomation: !config.nativeAutomation, experimentalMultipleWindows: true });
             })
             .then(() => {
                 return testCafeInstance.close();
@@ -110,7 +127,7 @@ describe('Multiple windows', () => {
         return runTests('testcafe-fixtures/debug-synchronization.js', null, { only: 'chrome' });
     });
 
-    it('Should make screenshots of different windows', () => {
+    skipInNativeAutomation('Should make screenshots of different windows', () => {
         return runTests('testcafe-fixtures/features/screenshots.js', null, { setScreenshotPath: true })
             .then(() => {
                 return assertScreenshotColor('0.png', RED_PIXEL);
@@ -140,7 +157,7 @@ describe('Multiple windows', () => {
             });
     });
 
-    it('Should switch to parent and close window if the file was downloaded in separate window', () => {
+    skipInNativeAutomation('Should switch to parent and close window if the file was downloaded in separate window', () => {
         return runTests('testcafe-fixtures/i6242.js', null, { only: 'chrome' });
     });
 
@@ -152,7 +169,7 @@ describe('Multiple windows', () => {
         return runTests('testcafe-fixtures/i6085.js');
     });
 
-    it('Should not hang on close window whide video is recording', () => {
+    skipInNativeAutomation('Should not hang on close window while video is recording', () => {
         return runTests('testcafe-fixtures/i6037.js', '', {
             setVideoPath: true,
         });
@@ -341,7 +358,7 @@ describe('Multiple windows', () => {
     });
 
     describe('Emulation', () => {
-        it('Should resize window when emulating device', async () => {
+        skipInNativeAutomation('Should resize window when emulating device', async () => {
             return createTestCafe('127.0.0.1', 1335, 1336)
                 .then(tc => {
                     testCafeInstance = tc;
@@ -382,15 +399,15 @@ describe('Multiple windows', () => {
                 });
         }
 
-        it('Resize multiple windows', () => {
+        skipInNativeAutomation('Resize multiple windows', () => {
             return runTests('testcafe-fixtures/api/api-test.js', 'Resize multiple windows');
         });
 
-        it('Maximize multiple windows', () => {
+        skipInNativeAutomation('Maximize multiple windows', () => {
             return runTests('testcafe-fixtures/api/api-test.js', 'Maximize multiple windows');
         });
 
-        it('Resize headless', () => {
+        skipInNativeAutomation('Resize headless', () => {
             return runTestsResize('firefox:headless').then(() => {
                 return runTestsResize('chrome:headless');
             });
diff --git a/test/functional/setup.js b/test/functional/setup.js
index 2738fed76..45d973c42 100644
--- a/test/functional/setup.js
+++ b/test/functional/setup.js
@@ -286,6 +286,7 @@ before(function () {
                     disablePageReloads,
                     disableScreenshots,
                     disableMultipleWindows,
+                    experimentalMultipleWindows,
                     pageRequestTimeout,
                     ajaxRequestTimeout,
                     userVariables,
@@ -365,6 +366,7 @@ before(function () {
                         disablePageReloads,
                         disableScreenshots,
                         disableMultipleWindows,
+                        experimentalMultipleWindows,
                         pageRequestTimeout,
                         ajaxRequestTimeout,
                         userVariables,
diff --git a/test/server/cli-argument-parser-test.js b/test/server/cli-argument-parser-test.js
index 3fa018a2e..0879a2808 100644
--- a/test/server/cli-argument-parser-test.js
+++ b/test/server/cli-argument-parser-test.js
@@ -6,7 +6,7 @@ const { find }                    = require('lodash');
 const CliArgumentParser           = require('../../lib/cli/argument-parser');
 const { nanoid }                  = require('nanoid');
 const runOptionNames              = require('../../lib/configuration/run-option-names');
-const shouldMoveOptionToEnd       =  require('../../lib/cli/utils/should-move-option-to-end');
+const shouldMoveOptionToEnd       = require('../../lib/cli/utils/should-move-option-to-end');
 const QUARANTINE_OPTION_NAMES     = require('../../lib/configuration/quarantine-option-names');
 const { SKIP_JS_ERRORS_OPTIONS_OBJECT_OPTION_NAMES } = require('../../lib/configuration/skip-js-errors-option-names');
 
@@ -889,6 +889,7 @@ describe('CLI argument parser', function () {
             { long: '--base-url' },
             { long: '--disable-cross-domain' },
             { long: '--esm' },
+            { long: '--experimental-multiple-windows' },
         ];
 
         const parser  = new CliArgumentParser('');
@@ -904,7 +905,7 @@ describe('CLI argument parser', function () {
             expect(option.short).eql(EXPECTED_OPTIONS[i].short, CHANGE_CLI_WARNING);
         }
 
-        const expectedRunOptionsCount   = 22;
+        const expectedRunOptionsCount   = 23;
         const expectedOtherOptionsCount = 37;
         const otherOptionsCount         = options.length - expectedRunOptionsCount;