From ffd2e02aa3ba2cbaee8e9de01540b6ab66f1ce3b Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Sat, 4 Nov 2023 21:18:27 -0700 Subject: [PATCH] feat(recorder): various UX fixes (#27967) --- .../src/server/injected/recorder.ts | 245 ++++++++++-------- .../playwright-core/src/server/recorder.ts | 17 ++ .../src/server/recorder/recorderApp.ts | 22 +- packages/recorder/src/recorder.css | 6 +- packages/recorder/src/recorder.tsx | 29 ++- packages/recorder/src/recorderTypes.ts | 3 +- packages/trace-viewer/src/ui/snapshotTab.tsx | 1 + packages/web/src/components/toolbarButton.css | 9 + packages/web/src/components/toolbarButton.tsx | 6 + 9 files changed, 206 insertions(+), 132 deletions(-) diff --git a/packages/playwright-core/src/server/injected/recorder.ts b/packages/playwright-core/src/server/injected/recorder.ts index ac28dbe0be..334e8569a5 100644 --- a/packages/playwright-core/src/server/injected/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder.ts @@ -29,6 +29,7 @@ interface RecorderDelegate { recordAction?(action: actions.Action): Promise; setSelector?(selector: string): Promise; setMode?(mode: Mode): Promise; + setOverlayPosition?(position: { x: number, y: number }): Promise; highlightUpdated?(): void; } @@ -441,7 +442,7 @@ class RecordActionTool implements RecorderTool { class TextAssertionTool implements RecorderTool { private _selectionHighlight: HighlightModel | null = null; - private _inputIsFocused = false; + private _inputHighlight: HighlightModel | null = null; constructor(private _recorder: Recorder) { } @@ -457,36 +458,15 @@ class TextAssertionTool implements RecorderTool { disable() { this._recorder.injectedScript.document.designMode = 'off'; this._selectionHighlight = null; - this._inputIsFocused = false; + this._inputHighlight = null; } onClick(event: MouseEvent) { consumeEvent(event); - const target = this._recorder.deepEventTarget(event); - if (event.detail === 1 && ['INPUT', 'TEXTAREA'].includes(target.nodeName)) { - const highlight = generateSelector(this._recorder.injectedScript, target, { testIdAttributeName: this._recorder.state.testIdAttributeName }); - if (target.nodeName === 'INPUT' && ['checkbox', 'radio'].includes((target as HTMLInputElement).type.toLowerCase())) { - this._recorder.delegate.recordAction?.({ - name: 'assertChecked', - selector: highlight.selector, - signals: [], - // Interestingly, inputElement.checked is reversed inside this event handler. - checked: !(target as HTMLInputElement).checked, - }); - } else { - this._recorder.delegate.recordAction?.({ - name: 'assertValue', - selector: highlight.selector, - signals: [], - value: target.isContentEditable ? target.innerText : (target as HTMLInputElement).value, - }); - } - return; - } - const selection = this._recorder.document.getSelection(); - if (event.detail === 1 && selection && !selection.toString()) { + if (event.detail === 1 && selection && !selection.toString() && !this._inputHighlight) { + const target = this._recorder.deepEventTarget(event); selection.selectAllChildren(target); this._updateSelectionHighlight(); } @@ -496,14 +476,13 @@ class TextAssertionTool implements RecorderTool { const target = this._recorder.deepEventTarget(event); if (['INPUT', 'TEXTAREA'].includes(target.nodeName)) { this._recorder.injectedScript.window.getSelection()?.empty(); - this._selectionHighlight = generateSelector(this._recorder.injectedScript, target, { testIdAttributeName: this._recorder.state.testIdAttributeName }); - this._inputIsFocused = true; - this._recorder.updateHighlight(this._selectionHighlight, true, '#6fdcbd38'); + this._inputHighlight = generateSelector(this._recorder.injectedScript, target, { testIdAttributeName: this._recorder.state.testIdAttributeName }); + this._recorder.updateHighlight(this._inputHighlight, true, '#6fdcbd38'); consumeEvent(event); return; } - this._inputIsFocused = false; + this._inputHighlight = null; this._updateSelectionHighlight(); } @@ -522,13 +501,35 @@ class TextAssertionTool implements RecorderTool { onKeyDown(event: KeyboardEvent) { if (event.key === 'Escape') { this._resetSelectionAndHighlight(); + this._recorder.delegate.setMode?.('recording'); consumeEvent(event); return; } if (event.key === 'Enter') { const selection = this._recorder.document.getSelection(); - if (selection && this._selectionHighlight) { + + if (this._inputHighlight) { + const target = this._inputHighlight.elements[0] as HTMLInputElement; + if (target.nodeName === 'INPUT' && ['checkbox', 'radio'].includes(target.type.toLowerCase())) { + this._recorder.delegate.recordAction?.({ + name: 'assertChecked', + selector: this._inputHighlight.selector, + signals: [], + // Interestingly, inputElement.checked is reversed inside this event handler. + checked: !(target as HTMLInputElement).checked, + }); + this._recorder.delegate.setMode?.('recording'); + } else { + this._recorder.delegate.recordAction?.({ + name: 'assertValue', + selector: this._inputHighlight.selector, + signals: [], + value: target.value, + }); + this._recorder.delegate.setMode?.('recording'); + } + } else if (selection && this._selectionHighlight) { const selectedText = normalizeWhiteSpace(selection.toString()); const fullText = normalizeWhiteSpace(elementText(new Map(), this._selectionHighlight.elements[0]).full); this._recorder.delegate.recordAction?.({ @@ -538,6 +539,7 @@ class TextAssertionTool implements RecorderTool { text: selectedText, substring: fullText !== selectedText, }); + this._recorder.delegate.setMode?.('recording'); this._resetSelectionAndHighlight(); } consumeEvent(event); @@ -561,12 +563,13 @@ class TextAssertionTool implements RecorderTool { private _resetSelectionAndHighlight() { this._selectionHighlight = null; + this._inputHighlight = null; this._recorder.injectedScript.window.getSelection()?.empty(); this._recorder.updateHighlight(null, false); } private _updateSelectionHighlight() { - if (this._inputIsFocused) + if (this._inputHighlight) return; const selection = this._recorder.document.getSelection(); let highlight: HighlightModel | null = null; @@ -586,7 +589,9 @@ class TextAssertionTool implements RecorderTool { class Overlay { private _overlayElement: HTMLElement; - private _tools: Record; + private _recordToggle: HTMLElement; + private _pickLocatorToggle: HTMLElement; + private _assertToggle: HTMLElement; private _position: { x: number, y: number } = { x: 0, y: 0 }; private _dragState: { position: { x: number, y: number }, dragStart: { x: number, y: number } } | undefined; private _measure: { width: number, height: number } = { width: 0, height: 0 }; @@ -603,78 +608,77 @@ class Overlay { max-width: min-content; z-index: 2147483647; background: transparent; - cursor: grab; } x-pw-tools-list { - box-shadow: rgba(0, 0, 0, 0.1) 0px 0.25em 0.5em; + box-shadow: rgba(0, 0, 0, 0.1) 0px 5px 5px; backdrop-filter: blur(5px); background-color: hsla(0 0% 100% / .9); - font-family: 'Dank Mono', 'Operator Mono', Inconsolata, 'Fira Mono', - 'SF Mono', Monaco, 'Droid Sans Mono', 'Source Code Pro', monospace; + font-family: 'Dank Mono', 'Operator Mono', Inconsolata, 'Fira Mono', 'SF Mono', Monaco, 'Droid Sans Mono', 'Source Code Pro', monospace; display: flex; flex-direction: column; - margin: 1em; - padding: 0px; - border-radius: 2em; + margin: 10px; + padding: 3px 0; + border-radius: 17px; + } + + x-pw-drag-handle { + cursor: grab; + height: 2px; + margin: 5px 9px; + border-top: 1px solid rgb(86 86 86 / 90%); + border-bottom: 1px solid rgb(86 86 86 / 90%); + } + x-pw-drag-handle:active { + cursor: grabbing; } x-pw-tool-item { cursor: pointer; - height: 2.25em; - width: 2.25em; - margin: 0.05em 0.25em; - display: inline-flex; - align-items: center; - justify-content: center; - position: relative; + height: 28px; + width: 28px; + margin: 2px 4px; border-radius: 50%; } - x-pw-tool-item:first-child { - margin-top: 0.25em; - } - x-pw-tool-item:last-child { - margin-bottom: 0.25em; - } - x-pw-tool-item:hover { - background-color: hsl(0, 0%, 95%); - } - x-pw-tool-item.active { - background-color: hsl(0, 0%, 100%); + x-pw-tool-item:not(.disabled):hover { + background-color: hsl(0, 0%, 86%); } x-pw-tool-item > div { width: 100%; height: 100%; - background-color: black; -webkit-mask-repeat: no-repeat; -webkit-mask-position: center; -webkit-mask-size: 20px; mask-repeat: no-repeat; mask-position: center; - mask-size: 20px; + mask-size: 16px; + background-color: #3a3a3a; + } + x-pw-tool-item.disabled > div { + background-color: rgba(97, 97, 97, 0.5); + cursor: default; } x-pw-tool-item.active > div { - background-color: #ff4ca5; + background-color: #006ab1; } - x-pw-tool-item.none > div { - /* codicon: close */ - -webkit-mask-image: url("data:image/svg+xml;utf8,"); - mask-image: url("data:image/svg+xml;utf8,"); + x-pw-tool-item.record.active > div { + background-color: #a1260d; } - x-pw-tool-item.inspecting > div { - /* codicon: target */ - -webkit-mask-image: url("data:image/svg+xml;utf8,"); - mask-image: url("data:image/svg+xml;utf8,"); + + x-pw-tool-item.record > div { + /* codicon: circle-large-filled */ + -webkit-mask-image: url("data:image/svg+xml;utf8,"); + mask-image: url("data:image/svg+xml;utf8,"); } - x-pw-tool-item.recording > div { - /* codicon: record */ - -webkit-mask-image: url("data:image/svg+xml;utf8,"); - mask-image: url("data:image/svg+xml;utf8,"); + x-pw-tool-item.pick-locator > div { + /* codicon: inspect */ + -webkit-mask-image: url("data:image/svg+xml;utf8,"); + mask-image: url("data:image/svg+xml;utf8,"); } - x-pw-tool-item.assertingText > div { - /* codicon: text-size */ - -webkit-mask-image: url("data:image/svg+xml;utf8,"); - mask-image: url("data:image/svg+xml;utf8,"); + x-pw-tool-item.assert > div { + /* codicon: check-all */ + -webkit-mask-image: url("data:image/svg+xml;utf8,"); + mask-image: url("data:image/svg+xml;utf8,"); } `; shadow.appendChild(styleElement); @@ -682,34 +686,50 @@ class Overlay { const toolsListElement = document.createElement('x-pw-tools-list'); shadow.appendChild(toolsListElement); - this._tools = { - none: this._createToolElement(toolsListElement, 'none', 'Disable'), - inspecting: this._createToolElement(toolsListElement, 'inspecting', 'Pick locator'), - recording: this._createToolElement(toolsListElement, 'recording', 'Record actions'), - assertingText: this._createToolElement(toolsListElement, 'assertingText', 'Assert text and values'), - }; + this._recordToggle = this._recorder.injectedScript.document.createElement('x-pw-tool-item'); + this._recordToggle.title = 'Record'; + this._recordToggle.classList.add('record'); + this._recordToggle.appendChild(this._recorder.injectedScript.document.createElement('div')); + this._recordToggle.addEventListener('click', () => { + this._recorder.delegate.setMode?.(this._recorder.state.mode === 'none' || this._recorder.state.mode === 'inspecting' ? 'recording' : 'none'); + }); + toolsListElement.appendChild(this._recordToggle); - this._overlayElement.addEventListener('mousedown', event => { + const dragHandle = document.createElement('x-pw-drag-handle'); + dragHandle.addEventListener('mousedown', event => { this._dragState = { position: this._position, dragStart: { x: event.clientX, y: event.clientY } }; }); + toolsListElement.appendChild(dragHandle); + + this._pickLocatorToggle = this._recorder.injectedScript.document.createElement('x-pw-tool-item'); + this._pickLocatorToggle.title = 'Pick locator'; + this._pickLocatorToggle.classList.add('pick-locator'); + this._pickLocatorToggle.appendChild(this._recorder.injectedScript.document.createElement('div')); + this._pickLocatorToggle.addEventListener('click', () => { + const newMode: Record = { + 'inspecting': 'none', + 'none': 'inspecting', + 'recording': 'recording-inspecting', + 'recording-inspecting': 'recording', + 'assertingText': 'recording-inspecting', + }; + this._recorder.delegate.setMode?.(newMode[this._recorder.state.mode]); + }); + toolsListElement.appendChild(this._pickLocatorToggle); + + this._assertToggle = this._recorder.injectedScript.document.createElement('x-pw-tool-item'); + this._assertToggle.title = 'Assert text and values'; + this._assertToggle.classList.add('assert'); + this._assertToggle.appendChild(this._recorder.injectedScript.document.createElement('div')); + this._assertToggle.addEventListener('click', () => { + if (!this._assertToggle.classList.contains('disabled')) + this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingText' ? 'recording' : 'assertingText'); + }); + toolsListElement.appendChild(this._assertToggle); - if (this._recorder.injectedScript.isUnderTest) { - // Most of our tests put elements at the top left, so get out of the way. - this._position = { x: 350, y: 350 }; - } this._updateVisualPosition(); } - private _createToolElement(parent: Element, mode: Mode, title: string) { - const element = this._recorder.injectedScript.document.createElement('x-pw-tool-item'); - element.title = title; - element.classList.add(mode); - element.appendChild(this._recorder.injectedScript.document.createElement('div')); - element.addEventListener('click', () => this._recorder.delegate.setMode?.(mode)); - parent.appendChild(element); - return element; - } - install() { this._recorder.injectedScript.document.documentElement.appendChild(this._overlayElement); this._measure = this._overlayElement.getBoundingClientRect(); @@ -720,8 +740,14 @@ class Overlay { } setUIState(state: UIState) { - for (const [mode, tool] of Object.entries(this._tools)) - tool.classList.toggle('active', state.mode === mode); + this._recordToggle.classList.toggle('active', state.mode === 'recording' || state.mode === 'assertingText' || state.mode === 'recording-inspecting'); + this._pickLocatorToggle.classList.toggle('active', state.mode === 'inspecting' || state.mode === 'recording-inspecting'); + this._assertToggle.classList.toggle('active', state.mode === 'assertingText'); + this._assertToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'inspecting'); + if (this._position.x !== state.overlayPosition.x || this._position.y !== state.overlayPosition.y) { + this._position = state.overlayPosition; + this._updateVisualPosition(); + } } private _updateVisualPosition() { @@ -742,6 +768,7 @@ class Overlay { this._position.x = Math.max(0, Math.min(this._recorder.injectedScript.window.innerWidth - this._measure.width, this._position.x)); this._position.y = Math.max(0, Math.min(this._recorder.injectedScript.window.innerHeight - this._measure.height, this._position.y)); this._updateVisualPosition(); + this._recorder.delegate.setOverlayPosition?.(this._position); consumeEvent(event); return true; } @@ -767,7 +794,7 @@ export class Recorder { private _highlight: Highlight; private _overlay: Overlay | undefined; private _styleElement: HTMLStyleElement; - state: UIState = { mode: 'none', testIdAttributeName: 'data-testid', language: 'javascript' }; + state: UIState = { mode: 'none', testIdAttributeName: 'data-testid', language: 'javascript', overlayPosition: { x: 0, y: 0 } }; readonly document: Document; delegate: RecorderDelegate = {}; @@ -776,10 +803,11 @@ export class Recorder { this.injectedScript = injectedScript; this._highlight = new Highlight(injectedScript); this._tools = { - none: new NoneTool(), - inspecting: new InspectTool(this), - recording: new RecordActionTool(this), - assertingText: new TextAssertionTool(this), + 'none': new NoneTool(), + 'inspecting': new InspectTool(this), + 'recording': new RecordActionTool(this), + 'recording-inspecting': new InspectTool(this), + 'assertingText': new TextAssertionTool(this), }; this._currentTool = this._tools.none; if (injectedScript.window.top === injectedScript.window) { @@ -1074,6 +1102,7 @@ interface Embedder { __pw_recorderState(): Promise; __pw_recorderSetSelector(selector: string): Promise; __pw_recorderSetMode(mode: Mode): Promise; + __pw_recorderSetOverlayPosition(position: { x: number, y: number }): Promise; __pw_refreshOverlay(): void; } @@ -1122,10 +1151,6 @@ export class PollingRecorder implements RecorderDelegate { await this._embedder.__pw_recorderRecordAction(action); } - async __pw_recorderState(): Promise { - return await this._embedder.__pw_recorderState(); - } - async setSelector(selector: string): Promise { await this._embedder.__pw_recorderSetSelector(selector); } @@ -1133,6 +1158,10 @@ export class PollingRecorder implements RecorderDelegate { async setMode(mode: Mode): Promise { await this._embedder.__pw_recorderSetMode(mode); } + + async setOverlayPosition(position: { x: number, y: number }): Promise { + await this._embedder.__pw_recorderSetOverlayPosition(position); + } } export default PollingRecorder; diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index f90645e778..8f46e2995b 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -55,6 +55,7 @@ export class Recorder implements InstrumentationListener { private _context: BrowserContext; private _mode: Mode; private _highlightedSelector = ''; + private _overlayPosition: Point = { x: 0, y: 0 }; private _recorderApp: IRecorderApp | null = null; private _currentCallsMetadata = new Map(); private _recorderSources: Source[] = []; @@ -97,6 +98,11 @@ export class Recorder implements InstrumentationListener { this._handleSIGINT = params.handleSIGINT; context.instrumentation.addListener(this, context); this._currentLanguage = this._contextRecorder.languageName(); + + if (isUnderTest()) { + // Most of our tests put elements at the top left, so get out of the way. + this._overlayPosition = { x: 350, y: 350 }; + } } private static async defaultRecorderAppFactory(recorder: Recorder) { @@ -180,6 +186,7 @@ export class Recorder implements InstrumentationListener { actionSelector, language: this._currentLanguage, testIdAttributeName: this._contextRecorder.testIdAttributeName(), + overlayPosition: this._overlayPosition, }; return uiState; }); @@ -202,6 +209,12 @@ export class Recorder implements InstrumentationListener { this.setMode(mode); }); + await this._context.exposeBinding('__pw_recorderSetOverlayPosition', false, async ({ frame }, position: Point) => { + if (frame.parentFrame()) + return; + this._overlayPosition = position; + }); + await this._context.exposeBinding('__pw_resume', false, () => { this._debugger.resume(false); }); @@ -244,6 +257,10 @@ export class Recorder implements InstrumentationListener { this._debugger.resume(false); } + mode() { + return this._mode; + } + setHighlightedSelector(language: Language, selector: string) { this._highlightedSelector = locatorOrSelectorAsSelector(language, selector, this._context.selectors().testIdAttributeName()); this._refreshOverlay(); diff --git a/packages/playwright-core/src/server/recorder/recorderApp.ts b/packages/playwright-core/src/server/recorder/recorderApp.ts index aef4b999ee..3fb72d9f9e 100644 --- a/packages/playwright-core/src/server/recorder/recorderApp.ts +++ b/packages/playwright-core/src/server/recorder/recorderApp.ts @@ -46,7 +46,7 @@ export interface IRecorderApp extends EventEmitter { setPaused(paused: boolean): Promise; setMode(mode: Mode): Promise; setFileIfNeeded(file: string): Promise; - setSelector(selector: string, focus?: boolean): Promise; + setSelector(selector: string, userGesture?: boolean): Promise; updateCallLogs(callLogs: CallLog[]): Promise; setSources(sources: Source[]): Promise; } @@ -56,7 +56,7 @@ export class EmptyRecorderApp extends EventEmitter implements IRecorderApp { async setPaused(paused: boolean): Promise {} async setMode(mode: Mode): Promise {} async setFileIfNeeded(file: string): Promise {} - async setSelector(selector: string, focus?: boolean): Promise {} + async setSelector(selector: string, userGesture?: boolean): Promise {} async updateCallLogs(callLogs: CallLog[]): Promise {} async setSources(sources: Source[]): Promise {} } @@ -166,14 +166,18 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { (process as any)._didSetSourcesForTest(sources[0].text); } - async setSelector(selector: string, focus?: boolean): Promise { - if (focus) { - this._recorder.setMode('none'); - this._page.bringToFront(); + async setSelector(selector: string, userGesture?: boolean): Promise { + if (userGesture) { + if (this._recorder.mode() === 'inspecting') { + this._recorder.setMode('none'); + this._page.bringToFront(); + } else { + this._recorder.setMode('recording'); + } } - await this._page.mainFrame().evaluateExpression(((arg: any) => { - window.playwrightSetSelector(arg.selector, arg.focus); - }).toString(), { isFunction: true }, { selector, focus }).catch(() => {}); + await this._page.mainFrame().evaluateExpression(((selector: string) => { + window.playwrightSetSelector(selector); + }).toString(), { isFunction: true }, selector).catch(() => {}); } async updateCallLogs(callLogs: CallLog[]): Promise { diff --git a/packages/recorder/src/recorder.css b/packages/recorder/src/recorder.css index b91eba9ff1..93eae2ba21 100644 --- a/packages/recorder/src/recorder.css +++ b/packages/recorder/src/recorder.css @@ -28,13 +28,11 @@ min-width: 100px; } -.recorder .toolbar-button.toggled.record, -.recorder .toolbar-button.toggled.text-size { +.recorder .toolbar-button.toggled.circle-large-filled { color: #a1260d; } -body.dark-mode .recorder .toolbar-button.toggled.record, -body.dark-mode .recorder .toolbar-button.toggled.text-size { +body.dark-mode .recorder .toolbar-button.toggled.circle-large-filled { color: #f48771; } diff --git a/packages/recorder/src/recorder.tsx b/packages/recorder/src/recorder.tsx index a11cdc2b87..6a1c6eb325 100644 --- a/packages/recorder/src/recorder.tsx +++ b/packages/recorder/src/recorder.tsx @@ -19,7 +19,7 @@ import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper'; import { SplitView } from '@web/components/splitView'; import { TabbedPane } from '@web/components/tabbedPane'; import { Toolbar } from '@web/components/toolbar'; -import { ToolbarButton } from '@web/components/toolbarButton'; +import { ToolbarButton, ToolbarSeparator } from '@web/components/toolbarButton'; import * as React from 'react'; import { CallLogView } from './callLog'; import './recorder.css'; @@ -66,7 +66,7 @@ export const Recorder: React.FC = ({ }; const [locator, setLocator] = React.useState(''); - window.playwrightSetSelector = (selector: string, focus?: boolean) => { + window.playwrightSetSelector = (selector: string) => { const language = source.language; setLocator(asLocator(language, selector)); }; @@ -113,12 +113,25 @@ export const Recorder: React.FC = ({ return
- { - window.dispatch({ event: 'setMode', params: { mode: mode === 'recording' ? 'none' : 'recording' } }); + { + window.dispatch({ event: 'setMode', params: { mode: mode === 'none' || mode === 'inspecting' ? 'recording' : 'none' } }); }}>Record - { - window.dispatch({ event: 'setMode', params: { mode: mode === 'assertingText' ? 'none' : 'assertingText' } }); + + { + const newMode = { + 'inspecting': 'none', + 'none': 'inspecting', + 'recording': 'recording-inspecting', + 'recording-inspecting': 'recording', + 'assertingText': 'recording-inspecting', + }[mode]; + window.dispatch({ event: 'setMode', params: { mode: newMode } }).catch(() => { }); + setSelectedTab('locator'); + }}>Pick locator + { + window.dispatch({ event: 'setMode', params: { mode: mode === 'assertingText' ? 'recording' : 'assertingText' } }); }}>Assert + { copy(source.text); }}> @@ -145,10 +158,6 @@ export const Recorder: React.FC = ({ { - window.dispatch({ event: 'setMode', params: { mode: mode === 'inspecting' ? 'none' : 'inspecting' } }).catch(() => { }); - setSelectedTab('locator'); - }} />]} rightToolbar={selectedTab === 'locator' ? [ copy(locator)} />] : []} tabs={[ { diff --git a/packages/recorder/src/recorderTypes.ts b/packages/recorder/src/recorderTypes.ts index fc1c3b9777..5855aa0f63 100644 --- a/packages/recorder/src/recorderTypes.ts +++ b/packages/recorder/src/recorderTypes.ts @@ -18,7 +18,7 @@ import type { Language } from '../../playwright-core/src/utils/isomorphic/locato export type Point = { x: number, y: number }; -export type Mode = 'inspecting' | 'recording' | 'none' | 'assertingText'; +export type Mode = 'inspecting' | 'recording' | 'none' | 'assertingText' | 'recording-inspecting'; export type EventData = { event: 'clear' | 'resume' | 'step' | 'pause' | 'setMode' | 'setRecordingTool' | 'selectorUpdated' | 'fileChanged'; @@ -31,6 +31,7 @@ export type UIState = { actionSelector?: string; language: 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl'; testIdAttributeName: string; + overlayPosition: Point; }; export type CallLogStatus = 'in-progress' | 'done' | 'error' | 'paused'; diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index b128827b9b..c29846ebb4 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -242,6 +242,7 @@ export const InspectModeController: React.FunctionComponent<{ actionSelector: actionSelector.startsWith(frameSelector) ? actionSelector.substring(frameSelector.length).trim() : undefined, language: sdkLanguage, testIdAttributeName, + overlayPosition: { x: 0, y: 0 }, }, { async setSelector(selector: string) { setHighlightedLocator(asLocator(sdkLanguage, frameSelector + selector, false /* isFrameLocator */, true /* playSafe */)); diff --git a/packages/web/src/components/toolbarButton.css b/packages/web/src/components/toolbarButton.css index 32f3f72f31..bdf6db9ad9 100644 --- a/packages/web/src/components/toolbarButton.css +++ b/packages/web/src/components/toolbarButton.css @@ -46,3 +46,12 @@ .toolbar-button.toggled .codicon { font-weight: bold; } + +.toolbar-separator { + flex: none; + background-color: var(--vscode-menu-separatorBackground); + width: 1px; + padding: 0; + margin: 5px 4px; + height: 16px; +} diff --git a/packages/web/src/components/toolbarButton.tsx b/packages/web/src/components/toolbarButton.tsx index 7725531a3f..ac255dd66f 100644 --- a/packages/web/src/components/toolbarButton.tsx +++ b/packages/web/src/components/toolbarButton.tsx @@ -53,6 +53,12 @@ export const ToolbarButton: React.FC ; }; +export const ToolbarSeparator: React.FC<{ style?: React.CSSProperties }> = ({ + style, +}) => { + return
; +}; + const preventDefault = (e: any) => { e.stopPropagation(); e.preventDefault();