@@ -24,6 +24,8 @@ import { buildFullSelector, generateFrameSelector, metadataToCallLog } from './r
2424import { locatorOrSelectorAsSelector } from '../utils/isomorphic/locatorParser' ;
2525import { stringifySelector } from '../utils/isomorphic/selectorParser' ;
2626import { ProgressController } from './progress' ;
27+ import { ManualPromise } from '../utils/isomorphic/manualPromise' ;
28+
2729import { RecorderSignalProcessor } from './recorder/recorderSignalProcessor' ;
2830import * as rawRecorderSource from './../generated/pollingRecorderSource' ;
2931import { eventsHelper , monotonicTime } from './../utils' ;
@@ -35,6 +37,7 @@ import type { Language } from './codegen/types';
3537import type { CallMetadata , InstrumentationListener , SdkObject } from './instrumentation' ;
3638import type { Point } from '../utils/isomorphic/types' ;
3739import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot' ;
40+ import type { Progress } from './progress' ;
3841import type * as channels from '@protocol/channels' ;
3942import type * as actions from '@recorder/actions' ;
4043import type { CallLog , CallLogStatus , ElementInfo , Mode , OverlayState , Source , UIState } from '@recorder/recorderTypes' ;
@@ -262,6 +265,38 @@ export class Recorder extends EventEmitter<RecorderEventMap> implements Instrume
262265 this . _refreshOverlay ( ) ;
263266 }
264267
268+ async pickLocator ( progress : Progress ) : Promise < string > {
269+ const selectorPromise = new ManualPromise < string > ( ) ;
270+ let recorderChangedState = false ;
271+ const onElementPicked = ( elementInfo : ElementInfo ) => {
272+ selectorPromise . resolve ( elementInfo . selector ) ;
273+ } ;
274+ const onModeChanged = ( ) => {
275+ recorderChangedState = true ;
276+ selectorPromise . reject ( new Error ( 'Locator picking was cancelled' ) ) ;
277+ } ;
278+ const onContextClosed = ( ) => {
279+ recorderChangedState = true ;
280+ selectorPromise . reject ( new Error ( 'Context was closed' ) ) ;
281+ } ;
282+ // Register listeners after setMode() to avoid consuming the ModeChanged
283+ // event that fires synchronously from our own setMode('inspecting') call.
284+ this . setMode ( 'inspecting' ) ;
285+ const listeners : RegisteredListener [ ] = [
286+ eventsHelper . addEventListener ( this , RecorderEvent . ElementPicked , onElementPicked ) ,
287+ eventsHelper . addEventListener ( this , RecorderEvent . ModeChanged , onModeChanged ) ,
288+ eventsHelper . addEventListener ( this , RecorderEvent . ContextClosed , onContextClosed ) ,
289+ ] ;
290+ try {
291+ return await progress . race ( selectorPromise ) ;
292+ } finally {
293+ // Remove listeners before setMode('none') to avoid triggering onModeChanged.
294+ eventsHelper . removeEventListeners ( listeners ) ;
295+ if ( ! recorderChangedState )
296+ this . setMode ( 'none' ) ;
297+ }
298+ }
299+
265300 url ( ) : string | undefined {
266301 const page = this . _context . pages ( ) [ 0 ] ;
267302 return page ?. mainFrame ( ) . url ( ) ;
0 commit comments