From 8a9048c2b505e7fb2586692c15154df202c0ca9d Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 19 Feb 2021 07:25:08 -0800 Subject: [PATCH] feat(inspector): selector input (#5502) --- src/server/supplements/injected/recorder.ts | 16 +----- .../supplements/injected/selectorGenerator.ts | 17 ++++-- .../supplements/recorder/recorderApp.ts | 7 +++ .../supplements/recorder/recorderTypes.ts | 2 +- src/server/supplements/recorderSupplement.ts | 31 ++++++++-- src/web/components/splitView.css | 1 + src/web/components/splitView.tsx | 17 +++--- src/web/components/toolbar.css | 20 ++++++- src/web/recorder/callLog.css | 11 ---- src/web/recorder/callLog.tsx | 57 +++++++++---------- src/web/recorder/index.tsx | 5 -- src/web/recorder/main.tsx | 2 +- src/web/recorder/recorder.css | 4 ++ src/web/recorder/recorder.stories.tsx | 30 +++++++++- src/web/recorder/recorder.tsx | 41 +++++++++++-- 15 files changed, 172 insertions(+), 89 deletions(-) diff --git a/src/server/supplements/injected/recorder.ts b/src/server/supplements/injected/recorder.ts index 80bde3de35491..b4f929813375b 100644 --- a/src/server/supplements/injected/recorder.ts +++ b/src/server/supplements/injected/recorder.ts @@ -28,6 +28,7 @@ declare global { _playwrightRecorderCommitAction: () => Promise; _playwrightRecorderState: () => Promise; _playwrightResume: () => Promise; + _playwrightRecorderSetSelector: (selector: string) => Promise; } } @@ -226,10 +227,8 @@ export class Recorder { } private _onClick(event: MouseEvent) { - if (this._mode === 'inspecting') { - if (this._hoveredModel) - copy(this._hoveredModel.selector); - } + if (this._mode === 'inspecting') + window._playwrightRecorderSetSelector(this._hoveredModel ? this._hoveredModel.selector : ''); if (this._shouldIgnoreMouseEvent(event)) return; if (this._actionInProgress(event)) @@ -603,13 +602,4 @@ function removeEventListeners(listeners: (() => void)[]) { listeners.splice(0, listeners.length); } -function copy(text: string) { - const input = html`` as any as HTMLInputElement; - input.value = text; - document.body.appendChild(input); - input.select(); - document.execCommand('copy'); - input.remove(); -} - export default Recorder; diff --git a/src/server/supplements/injected/selectorGenerator.ts b/src/server/supplements/injected/selectorGenerator.ts index 37f72f1801531..5b49373dfa7fa 100644 --- a/src/server/supplements/injected/selectorGenerator.ts +++ b/src/server/supplements/injected/selectorGenerator.ts @@ -27,11 +27,18 @@ const cacheAllowText = new Map(); const cacheDisallowText = new Map(); export function querySelector(injectedScript: InjectedScript, selector: string, ownerDocument: Document): { selector: string, elements: Element[] } { - const parsedSelector = injectedScript.parseSelector(selector); - return { - selector, - elements: injectedScript.querySelectorAll(parsedSelector, ownerDocument) - }; + try { + const parsedSelector = injectedScript.parseSelector(selector); + return { + selector, + elements: injectedScript.querySelectorAll(parsedSelector, ownerDocument) + }; + } catch (e) { + return { + selector, + elements: [], + }; + } } export function generateSelector(injectedScript: InjectedScript, targetElement: Element): { selector: string, elements: Element[] } { diff --git a/src/server/supplements/recorder/recorderApp.ts b/src/server/supplements/recorder/recorderApp.ts index 7359f8ac41aa2..30eccc7ca3b38 100644 --- a/src/server/supplements/recorder/recorderApp.ts +++ b/src/server/supplements/recorder/recorderApp.ts @@ -35,6 +35,7 @@ declare global { playwrightSetMode: (mode: Mode) => void; playwrightSetPaused: (paused: boolean) => void; playwrightSetSources: (sources: Source[]) => void; + playwrightSetSelector: (selector: string, focus?: boolean) => void; playwrightUpdateLogs: (callLogs: CallLog[]) => void; dispatch(data: EventData): Promise; } @@ -151,6 +152,12 @@ export class RecorderApp extends EventEmitter { } } + async setSelector(selector: string, focus?: boolean): Promise { + await this._page.mainFrame()._evaluateExpression(((arg: any) => { + window.playwrightSetSelector(arg.selector, arg.focus); + }).toString(), true, { selector, focus }, 'main').catch(() => {}); + } + async updateCallLogs(callLogs: CallLog[]): Promise { await this._page.mainFrame()._evaluateExpression(((callLogs: CallLog[]) => { window.playwrightUpdateLogs(callLogs); diff --git a/src/server/supplements/recorder/recorderTypes.ts b/src/server/supplements/recorder/recorderTypes.ts index 9d010e5b7d583..6660e995226ad 100644 --- a/src/server/supplements/recorder/recorderTypes.ts +++ b/src/server/supplements/recorder/recorderTypes.ts @@ -19,7 +19,7 @@ import { Point } from '../../../common/types'; export type Mode = 'inspecting' | 'recording' | 'none'; export type EventData = { - event: 'clear' | 'resume' | 'step' | 'pause' | 'setMode'; + event: 'clear' | 'resume' | 'step' | 'pause' | 'setMode' | 'selectorUpdated'; params: any; }; diff --git a/src/server/supplements/recorderSupplement.ts b/src/server/supplements/recorderSupplement.ts index b669df2ad6359..b61fd6504ef12 100644 --- a/src/server/supplements/recorderSupplement.ts +++ b/src/server/supplements/recorderSupplement.ts @@ -45,6 +45,7 @@ export class RecorderSupplement { private _timers = new Set(); private _context: BrowserContext; private _mode: Mode; + private _highlightedSelector = ''; private _recorderApp: RecorderApp | null = null; private _params: channels.BrowserContextRecorderSupplementEnableParams; private _currentCallsMetadata = new Map(); @@ -127,11 +128,11 @@ export class RecorderSupplement { }); recorderApp.on('event', (data: EventData) => { if (data.event === 'setMode') { - this._mode = data.params.mode; - recorderApp.setMode(this._mode); - this._generator.setEnabled(this._mode === 'recording'); - if (this._mode !== 'none') - this._context.pages()[0].bringToFront().catch(() => {}); + this._setMode(data.params.mode); + return; + } + if (data.event === 'selectorUpdated') { + this._highlightedSelector = data.params.selector; return; } if (data.event === 'step') { @@ -191,10 +192,16 @@ export class RecorderSupplement { actionSelector = metadata.params.selector || actionSelector; } } - const uiState: UIState = { mode: this._mode, actionPoint, actionSelector }; + const uiState: UIState = { mode: this._mode, actionPoint, actionSelector: this._highlightedSelector || actionSelector }; return uiState; }); + await this._context.exposeBinding('_playwrightRecorderSetSelector', false, async (_, selector: string) => { + this._setMode('none'); + await this._recorderApp?.setSelector(selector, true); + await this._recorderApp?.bringToFront(); + }); + await this._context.exposeBinding('_playwrightResume', false, () => { this._resume(false).catch(() => {}); }); @@ -216,6 +223,14 @@ export class RecorderSupplement { return result; } + private _setMode(mode: Mode) { + this._mode = mode; + this._recorderApp?.setMode(this._mode); + this._generator.setEnabled(this._mode === 'recording'); + if (this._mode !== 'none') + this._context.pages()[0].bringToFront().catch(() => {}); + } + private async _resume(step: boolean) { this._pauseOnNextStatement = step; this._recorderApp?.setPaused(false); @@ -354,6 +369,10 @@ export class RecorderSupplement { this.updateCallLog([metadata]); if (metadata.method === 'pause' || (this._pauseOnNextStatement && metadata.method === 'goto')) await this.pause(metadata); + if (metadata.params && metadata.params.selector) { + this._highlightedSelector = metadata.params.selector; + await this._recorderApp?.setSelector(this._highlightedSelector); + } } async onAfterCall(metadata: CallMetadata): Promise { diff --git a/src/web/components/splitView.css b/src/web/components/splitView.css index d3a8d1af02600..adc6dc5b6f2f5 100644 --- a/src/web/components/splitView.css +++ b/src/web/components/splitView.css @@ -18,6 +18,7 @@ display: flex; flex: auto; flex-direction: column; + position: relative; } .split-view-main { diff --git a/src/web/components/splitView.tsx b/src/web/components/splitView.tsx index 69202cb42464c..842023a235934 100644 --- a/src/web/components/splitView.tsx +++ b/src/web/components/splitView.tsx @@ -21,25 +21,28 @@ export interface SplitViewProps { sidebarSize: number, } +const kMinSidebarSize = 50; + export const SplitView: React.FC = ({ sidebarSize, children }) => { - let [size, setSize] = React.useState(sidebarSize); - const [resizing, setResizing] = React.useState<{ offsetY: number } | null>(null); - if (size < 50) - size = 50; + let [size, setSize] = React.useState(Math.max(kMinSidebarSize, sidebarSize)); + const [resizing, setResizing] = React.useState<{ offsetY: number, size: number } | null>(null); const childrenArray = React.Children.toArray(children); return
{childrenArray[0]}
{childrenArray[1]}
setResizing({ offsetY: event.clientY - (event.target as HTMLElement).getBoundingClientRect().y })} + onMouseDown={event => setResizing({ offsetY: event.clientY, size })} onMouseUp={() => setResizing(null)} - onMouseMove={event => resizing ? setSize((event.target as HTMLElement).clientHeight - event.clientY + resizing.offsetY) : 0} + onMouseMove={event => { + if (resizing) + setSize(Math.max(kMinSidebarSize, resizing.size - event.clientY + resizing.offsetY)); + }} >
; }; diff --git a/src/web/components/toolbar.css b/src/web/components/toolbar.css index cc1c398dec5d6..3c669c5b984d1 100644 --- a/src/web/components/toolbar.css +++ b/src/web/components/toolbar.css @@ -22,10 +22,28 @@ align-items: center; padding-right: 10px; flex: none; - z-index: 2; + z-index: 2; } .toolbar-linewrap { display: block; flex: auto; } + +.toolbar input { + border: 1px solid #ddd; + padding: 0 10px; + border-radius: 14px; + line-height: 24px; + background: white; + outline: none; + margin-left: 10px; + color: var(--toolbar-color); +} + +.toolbar select { + border: none; + background: none; + outline: none; + color: var(--toolbar-color); +} diff --git a/src/web/recorder/callLog.css b/src/web/recorder/callLog.css index de2693cdd1253..aebf4ee6bb7bf 100644 --- a/src/web/recorder/callLog.css +++ b/src/web/recorder/callLog.css @@ -31,17 +31,6 @@ align-items: center; } -.call-log-header { - color: var(--toolbar-color); - box-shadow: var(--box-shadow); - background-color: var(--toolbar-bg-color); - height: 32px; - display: flex; - align-items: center; - padding: 0 9px; - z-index: 10; -} - .call-log-call { display: flex; flex: none; diff --git a/src/web/recorder/callLog.tsx b/src/web/recorder/callLog.tsx index be98680dd1f8a..d98fe73a83ccb 100644 --- a/src/web/recorder/callLog.tsx +++ b/src/web/recorder/callLog.tsx @@ -33,37 +33,34 @@ export const CallLogView: React.FC = ({ messagesEndRef.current?.scrollIntoView({ block: 'center', inline: 'nearest' }); }, [messagesEndRef]); - return
-
Log
-
- {log.map(callLog => { - const expandOverride = expandOverrides.get(callLog.id); - const isExpanded = typeof expandOverride === 'boolean' ? expandOverride : callLog.status !== 'done'; - return
-
- { - const newOverrides = new Map(expandOverrides); - newOverrides.set(callLog.id, !isExpanded); - setExpandOverrides(newOverrides); - }}> - { callLog.title } - { callLog.params.url ? ({callLog.params.url}) : undefined } - { callLog.params.selector ? ({callLog.params.selector}) : undefined } - - { typeof callLog.duration === 'number' ? — {msToString(callLog.duration)} : undefined} -
- { (isExpanded ? callLog.messages : []).map((message, i) => { - return
- { message.trim() } -
; - })} - { callLog.error ? : undefined } + return
+ {log.map(callLog => { + const expandOverride = expandOverrides.get(callLog.id); + const isExpanded = typeof expandOverride === 'boolean' ? expandOverride : callLog.status !== 'done'; + return
+
+ { + const newOverrides = new Map(expandOverrides); + newOverrides.set(callLog.id, !isExpanded); + setExpandOverrides(newOverrides); + }}> + { callLog.title } + { callLog.params.url ? ({callLog.params.url}) : undefined } + { callLog.params.selector ? ({callLog.params.selector}) : undefined } + + { typeof callLog.duration === 'number' ? — {msToString(callLog.duration)} : undefined}
- })} -
-
+ { (isExpanded ? callLog.messages : []).map((message, i) => { + return
+ { message.trim() } +
; + })} + { callLog.error ? : undefined } +
+ })} +
; }; diff --git a/src/web/recorder/index.tsx b/src/web/recorder/index.tsx index 8f7b5f2b4297e..d82790f2c3d91 100644 --- a/src/web/recorder/index.tsx +++ b/src/web/recorder/index.tsx @@ -21,11 +21,6 @@ import { applyTheme } from '../theme'; import '../common.css'; import { Main } from './main'; -declare global { - interface Window { - } -} - (async () => { applyTheme(); ReactDOM.render(
, document.querySelector('#root')); diff --git a/src/web/recorder/main.tsx b/src/web/recorder/main.tsx index b0b631a8ce49f..e46e5b0b6c977 100644 --- a/src/web/recorder/main.tsx +++ b/src/web/recorder/main.tsx @@ -25,7 +25,6 @@ declare global { playwrightSetPaused: (paused: boolean) => void; playwrightSetSources: (sources: Source[]) => void; playwrightUpdateLogs: (callLogs: CallLog[]) => void; - dispatch(data: any): Promise; playwrightSourcesEchoForTest: Source[]; } } @@ -36,6 +35,7 @@ export const Main: React.FC = ({ const [paused, setPaused] = React.useState(false); const [log, setLog] = React.useState(new Map()); const [mode, setMode] = React.useState('none'); + const [selector, setSelector] = React.useState(''); window.playwrightSetMode = setMode; window.playwrightSetSources = setSources; diff --git a/src/web/recorder/recorder.css b/src/web/recorder/recorder.css index bb26da5c3873b..c2e06b3824171 100644 --- a/src/web/recorder/recorder.css +++ b/src/web/recorder/recorder.css @@ -55,3 +55,7 @@ .recorder .toolbar-button:not([disabled]):hover .codicon-debug-step-over { color: #41ca1e; } + +.recorder .selector-input { + flex: auto; +} diff --git a/src/web/recorder/recorder.stories.tsx b/src/web/recorder/recorder.stories.tsx index 9c67ee5948063..8b580f78988f6 100644 --- a/src/web/recorder/recorder.stories.tsx +++ b/src/web/recorder/recorder.stories.tsx @@ -46,7 +46,7 @@ OneSource.args = { file: '', text: '// Text One', language: 'javascript', - highlight: [], + highlight: [], }, ], paused: false, @@ -61,13 +61,13 @@ TwoSources.args = { file: '', text: '// Text One', language: 'javascript', - highlight: [], + highlight: [], }, { file: '', text: '// Text Two', language: 'javascript', - highlight: [], + highlight: [], }, ], paused: false, @@ -83,3 +83,27 @@ WithLog.args = { log: exampleCallLog(), mode: 'none' }; + +export const Inspecting = Template.bind({}); +Inspecting.args = { + sources: [], + paused: false, + log: [], + mode: 'inspecting', + initialSelector: 'text=Find me' +}; + +export const Recording = Template.bind({}); +Recording.args = { + sources: [ + { + file: '', + text: `await page.click('button');\n\nawait page.click('button');\n`, + language: 'javascript', + highlight: [], + }, + ], + paused: false, + log: [], + mode: 'recording', +}; diff --git a/src/web/recorder/recorder.tsx b/src/web/recorder/recorder.tsx index 21611a7bddeba..5bb764d865809 100644 --- a/src/web/recorder/recorder.tsx +++ b/src/web/recorder/recorder.tsx @@ -26,6 +26,8 @@ import { CallLogView } from './callLog'; declare global { interface Window { playwrightSetFile: (file: string) => void; + playwrightSetSelector: (selector: string, focus?: boolean) => void; + dispatch(data: any): Promise; } } @@ -33,15 +35,24 @@ export interface RecorderProps { sources: Source[], paused: boolean, log: Map, - mode: Mode + mode: Mode, + initialSelector?: string, } export const Recorder: React.FC = ({ sources, paused, log, - mode + mode, + initialSelector, }) => { + const [selector, setSelector] = React.useState(initialSelector || ''); + const [focusSelectorInput, setFocusSelectorInput] = React.useState(false); + window.playwrightSetSelector = (selector: string, focus?: boolean) => { + setSelector(selector); + setFocusSelectorInput(!!focus); + }; + const [f, setFile] = React.useState(); window.playwrightSetFile = setFile; const file = f || sources[0]?.file; @@ -57,14 +68,21 @@ export const Recorder: React.FC = ({ React.useLayoutEffect(() => { messagesEndRef.current?.scrollIntoView({ block: 'center', inline: 'nearest' }); }, [messagesEndRef]); + + const selectorInputRef = React.createRef(); + React.useLayoutEffect(() => { + if (focusSelectorInput && selectorInputRef.current) { + selectorInputRef.current.select(); + selectorInputRef.current.focus(); + setFocusSelectorInput(false); + } + }, [focusSelectorInput, selectorInputRef]); + return
{ window.dispatch({ event: 'setMode', params: { mode: mode === 'recording' ? 'none' : 'recording' }}).catch(() => { }); }}>Record - { - window.dispatch({ event: 'setMode', params: { mode: mode === 'inspecting' ? 'none' : 'inspecting' }}).catch(() => { }); - }}>Explore { copy(source.text); }}> @@ -93,7 +111,18 @@ export const Recorder: React.FC = ({ - +
+ + { + window.dispatch({ event: 'setMode', params: { mode: mode === 'inspecting' ? 'none' : 'inspecting' }}).catch(() => { }); + }}>Explore + { + setSelector(event.target.value); + window.dispatch({ event: 'selectorUpdated', params: { selector: event.target.value } }); + }} /> + + +
; };