Skip to content

Commit cae271e

Browse files
authored
feat(inspector): scope pickLocator mode to the owning page only (microsoft#39631)
1 parent dc12f3f commit cae271e

6 files changed

Lines changed: 65 additions & 41 deletions

File tree

packages/dashboard/src/dashboard.tsx

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const Dashboard: React.FC<{ wsUrl?: string }> = ({ wsUrl }) => {
4444
const [url, setUrl] = React.useState('');
4545
const [frame, setFrame] = React.useState<DashboardChannelEvents['frame']>();
4646
const [showInspector, setShowInspector] = React.useState(false);
47-
const [picking, setPicking] = React.useState(false);
47+
const [pickingTabId, setPickingTabId] = React.useState<string | null>(null);
4848
const [locatorToast, setLocatorToast] = React.useState<{ text: string; timer: ReturnType<typeof setTimeout> }>();
4949

5050
const [channel, setChannel] = React.useState<DashboardClientChannel | undefined>();
@@ -62,7 +62,7 @@ export const Dashboard: React.FC<{ wsUrl?: string }> = ({ wsUrl }) => {
6262
channel.onopen = () => {
6363
setChannel(channel);
6464
setInteractive(false);
65-
setPicking(false);
65+
setPickingTabId(null);
6666
};
6767

6868
channel.on('tabs', params => {
@@ -92,7 +92,7 @@ export const Dashboard: React.FC<{ wsUrl?: string }> = ({ wsUrl }) => {
9292
channel.on('elementPicked', params => {
9393
const locator = asLocator('javascript', params.selector);
9494
navigator.clipboard?.writeText(locator).catch(() => {});
95-
setPicking(false);
95+
setPickingTabId(null);
9696
setLocatorToast(old => {
9797
clearTimeout(old?.timer);
9898
return { text: locator, timer: setTimeout(() => setLocatorToast(undefined), 3000) };
@@ -102,7 +102,7 @@ export const Dashboard: React.FC<{ wsUrl?: string }> = ({ wsUrl }) => {
102102
channel.onclose = () => {
103103
setChannel(undefined);
104104
setInteractive(false);
105-
setPicking(false);
105+
setPickingTabId(null);
106106
setShowInspector(false);
107107
};
108108

@@ -185,10 +185,10 @@ export const Dashboard: React.FC<{ wsUrl?: string }> = ({ wsUrl }) => {
185185
}
186186

187187
function onScreenKeyDown(e: React.KeyboardEvent) {
188-
if (picking && e.key === 'Escape') {
188+
if (pickingTabId !== null && e.key === 'Escape') {
189189
e.preventDefault();
190190
channel?.cancelPickLocator();
191-
setPicking(false);
191+
setPickingTabId(null);
192192
return;
193193
}
194194
if (!interactive)
@@ -216,6 +216,7 @@ export const Dashboard: React.FC<{ wsUrl?: string }> = ({ wsUrl }) => {
216216
}
217217

218218
const selectedTab = tabs?.find(t => t.selected);
219+
const picking = selectedTab?.pageId === pickingTabId;
219220

220221
let overlayText: string | undefined;
221222
if (!channel)
@@ -270,7 +271,7 @@ export const Dashboard: React.FC<{ wsUrl?: string }> = ({ wsUrl }) => {
270271
title='Read-only mode'
271272
onClick={() => {
272273
channel?.cancelPickLocator();
273-
setPicking(false);
274+
setPickingTabId(null);
274275
setShowInspector(false);
275276
setInteractive(false);
276277
}}
@@ -319,15 +320,15 @@ export const Dashboard: React.FC<{ wsUrl?: string }> = ({ wsUrl }) => {
319320
title='Pick locator'
320321
aria-pressed={picking}
321322
disabled={!channel}
322-
onClick={async () => {
323+
onClick={() => {
323324
if (picking) {
324-
await channel?.cancelPickLocator();
325-
setPicking(false);
325+
channel?.cancelPickLocator();
326+
setPickingTabId(null);
326327
} else {
327328
setInteractive(true);
328-
await channel?.pickLocator();
329-
setPicking(true);
329+
setPickingTabId(selectedTab?.pageId ?? null);
330330
screenRef.current?.focus();
331+
channel?.pickLocator();
331332
}
332333
}}
333334
>

packages/playwright-core/src/server/dispatchers/pageDispatcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
354354

355355
async pickLocator(params: channels.PagePickLocatorParams, progress: Progress): Promise<channels.PagePickLocatorResult> {
356356
const recorder = await Recorder.forContext(this._page.browserContext, { omitCallTracking: true, hideToolbar: true });
357-
const selector = await recorder.pickLocator(progress);
357+
const selector = await recorder.pickLocator(progress, this._page);
358358
return { selector };
359359
}
360360

packages/playwright-core/src/server/recorder.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export class Recorder extends EventEmitter<RecorderEventMap> implements Instrume
9494
private _listeners: RegisteredListener[] = [];
9595
private _enabled: boolean = false;
9696
private _callLogs: CallLog[] = [];
97+
private _pickLocatorPage: Page | undefined;
9798

9899
static forContext(context: BrowserContext, params: RecorderParams): Promise<Recorder> {
99100
let recorderPromise = (context as any)[recorderSymbol] as Promise<Recorder>;
@@ -178,8 +179,11 @@ export class Recorder extends EventEmitter<RecorderEventMap> implements Instrume
178179
}
179180
}
180181
}
182+
let mode = this._mode;
183+
if (this._pickLocatorPage && source.page !== this._pickLocatorPage)
184+
mode = 'none';
181185
const uiState: UIState = {
182-
mode: this._mode,
186+
mode,
183187
actionPoint,
184188
actionSelector,
185189
ariaTemplate: this._highlightedElement.ariaTemplate,
@@ -260,12 +264,19 @@ export class Recorder extends EventEmitter<RecorderEventMap> implements Instrume
260264
this.emit(RecorderEvent.ModeChanged, this._mode);
261265
this._setEnabled(this._isRecording());
262266
this._debugger.setMuted(this._isRecording());
263-
if (this._mode !== 'none' && this._mode !== 'standby' && this._context.pages().length === 1)
264-
this._context.pages()[0].bringToFront().catch(() => {});
267+
if (this._mode !== 'none' && this._mode !== 'standby') {
268+
let pageToFocus = this._pickLocatorPage;
269+
if (!pageToFocus && this._context.pages().length === 1)
270+
pageToFocus = this._context.pages()[0];
271+
pageToFocus?.bringToFront().catch(() => {});
272+
}
265273
await this._refreshOverlay();
266274
}
267275

268-
async pickLocator(progress: Progress): Promise<string> {
276+
async pickLocator(progress: Progress, page: Page): Promise<string> {
277+
if (this._mode !== 'none')
278+
await this.setMode('none');
279+
269280
const selectorPromise = new ManualPromise<string>();
270281
let recorderChangedState = false;
271282
const onElementPicked = (elementInfo: ElementInfo) => {
@@ -277,25 +288,22 @@ export class Recorder extends EventEmitter<RecorderEventMap> implements Instrume
277288
recorderChangedState = true;
278289
selectorPromise.reject(new Error('Locator picking was cancelled'));
279290
};
280-
const onContextClosed = () => {
281-
recorderChangedState = true;
282-
selectorPromise.reject(new Error('Context was closed'));
283-
};
284291
const listeners: RegisteredListener[] = [
285292
eventsHelper.addEventListener(this, RecorderEvent.ElementPicked, onElementPicked),
286293
eventsHelper.addEventListener(this, RecorderEvent.ModeChanged, onModeChanged),
287-
eventsHelper.addEventListener(this, RecorderEvent.ContextClosed, onContextClosed),
288294
];
289295
try {
290296
const doPickLocator = async () => {
291297
// Prevent unhandled rejection in case of cancellation during setMode
292298
selectorPromise.catch(() => {});
299+
this._pickLocatorPage = page;
293300
await this.setMode('inspecting');
294301
return await selectorPromise;
295302
};
296303
return await progress.race(doPickLocator());
297304
} finally {
298305
eventsHelper.removeEventListeners(listeners);
306+
this._pickLocatorPage = undefined;
299307
if (!recorderChangedState)
300308
await this.setMode('none');
301309
}

packages/playwright-core/src/server/webkit/wkPage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -694,7 +694,7 @@ export class WKPage implements PageDelegate {
694694
}
695695

696696
async bringToFront(): Promise<void> {
697-
this._pageProxySession.send('Target.activate', {
697+
await this._pageProxySession.send('Target.activate', {
698698
targetId: this._session.sessionId
699699
});
700700
}

packages/playwright-core/src/tools/dashboard/dashboardController.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -241,14 +241,18 @@ export class DashboardConnection implements Transport, DashboardChannel {
241241
const pages = this._context.pages();
242242
if (pages.length === 0)
243243
return [];
244-
const dashboardUrl = await this._dashboardUrl(pages[0]);
245-
return await Promise.all(pages.map(async page => ({
246-
pageId: this._pageId(page),
247-
title: await page.title() || page.url(),
248-
url: page.url(),
249-
selected: page === this.selectedPage,
250-
inspectorUrl: dashboardUrl ? await this._pageInspectorUrl(page, dashboardUrl) : 'data:text/plain,Dashboard only supported in Chromium based browsers',
251-
})));
244+
const devtoolsUrl = await this._devtoolsUrl(pages[0]);
245+
return await Promise.all(pages.map(async page => {
246+
// page.title() throws on navigation.
247+
const title = await page.title().catch(() => undefined) || `Loading ${page.url()}`;
248+
return {
249+
pageId: this._pageId(page),
250+
title,
251+
url: page.url(),
252+
selected: page === this.selectedPage,
253+
inspectorUrl: devtoolsUrl ? await this._pageInspectorUrl(page, devtoolsUrl) : 'data:text/plain,Dashboard only supported in Chromium based browsers',
254+
};
255+
}));
252256
}
253257

254258
pageForId(pageId: string) {
@@ -259,19 +263,19 @@ export class DashboardConnection implements Transport, DashboardChannel {
259263
return (p as any)._guid;
260264
}
261265

262-
private async _dashboardUrl(page: api.Page) {
266+
private async _devtoolsUrl(page: api.Page) {
263267
const cdpPort = (this._browserDescriptor.browser.launchOptions as any).cdpPort;
264268
if (cdpPort)
265-
return new URL(`http://localhost:${cdpPort}/dashboard/`);
269+
return new URL(`http://localhost:${cdpPort}/devtools/`);
266270

267271
const browserRevision = await getBrowserRevision(page);
268272
if (!browserRevision)
269273
return null;
270274
return new URL(`https://chrome-devtools-frontend.appspot.com/serve_rev/${browserRevision}/`);
271275
}
272276

273-
private async _pageInspectorUrl(page: api.Page, dashboardUrl: URL): Promise<string | undefined> {
274-
const inspector = new URL('./devtools_app.html', dashboardUrl);
277+
private async _pageInspectorUrl(page: api.Page, devtoolsUrl: URL): Promise<string | undefined> {
278+
const inspector = new URL('./devtools_app.html', devtoolsUrl);
275279
const cdp = new URL(this._cdpUrl);
276280
cdp.searchParams.set('cdpPageId', this._pageId(page));
277281
inspector.searchParams.set('ws', `${cdp.host}${cdp.pathname}${cdp.search}`);

tests/library/inspector/recorder-api.spec.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,25 @@ test('page.pickLocator should return locator for picked element', async ({ page
167167
});
168168

169169
test('page.cancelPickLocator should cancel ongoing pickLocator', async ({ page }) => {
170-
await page.setContent(`<button>Submit</button>`);
171-
172-
const scriptReady = page.waitForEvent('console', msg => msg.text() === 'Recorder script ready for test');
173170
const pickPromise = page.pickLocator();
174-
await scriptReady;
175-
176171
await Promise.all([
177172
page.cancelPickLocator(),
178-
expect(pickPromise).rejects.toThrow('Locator picking was cancelled'),
173+
expect(pickPromise).rejects.toThrow('Locator picking was cancelled')
179174
]);
180175
});
176+
177+
test('closing page should cancel ongoing pickLocator', async ({ page }) => {
178+
await page.setContent(`<button>Click me</button>`);
179+
const pickPromise = page.pickLocator().catch(e => e.message);
180+
await page.close();
181+
expect(await pickPromise).toContain('Target page, context or browser has been closed');
182+
});
183+
184+
test('page2.pickLocator() should cancel page1.pickLocator()', async ({ page, context }) => {
185+
const pick1Promise = page.pickLocator().catch(e => e.message);
186+
187+
const page2 = await context.newPage();
188+
page2.pickLocator().catch(() => {});
189+
190+
expect(await pick1Promise).toContain('Locator picking was cancelled');
191+
});

0 commit comments

Comments
 (0)