UNPKG

chrome-devtools-frontend

Version:
218 lines (192 loc) • 7.27 kB
// Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../../core/common/common.js'; import * as Platform from '../../../core/platform/platform.js'; import * as SDK from '../../../core/sdk/sdk.js'; // eslint-disable-next-line rulesdir/es_modules_import import type * as Protocol from '../../../generated/protocol.js'; import type * as LitHtml from '../../../ui/lit-html/lit-html.js'; import * as Models from '../models/models.js'; import * as Util from '../util/util.js'; const BINDING_NAME = 'captureSelectors'; export class SelectorPickedEvent extends Event { static readonly eventName = 'selectorpicked'; data: Models.Schema.StepWithSelectors&Pick<Models.Schema.ClickAttributes, 'offsetX'|'offsetY'>; constructor( data: Models.Schema.StepWithSelectors&Pick<Models.Schema.ClickAttributes, 'offsetX'|'offsetY'>, ) { super(SelectorPickedEvent.eventName, {bubbles: true, composed: true}); this.data = data; } } export class RequestSelectorAttributeEvent extends Event { static readonly eventName = 'requestselectorattribute'; send: (attribute?: string) => void; constructor(send: (attribute?: string) => void) { super(RequestSelectorAttributeEvent.eventName, { bubbles: true, composed: true, }); this.send = send; } } export class SelectorPicker implements SDK.TargetManager.Observer { static get #targetManager(): SDK.TargetManager.TargetManager { return SDK.TargetManager.TargetManager.instance(); } readonly #element: LitHtml.LitElement; #selectorAttribute?: string; readonly #activeMutex = new Common.Mutex.Mutex(); active = false; constructor(element: LitHtml.LitElement) { this.#element = element; } start = (): Promise<void> => { return this.#activeMutex.run(async () => { if (this.active) { return; } this.active = true; this.#selectorAttribute = await new Promise<string|undefined>( (resolve, reject) => { const timeout = setTimeout(reject, 1000); this.#element.dispatchEvent( new RequestSelectorAttributeEvent(attribute => { clearTimeout(timeout); resolve(attribute); }), ); }, ); SelectorPicker.#targetManager.observeTargets(this); this.#element.requestUpdate(); }); }; stop = (): Promise<void> => { return this.#activeMutex.run(async () => { if (!this.active) { return; } this.active = false; SelectorPicker.#targetManager.unobserveTargets(this); SelectorPicker.#targetManager.targets().map(this.targetRemoved.bind(this)); this.#selectorAttribute = undefined; this.#element.requestUpdate(); }); }; toggle = (): Promise<void> => { if (!this.active) { return this.start(); } return this.stop(); }; readonly #targetMutexes = new Map<SDK.Target.Target, Common.Mutex.Mutex>(); targetAdded(target: SDK.Target.Target): void { if (target.type() !== SDK.Target.Type.Frame) { return; } let mutex = this.#targetMutexes.get(target); if (!mutex) { mutex = new Common.Mutex.Mutex(); this.#targetMutexes.set(target, mutex); } void mutex.run(async () => { await this.#addBindings(target); await this.#injectApplicationScript(target); }); } targetRemoved(target: SDK.Target.Target): void { const mutex = this.#targetMutexes.get(target); if (!mutex) { return; } void mutex.run(async () => { try { await this.#injectCleanupScript(target); await this.#removeBindings(target); } catch { } }); } #handleBindingCalledEvent = ( event: Common.EventTarget.EventTargetEvent<Protocol.Runtime.BindingCalledEvent>, ): void => { if (event.data.name !== BINDING_NAME) { return; } const contextId = event.data.executionContextId; const frames = SDK.TargetManager.TargetManager.instance().targets(); const contextTarget = Models.SDKUtils.findTargetByExecutionContext( frames, contextId, ); const frameId = Models.SDKUtils.findFrameIdByExecutionContext( frames, contextId, ); if (!contextTarget || !frameId) { throw new Error( `No execution context found for the binding call + ${ JSON.stringify( event.data, )}`, ); } const model = contextTarget.model(SDK.ResourceTreeModel.ResourceTreeModel); if (!model) { throw new Error( `ResourceTreeModel instance is missing for the target: ${contextTarget.id()}`, ); } const frame = model.frameForId(frameId); if (!frame) { throw new Error('Frame is not found'); } this.#element.dispatchEvent( new SelectorPickedEvent({ ...JSON.parse(event.data.payload), ...Models.SDKUtils.getTargetFrameContext(contextTarget, frame), }), ); void this.stop(); }; readonly #scriptIdentifier = new Map<SDK.Target.Target, Protocol.Page.ScriptIdentifier>(); async #injectApplicationScript(target: SDK.Target.Target): Promise<void> { const injectedScript = await Util.InjectedScript.get(); const script = `${injectedScript};DevToolsRecorder.startSelectorPicker({getAccessibleName, getAccessibleRole}, ${ JSON.stringify(this.#selectorAttribute ? this.#selectorAttribute : undefined)}, ${Util.isDebugBuild})`; const [{identifier}] = await Promise.all([ target.pageAgent().invoke_addScriptToEvaluateOnNewDocument({ source: script, worldName: Util.DEVTOOLS_RECORDER_WORLD_NAME, includeCommandLineAPI: true, }), Models.SDKUtils.evaluateInAllFrames(Util.DEVTOOLS_RECORDER_WORLD_NAME, target, script), ]); this.#scriptIdentifier.set(target, identifier); } async #injectCleanupScript(target: SDK.Target.Target): Promise<void> { const identifier = this.#scriptIdentifier.get(target); Platform.assertNotNullOrUndefined(identifier); this.#scriptIdentifier.delete(target); await target.pageAgent().invoke_removeScriptToEvaluateOnNewDocument({identifier}); const script = 'DevToolsRecorder.stopSelectorPicker()'; await Models.SDKUtils.evaluateInAllFrames(Util.DEVTOOLS_RECORDER_WORLD_NAME, target, script); } async #addBindings(target: SDK.Target.Target): Promise<void> { const model = target.model(SDK.RuntimeModel.RuntimeModel); Platform.assertNotNullOrUndefined(model); model.addEventListener(SDK.RuntimeModel.Events.BindingCalled, this.#handleBindingCalledEvent); await model.addBinding({ name: BINDING_NAME, executionContextName: Util.DEVTOOLS_RECORDER_WORLD_NAME, }); } async #removeBindings(target: SDK.Target.Target): Promise<void> { await target.runtimeAgent().invoke_removeBinding({name: BINDING_NAME}); const model = target.model(SDK.RuntimeModel.RuntimeModel); Platform.assertNotNullOrUndefined(model); model.removeEventListener(SDK.RuntimeModel.Events.BindingCalled, this.#handleBindingCalledEvent); } }