Skip to content

Commit

Permalink
proxyless: initial same-domain iframe support (#7349)
Browse files Browse the repository at this point in the history
  • Loading branch information
miherlosev committed Oct 31, 2022
1 parent 97063a1 commit 9e35d0a
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 27 deletions.
1 change: 1 addition & 0 deletions gulp/constants/functional-test-globs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
40 changes: 33 additions & 7 deletions src/proxyless/request-pipeline/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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}`);
}
Expand Down Expand Up @@ -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);
}
}
}
Expand All @@ -143,6 +149,22 @@ export default class ProxylessRequestPipeline {
&& !event.frame.parentId;
}

private async _updateCurrentFrameTree (): Promise<void> {
// 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;

Expand All @@ -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);

Expand Down
10 changes: 5 additions & 5 deletions src/proxyless/resource-injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default class ResourceInjector {
this._specialServiceRoutes = specialServiceRoutes;
}

private async _prepareInjectableResources (): Promise<PageInjectableResources | null> {
private async _prepareInjectableResources (isIframe: boolean): Promise<PageInjectableResources | null> {
const browserConnection = BrowserConnection.getById(this._browserId) as BrowserConnection;
const proxy = browserConnection.browserConnectionGateway.proxy;
const windowId = browserConnection.activeWindowId;
Expand All @@ -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 = {
Expand Down Expand Up @@ -146,7 +146,7 @@ export default class ResourceInjector {
public async processAboutBlankPage (event: FrameNavigatedEvent, client: ProtocolApi): Promise<void> {
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({
Expand All @@ -155,8 +155,8 @@ export default class ResourceInjector {
});
}

public async processHTMLPageContent (fulfillRequestInfo: FulfillRequestRequest, client: ProtocolApi): Promise<void> {
const injectableResources = await this._prepareInjectableResources();
public async processHTMLPageContent (fulfillRequestInfo: FulfillRequestRequest, isIframe: boolean, client: ProtocolApi): Promise<void> {
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.
Expand Down
32 changes: 17 additions & 15 deletions test/functional/fixtures/api/es-next/iframe-switching/test.js
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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 })
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 9e35d0a

Please sign in to comment.