mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-14 13:45:36 +03:00
feat(popups): auto-attach to all pages in Chromium (#1226)
This commit is contained in:
parent
aabdac8380
commit
665888d579
@ -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!;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -2,6 +2,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Popup</title>
|
||||
<script>
|
||||
window.initialUserAgent = navigator.userAgent;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
I am a popup
|
||||
|
@ -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([
|
||||
|
Loading…
Reference in New Issue
Block a user