feat(popups): auto-attach to all pages in Chromium (#1226)

This commit is contained in:
Yury Semikhatsky 2020-03-05 10:45:32 -08:00 committed by GitHub
parent aabdac8380
commit 665888d579
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 128 additions and 80 deletions

View File

@ -43,10 +43,28 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
private _tracingPath: string | null = '';
private _tracingClient: CRSession | undefined;
static async connect(transport: ConnectionTransport, slowMo?: number): Promise<CRBrowser> {
static async connect(transport: ConnectionTransport, isPersistent: boolean, slowMo?: number): Promise<CRBrowser> {
const connection = new CRConnection(SlowMoTransport.wrap(transport, slowMo));
const browser = new CRBrowser(connection);
await connection.rootSession.send('Target.setDiscoverTargets', { discover: true });
const session = connection.rootSession;
const promises = [
session.send('Target.setDiscoverTargets', { discover: true }),
session.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }),
];
const existingPageAttachPromises: Promise<any>[] = [];
if (isPersistent) {
// First page and background pages in the persistent context are created automatically
// and may be initialized before we enable auto-attach.
function attachToExistingPage({targetInfo}: Protocol.Target.targetCreatedPayload) {
if (!CRTarget.isPageType(targetInfo.type))
return;
existingPageAttachPromises.push(session.send('Target.attachToTarget', {targetId: targetInfo.targetId, flatten: true}));
}
session.on('Target.targetCreated', attachToExistingPage);
Promise.all(promises).then(() => session.off('Target.targetCreated', attachToExistingPage)).catch(debugError);
}
await Promise.all(promises);
await Promise.all(existingPageAttachPromises);
return browser;
}
@ -64,6 +82,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
this._client.on('Target.targetCreated', this._targetCreated.bind(this));
this._client.on('Target.targetDestroyed', this._targetDestroyed.bind(this));
this._client.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this));
this._client.on('Target.attachedToTarget', this._onAttachedToTarget.bind(this));
}
async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
@ -83,14 +102,20 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
return createPageInNewContext(this, options);
}
async _targetCreated(event: Protocol.Target.targetCreatedPayload) {
const targetInfo = event.targetInfo;
async _onAttachedToTarget(event: Protocol.Target.attachedToTargetPayload) {
if (!CRTarget.isPageType(event.targetInfo.type))
return;
const target = this._targets.get(event.targetInfo.targetId);
const session = this._connection.session(event.sessionId)!;
await target!.initializePageSession(session).catch(debugError);
}
async _targetCreated({targetInfo}: Protocol.Target.targetCreatedPayload) {
const {browserContextId} = targetInfo;
const context = (browserContextId && this._contexts.has(browserContextId)) ? this._contexts.get(browserContextId)! : this._defaultContext;
const target = new CRTarget(this, targetInfo, context, () => this._connection.createSession(targetInfo));
assert(!this._targets.has(event.targetInfo.targetId), 'Target should not exist before targetCreated');
this._targets.set(event.targetInfo.targetId, target);
assert(!this._targets.has(targetInfo.targetId), 'Target should not exist before targetCreated');
this._targets.set(targetInfo.targetId, target);
try {
switch (targetInfo.type) {
@ -120,7 +145,6 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
async _targetDestroyed(event: { targetId: string; }) {
const target = this._targets.get(event.targetId)!;
target._initializedCallback(false);
this._targets.delete(event.targetId);
target._didClose();
}
@ -136,7 +160,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
}
_allTargets(): CRTarget[] {
return Array.from(this._targets.values()).filter(target => target._isInitialized);
return Array.from(this._targets.values());
}
async close() {
@ -252,7 +276,6 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
assertBrowserContextIsNotOwned(this);
const { targetId } = await this._browser._client.send('Target.createTarget', { url: 'about:blank', browserContextId: this._browserContextId || undefined });
const target = this._browser._targets.get(targetId)!;
assert(await target._initializedPromise, 'Failed to create target for page');
const page = await target.page();
return page!;
}

View File

@ -68,10 +68,9 @@ export class CRPage implements PageDelegate {
}
async initialize() {
const [, { frameTree }] = await Promise.all([
const promises: Promise<any>[] = [
this._client.send('Page.enable'),
this._client.send('Page.getFrameTree'),
] as const);
this._client.send('Page.getFrameTree').then(({frameTree}) => {
this._handleFrameTree(frameTree);
this._eventListeners = [
helper.addEventListener(this._client, 'Inspector.targetCrashed', event => this._onTargetCrashed()),
@ -94,7 +93,7 @@ export class CRPage implements PageDelegate {
helper.addEventListener(this._client, 'Target.attachedToTarget', event => this._onAttachedToTarget(event)),
helper.addEventListener(this._client, 'Target.detachedFromTarget', event => this._onDetachedFromTarget(event)),
];
const promises: Promise<any>[] = [
}),
this._client.send('Log.enable', {}),
this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
this._client.send('Runtime.enable', {}).then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)),
@ -126,6 +125,7 @@ export class CRPage implements PageDelegate {
promises.push(this._initBinding(binding));
for (const source of this._browserContext._evaluateOnNewDocumentSources)
promises.push(this.evaluateOnNewDocument(source));
promises.push(this._client.send('Runtime.runIfWaitingForDebugger'));
await Promise.all(promises);
}

View File

@ -32,17 +32,20 @@ export class CRTarget {
private readonly _browserContext: CRBrowserContext;
readonly _targetId: string;
readonly sessionFactory: () => Promise<CRSession>;
private _pagePromiseFulfill: ((page: Page) => void) | null = null;
private _pagePromiseReject: ((error: Error) => void) | null = null;
private _pagePromise: Promise<Page> | null = null;
_crPage: CRPage | null = null;
private _workerPromise: Promise<Worker> | null = null;
readonly _initializedPromise: Promise<boolean>;
_initializedCallback: (success: boolean) => void = () => {};
_isInitialized: boolean;
static fromPage(page: Page): CRTarget {
return (page as any)[targetSymbol];
}
static isPageType(type: string): boolean {
return type === 'page' || type === 'background_page';
}
constructor(
browser: CRBrowser,
targetInfo: Protocol.Target.TargetInfo,
@ -53,22 +56,12 @@ export class CRTarget {
this._browserContext = browserContext;
this._targetId = targetInfo.targetId;
this.sessionFactory = sessionFactory;
this._initializedPromise = new Promise(fulfill => this._initializedCallback = fulfill).then(async success => {
if (!success)
return false;
const opener = this.opener();
if (!opener || !opener._pagePromise || this.type() !== 'page')
return true;
const openerPage = await opener._pagePromise;
if (!openerPage.listenerCount(Events.Page.Popup))
return true;
const popupPage = await this.page();
openerPage.emit(Events.Page.Popup, popupPage);
return true;
if (CRTarget.isPageType(targetInfo.type)) {
this._pagePromise = new Promise<Page>((fulfill, reject) => {
this._pagePromiseFulfill = fulfill;
this._pagePromiseReject = reject;
});
this._isInitialized = this._targetInfo.type !== 'page' || this._targetInfo.url !== '';
if (this._isInitialized)
this._initializedCallback(true);
}
}
_didClose() {
@ -77,17 +70,30 @@ export class CRTarget {
}
async page(): Promise<Page | null> {
if ((this._targetInfo.type === 'page' || this._targetInfo.type === 'background_page') && !this._pagePromise) {
this._pagePromise = this.sessionFactory().then(async client => {
this._crPage = new CRPage(client, this._browser, this._browserContext);
return this._pagePromise;
}
async initializePageSession(session: CRSession) {
this._crPage = new CRPage(session, this._browser, this._browserContext);
const page = this._crPage.page();
(page as any)[targetSymbol] = this;
client.once(CRSessionEvents.Disconnected, () => page._didDisconnect());
session.once(CRSessionEvents.Disconnected, () => page._didDisconnect());
try {
await this._crPage.initialize();
return page;
});
this._pagePromiseFulfill!(page);
} catch (error) {
this._pagePromiseReject!(error);
}
return this._pagePromise;
if (this.type() !== 'page')
return;
const opener = this.opener();
if (!opener)
return;
const openerPage = await opener.page();
if (!openerPage)
return;
openerPage.emit(Events.Page.Popup, page);
}
async serviceWorker(): Promise<Worker | null> {
@ -132,11 +138,5 @@ export class CRTarget {
_targetInfoChanged(targetInfo: Protocol.Target.TargetInfo) {
this._targetInfo = targetInfo;
if (!this._isInitialized && (this._targetInfo.type !== 'page' || this._targetInfo.url !== '')) {
this._isInitialized = true;
this._initializedCallback(true);
return;
}
}
}

View File

@ -55,7 +55,7 @@ export class Chromium implements BrowserType {
if (options && (options as any).userDataDir)
throw new Error('userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistent` instead');
const { browserServer, transport } = await this._launchServer(options, 'local');
const browser = await CRBrowser.connect(transport!, options && options.slowMo);
const browser = await CRBrowser.connect(transport!, false, options && options.slowMo);
// Hack: for typical launch scenario, ensure that close waits for actual process termination.
browser.close = () => browserServer.close();
(browser as any)['__server__'] = browserServer;
@ -69,7 +69,7 @@ export class Chromium implements BrowserType {
async launchPersistent(userDataDir: string, options?: LaunchOptions): Promise<BrowserContext> {
const { timeout = 30000 } = options || {};
const { browserServer, transport } = await this._launchServer(options, 'persistent', userDataDir);
const browser = await CRBrowser.connect(transport!);
const browser = await CRBrowser.connect(transport!, true);
const firstPage = new Promise(r => browser._defaultContext.once(Events.BrowserContext.Page, r));
await helper.waitWithTimeout(firstPage, 'first page', timeout);
// Hack: for typical launch scenario, ensure that close waits for actual process termination.
@ -155,7 +155,7 @@ export class Chromium implements BrowserType {
async connect(options: ConnectOptions): Promise<CRBrowser> {
return await platform.connectToWebsocket(options.wsEndpoint, transport => {
return CRBrowser.connect(transport, options.slowMo);
return CRBrowser.connect(transport, false, options.slowMo);
});
}

View File

@ -23,7 +23,7 @@ const connect = {
chromium: {
connect: async (url: string) => {
return await platform.connectToWebsocket(url, transport => {
return ChromiumBrowser.connect(transport);
return ChromiumBrowser.connect(transport, false);
});
}
},

View File

@ -2,6 +2,9 @@
<html>
<head>
<title>Popup</title>
<script>
window.initialUserAgent = navigator.userAgent;
</script>
</head>
<body>
I am a popup

View File

@ -19,8 +19,30 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
const {it, fit, xit, dit} = testRunner;
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
describe('window.open', function() {
describe('Link navigation', function() {
it.fail(CHROMIUM)('should inherit user agent from browser context', async function({browser, server}) {
const context = await browser.newContext({
userAgent: 'hey'
});
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
await page.setContent('<a target=_blank rel=noopener href="/popup/popup.html">link</a>');
const requestPromise = server.waitForRequest('/popup/popup.html');
const [popup] = await Promise.all([
new Promise(fulfill => context.once('page', async pageEvent => fulfill(await pageEvent.page()))),
page.click('a'),
]);
await popup.waitForLoadState();
const userAgent = await popup.evaluate(() => window.initialUserAgent);
const request = await requestPromise;
await context.close();
expect(userAgent).toBe('hey');
expect(request.headers['user-agent']).toBe('hey');
});
});
describe('window.open', function() {
it('should inherit user agent from browser context', async function({browser, server}) {
const context = await browser.newContext({
userAgent: 'hey'
});
@ -36,7 +58,7 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
expect(userAgent).toBe('hey');
expect(request.headers['user-agent']).toBe('hey');
});
it.fail(CHROMIUM)('should inherit extra headers from browser context', async function({browser, server}) {
it('should inherit extra headers from browser context', async function({browser, server}) {
const context = await browser.newContext({
extraHTTPHeaders: { 'foo': 'bar' },
});
@ -60,7 +82,7 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
await context.close();
expect(online).toBe(false);
});
it.skip(FFOX).fail(CHROMIUM)('should inherit touch support from browser context', async function({browser, server}) {
it.skip(FFOX)('should inherit touch support from browser context', async function({browser, server}) {
const context = await browser.newContext({
viewport: { width: 400, height: 500, isMobile: true }
});
@ -73,7 +95,7 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
await context.close();
expect(hasTouch).toBe(true);
});
it.fail(CHROMIUM)('should inherit viewport size from browser context', async function({browser, server}) {
it('should inherit viewport size from browser context', async function({browser, server}) {
const context = await browser.newContext({
viewport: { width: 400, height: 500 }
});
@ -124,7 +146,7 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
expect(await popup.evaluate(() => !!window.opener)).toBe(true);
await context.close();
});
it.fail(CHROMIUM)('should work with empty url', async({browser}) => {
it('should work with empty url', async({browser}) => {
const context = await browser.newContext();
const page = await context.newPage();
const [popup] = await Promise.all([