From 9e35d0a4a594eeeca6fecf7e9689f68b4caf9946 Mon Sep 17 00:00:00 2001 From: Mikhail Losev Date: Mon, 31 Oct 2022 22:31:42 +0400 Subject: [PATCH] proxyless: initial same-domain iframe support (#7349) --- gulp/constants/functional-test-globs.js | 1 + src/proxyless/request-pipeline/index.ts | 40 +++++++++++++++---- src/proxyless/resource-injector.ts | 10 ++--- .../api/es-next/iframe-switching/test.js | 32 ++++++++------- 4 files changed, 56 insertions(+), 27 deletions(-) diff --git a/gulp/constants/functional-test-globs.js b/gulp/constants/functional-test-globs.js index eb797786c7..45f655e9a6 100644 --- a/gulp/constants/functional-test-globs.js +++ b/gulp/constants/functional-test-globs.js @@ -38,6 +38,7 @@ const PROXYLESS_TESTS_GLOB = [ 'test/functional/fixtures/api/es-next/cookies/test.js', 'test/functional/fixtures/concurrency/test.js', 'test/functional/fixtures/api/es-next/request-hooks/test.js', + 'test/functional/fixtures/api/es-next/iframe-switching/test.js', ]; module.exports = { diff --git a/src/proxyless/request-pipeline/index.ts b/src/proxyless/request-pipeline/index.ts index dc87acaf20..65be750fbc 100644 --- a/src/proxyless/request-pipeline/index.ts +++ b/src/proxyless/request-pipeline/index.ts @@ -4,6 +4,7 @@ import RequestPausedEvent = Protocol.Fetch.RequestPausedEvent; import FrameNavigatedEvent = Protocol.Page.FrameNavigatedEvent; import LoadingFailedEvent = Protocol.Network.LoadingFailedEvent; import ContinueResponseRequest = Protocol.Fetch.ContinueResponseRequest; +import FrameTree = Protocol.Page.FrameTree; import ProxylessRequestHookEventProvider from '../request-hooks/event-provider'; import ResourceInjector from '../resource-injector'; import { convertToHeaderEntries } from '../utils/headers'; @@ -30,6 +31,7 @@ export default class ProxylessRequestPipeline { private _options: ProxylessSetupOptions; private readonly _specialServiceRoutes: SpecialServiceRoutes; private _stopped: boolean; + private _currentFrameTree: FrameTree | null; public constructor (browserId: string, client: ProtocolApi) { this._client = client; @@ -38,6 +40,7 @@ export default class ProxylessRequestPipeline { this._resourceInjector = new ResourceInjector(browserId, this._specialServiceRoutes); this._options = DEFAULT_PROXYLESS_SETUP_OPTIONS; this._stopped = false; + this._currentFrameTree = null; } private _getSpecialServiceRoutes (browserId: string): SpecialServiceRoutes { @@ -74,7 +77,7 @@ export default class ProxylessRequestPipeline { if (pipelineContext.reqOpts.isAjax) await this._resourceInjector.processNonProxiedContent(fulfillInfo, this._client); else - await this._resourceInjector.processHTMLPageContent(fulfillInfo, this._client); + await this._resourceInjector.processHTMLPageContent(fulfillInfo, false, this._client); requestPipelineMockLogger(`Mock request ${event.requestId}`); } @@ -128,12 +131,15 @@ export default class ProxylessRequestPipeline { await this._client.Fetch.continueResponse(continueResponseRequest); } else { - await this._resourceInjector.processHTMLPageContent({ - requestId: event.requestId, - responseHeaders: event.responseHeaders, - responseCode: event.responseStatusCode as number, - body: (resourceInfo.body as Buffer).toString(), - }, this._client); + await this._resourceInjector.processHTMLPageContent( + { + requestId: event.requestId, + responseHeaders: event.responseHeaders, + responseCode: event.responseStatusCode as number, + body: (resourceInfo.body as Buffer).toString(), + }, + this._isIframe(event.frameId), + this._client); } } } @@ -143,6 +149,22 @@ export default class ProxylessRequestPipeline { && !event.frame.parentId; } + private async _updateCurrentFrameTree (): Promise { + // NOTE: Due to CDP restrictions (it hangs), we can't get the frame tree + // right before injecting service scripts. + // So, we are forced tracking frames tree. + const result = await this._client.Page.getFrameTree(); + + this._currentFrameTree = result.frameTree; + } + + private _isIframe (frameId: string): boolean { + if (!this._currentFrameTree) + return false; + + return this._currentFrameTree.frame.id !== frameId; + } + public init (options: ProxylessSetupOptions): void { this._options = options; @@ -168,6 +190,10 @@ export default class ProxylessRequestPipeline { await this._resourceInjector.processAboutBlankPage(event, this._client); }); + this._client.Page.on('frameStartedLoading', async () => { + await this._updateCurrentFrameTree(); + }); + this._client.Network.on('loadingFailed', async (event: LoadingFailedEvent) => { requestPipelineLogger('%l', event); diff --git a/src/proxyless/resource-injector.ts b/src/proxyless/resource-injector.ts index d895b04af7..03dd266351 100644 --- a/src/proxyless/resource-injector.ts +++ b/src/proxyless/resource-injector.ts @@ -40,7 +40,7 @@ export default class ResourceInjector { this._specialServiceRoutes = specialServiceRoutes; } - private async _prepareInjectableResources (): Promise { + private async _prepareInjectableResources (isIframe: boolean): Promise { const browserConnection = BrowserConnection.getById(this._browserId) as BrowserConnection; const proxy = browserConnection.browserConnectionGateway.proxy; const windowId = browserConnection.activeWindowId; @@ -52,10 +52,10 @@ export default class ResourceInjector { const taskScript = await currentTestRun.session.getTaskScript({ referer: '', cookieUrl: '', - isIframe: false, withPayload: true, serverInfo: proxy.server1Info, windowId, + isIframe, }); const injectableResources = { @@ -146,7 +146,7 @@ export default class ResourceInjector { public async processAboutBlankPage (event: FrameNavigatedEvent, client: ProtocolApi): Promise { resourceInjectorLogger('Handle page as about:blank. Origin url: %s', event.frame.url); - const injectableResources = await this._prepareInjectableResources() as PageInjectableResources; + const injectableResources = await this._prepareInjectableResources(false) as PageInjectableResources; const html = injectResources(EMPTY_PAGE_MARKUP, injectableResources); await client.Page.setDocumentContent({ @@ -155,8 +155,8 @@ export default class ResourceInjector { }); } - public async processHTMLPageContent (fulfillRequestInfo: FulfillRequestRequest, client: ProtocolApi): Promise { - const injectableResources = await this._prepareInjectableResources(); + public async processHTMLPageContent (fulfillRequestInfo: FulfillRequestRequest, isIframe: boolean, client: ProtocolApi): Promise { + const injectableResources = await this._prepareInjectableResources(isIframe); // NOTE: an unhandled exception interrupts the test execution, // and we are force to redirect manually to the idle page. diff --git a/test/functional/fixtures/api/es-next/iframe-switching/test.js b/test/functional/fixtures/api/es-next/iframe-switching/test.js index f2f47c5d42..5e1d208635 100644 --- a/test/functional/fixtures/api/es-next/iframe-switching/test.js +++ b/test/functional/fixtures/api/es-next/iframe-switching/test.js @@ -1,5 +1,7 @@ -const expect = require('chai').expect; -const errorInEachBrowserContains = require('../../../../assertion-helper.js').errorInEachBrowserContains; +const { expect } = require('chai'); +const { errorInEachBrowserContains } = require('../../../../assertion-helper.js'); + +const { skipInProxyless, skipDescribeInProxyless } = require('../../../../utils/skip-in'); // NOTE: we set selectorTimeout to a large value in some tests to wait for // an iframe to load on the farm (it is fast locally but can take some time on the farm) @@ -19,61 +21,61 @@ describe('[API] t.switchToIframe(), t.switchToMainWindow()', function () { DEFAULT_RUN_OPTIONS); }); - it('Should switch context between a nested iframe and the main window', function () { + skipInProxyless('Should switch context between a nested iframe and the main window', function () { return runTests('./testcafe-fixtures/iframe-switching-test.js', 'Click on element in a nested iframe', { skip: 'firefox-osx', ...DEFAULT_RUN_OPTIONS, }); }); - it('Should switch context between a shadow iframe and the main window', function () { + skipInProxyless('Should switch context between a shadow iframe and the main window', function () { return runTests('./testcafe-fixtures/iframe-switching-test.js', 'Click on an element in a shadow iframe and return to the main window', { ...DEFAULT_RUN_OPTIONS, skip: ['ie', 'edge'], }); }); - it('Should switch context between a nested shadow iframe and the main window', function () { + skipInProxyless('Should switch context between a nested shadow iframe and the main window', function () { return runTests('./testcafe-fixtures/iframe-switching-test.js', 'Click on element in a nested shadow iframe', { ...DEFAULT_RUN_OPTIONS, skip: ['ie', 'edge'], }); }); - it('Should wait while a target iframe is loaded', function () { + skipInProxyless('Should wait while a target iframe is loaded', function () { return runTests('./testcafe-fixtures/iframe-switching-test.js', 'Click in a slowly loading iframe', DEFAULT_RUN_OPTIONS); }); - it('Should resume execution if an iframe is removed as a result of an action', function () { + skipInProxyless('Should resume execution if an iframe is removed as a result of an action', function () { return runTests('./testcafe-fixtures/iframe-switching-test.js', 'Remove an iframe during execution', DEFAULT_RUN_OPTIONS); }); - it('Should execute an action in an iframe with redirect', function () { + skipInProxyless('Should execute an action in an iframe with redirect', function () { return runTests('./testcafe-fixtures/iframe-switching-test.js', 'Click in an iframe with redirect', DEFAULT_RUN_OPTIONS); }); - it('Should keep context if the page was reloaded', function () { + skipInProxyless('Should keep context if the page was reloaded', function () { return runTests('./testcafe-fixtures/iframe-switching-test.js', 'Reload the main page from an iframe', DEFAULT_RUN_OPTIONS); }); - it('Should correctly switch to the main window context if an iframe was removed from the nested one', function () { + skipInProxyless('Should correctly switch to the main window context if an iframe was removed from the nested one', function () { return runTests('./testcafe-fixtures/iframe-switching-test.js', 'Remove the parent iframe from the nested one', DEFAULT_RUN_OPTIONS); }); - it('Should work in an iframe without src', function () { + skipInProxyless('Should work in an iframe without src', function () { return runTests('./testcafe-fixtures/iframe-switching-test.js', 'Click in an iframe without src', DEFAULT_RUN_OPTIONS); }); - it('Should work in a cross-domain iframe', function () { + skipInProxyless('Should work in a cross-domain iframe', function () { // TODO: fix this test for Safari on BrowserStack return runTests('./testcafe-fixtures/iframe-switching-test.js', 'Click in a cross-domain iframe with redirect', { skip: ['safari', 'firefox-osx'], ...DEFAULT_RUN_OPTIONS }); }); - it('Should work in an iframe with the srcdoc attribute', function () { + skipInProxyless('Should work in an iframe with the srcdoc attribute', function () { return runTests('./testcafe-fixtures/iframe-switching-test.js', 'Click in an iframe with the srcdoc attribute', { skip: 'ie', ...DEFAULT_RUN_OPTIONS }); }); - describe('Unavailable iframe errors', function () { + skipDescribeInProxyless('Unavailable iframe errors', function () { it('Should ensure the iframe element exists before switching to it', function () { return runTests('./testcafe-fixtures/iframe-switching-test.js', 'Switch to a non-existent iframe', { shouldFail: true }) @@ -144,7 +146,7 @@ describe('[API] t.switchToIframe(), t.switchToMainWindow()', function () { }); }); - describe('Page errors handling', function () { + skipDescribeInProxyless('Page errors handling', function () { it('Should fail if an error occurs in a same-domain iframe while an action is being executed', function () { return runTests('./testcafe-fixtures/page-errors-test.js', 'Error in a same-domain iframe', DEFAULT_FAILED_RUN_OPTIONS) .catch(function (errs) {