feat(inspector): selector input (#5502)
This commit is contained in:
parent
a9faa9c941
commit
8a9048c2b5
|
|
@ -28,6 +28,7 @@ declare global {
|
||||||
_playwrightRecorderCommitAction: () => Promise<void>;
|
_playwrightRecorderCommitAction: () => Promise<void>;
|
||||||
_playwrightRecorderState: () => Promise<UIState>;
|
_playwrightRecorderState: () => Promise<UIState>;
|
||||||
_playwrightResume: () => Promise<void>;
|
_playwrightResume: () => Promise<void>;
|
||||||
|
_playwrightRecorderSetSelector: (selector: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,10 +227,8 @@ export class Recorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onClick(event: MouseEvent) {
|
private _onClick(event: MouseEvent) {
|
||||||
if (this._mode === 'inspecting') {
|
if (this._mode === 'inspecting')
|
||||||
if (this._hoveredModel)
|
window._playwrightRecorderSetSelector(this._hoveredModel ? this._hoveredModel.selector : '');
|
||||||
copy(this._hoveredModel.selector);
|
|
||||||
}
|
|
||||||
if (this._shouldIgnoreMouseEvent(event))
|
if (this._shouldIgnoreMouseEvent(event))
|
||||||
return;
|
return;
|
||||||
if (this._actionInProgress(event))
|
if (this._actionInProgress(event))
|
||||||
|
|
@ -603,13 +602,4 @@ function removeEventListeners(listeners: (() => void)[]) {
|
||||||
listeners.splice(0, listeners.length);
|
listeners.splice(0, listeners.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
function copy(text: string) {
|
|
||||||
const input = html`<textarea style="position: absolute; z-index: -1000;"></textarea>` as any as HTMLInputElement;
|
|
||||||
input.value = text;
|
|
||||||
document.body.appendChild(input);
|
|
||||||
input.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
input.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Recorder;
|
export default Recorder;
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,18 @@ const cacheAllowText = new Map<Element, SelectorToken[] | null>();
|
||||||
const cacheDisallowText = new Map<Element, SelectorToken[] | null>();
|
const cacheDisallowText = new Map<Element, SelectorToken[] | null>();
|
||||||
|
|
||||||
export function querySelector(injectedScript: InjectedScript, selector: string, ownerDocument: Document): { selector: string, elements: Element[] } {
|
export function querySelector(injectedScript: InjectedScript, selector: string, ownerDocument: Document): { selector: string, elements: Element[] } {
|
||||||
const parsedSelector = injectedScript.parseSelector(selector);
|
try {
|
||||||
return {
|
const parsedSelector = injectedScript.parseSelector(selector);
|
||||||
selector,
|
return {
|
||||||
elements: injectedScript.querySelectorAll(parsedSelector, ownerDocument)
|
selector,
|
||||||
};
|
elements: injectedScript.querySelectorAll(parsedSelector, ownerDocument)
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
selector,
|
||||||
|
elements: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateSelector(injectedScript: InjectedScript, targetElement: Element): { selector: string, elements: Element[] } {
|
export function generateSelector(injectedScript: InjectedScript, targetElement: Element): { selector: string, elements: Element[] } {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ declare global {
|
||||||
playwrightSetMode: (mode: Mode) => void;
|
playwrightSetMode: (mode: Mode) => void;
|
||||||
playwrightSetPaused: (paused: boolean) => void;
|
playwrightSetPaused: (paused: boolean) => void;
|
||||||
playwrightSetSources: (sources: Source[]) => void;
|
playwrightSetSources: (sources: Source[]) => void;
|
||||||
|
playwrightSetSelector: (selector: string, focus?: boolean) => void;
|
||||||
playwrightUpdateLogs: (callLogs: CallLog[]) => void;
|
playwrightUpdateLogs: (callLogs: CallLog[]) => void;
|
||||||
dispatch(data: EventData): Promise<void>;
|
dispatch(data: EventData): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
@ -151,6 +152,12 @@ export class RecorderApp extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setSelector(selector: string, focus?: boolean): Promise<void> {
|
||||||
|
await this._page.mainFrame()._evaluateExpression(((arg: any) => {
|
||||||
|
window.playwrightSetSelector(arg.selector, arg.focus);
|
||||||
|
}).toString(), true, { selector, focus }, 'main').catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
async updateCallLogs(callLogs: CallLog[]): Promise<void> {
|
async updateCallLogs(callLogs: CallLog[]): Promise<void> {
|
||||||
await this._page.mainFrame()._evaluateExpression(((callLogs: CallLog[]) => {
|
await this._page.mainFrame()._evaluateExpression(((callLogs: CallLog[]) => {
|
||||||
window.playwrightUpdateLogs(callLogs);
|
window.playwrightUpdateLogs(callLogs);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { Point } from '../../../common/types';
|
||||||
export type Mode = 'inspecting' | 'recording' | 'none';
|
export type Mode = 'inspecting' | 'recording' | 'none';
|
||||||
|
|
||||||
export type EventData = {
|
export type EventData = {
|
||||||
event: 'clear' | 'resume' | 'step' | 'pause' | 'setMode';
|
event: 'clear' | 'resume' | 'step' | 'pause' | 'setMode' | 'selectorUpdated';
|
||||||
params: any;
|
params: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ export class RecorderSupplement {
|
||||||
private _timers = new Set<NodeJS.Timeout>();
|
private _timers = new Set<NodeJS.Timeout>();
|
||||||
private _context: BrowserContext;
|
private _context: BrowserContext;
|
||||||
private _mode: Mode;
|
private _mode: Mode;
|
||||||
|
private _highlightedSelector = '';
|
||||||
private _recorderApp: RecorderApp | null = null;
|
private _recorderApp: RecorderApp | null = null;
|
||||||
private _params: channels.BrowserContextRecorderSupplementEnableParams;
|
private _params: channels.BrowserContextRecorderSupplementEnableParams;
|
||||||
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
|
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
|
||||||
|
|
@ -127,11 +128,11 @@ export class RecorderSupplement {
|
||||||
});
|
});
|
||||||
recorderApp.on('event', (data: EventData) => {
|
recorderApp.on('event', (data: EventData) => {
|
||||||
if (data.event === 'setMode') {
|
if (data.event === 'setMode') {
|
||||||
this._mode = data.params.mode;
|
this._setMode(data.params.mode);
|
||||||
recorderApp.setMode(this._mode);
|
return;
|
||||||
this._generator.setEnabled(this._mode === 'recording');
|
}
|
||||||
if (this._mode !== 'none')
|
if (data.event === 'selectorUpdated') {
|
||||||
this._context.pages()[0].bringToFront().catch(() => {});
|
this._highlightedSelector = data.params.selector;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data.event === 'step') {
|
if (data.event === 'step') {
|
||||||
|
|
@ -191,10 +192,16 @@ export class RecorderSupplement {
|
||||||
actionSelector = metadata.params.selector || actionSelector;
|
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;
|
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, () => {
|
await this._context.exposeBinding('_playwrightResume', false, () => {
|
||||||
this._resume(false).catch(() => {});
|
this._resume(false).catch(() => {});
|
||||||
});
|
});
|
||||||
|
|
@ -216,6 +223,14 @@ export class RecorderSupplement {
|
||||||
return result;
|
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) {
|
private async _resume(step: boolean) {
|
||||||
this._pauseOnNextStatement = step;
|
this._pauseOnNextStatement = step;
|
||||||
this._recorderApp?.setPaused(false);
|
this._recorderApp?.setPaused(false);
|
||||||
|
|
@ -354,6 +369,10 @@ export class RecorderSupplement {
|
||||||
this.updateCallLog([metadata]);
|
this.updateCallLog([metadata]);
|
||||||
if (metadata.method === 'pause' || (this._pauseOnNextStatement && metadata.method === 'goto'))
|
if (metadata.method === 'pause' || (this._pauseOnNextStatement && metadata.method === 'goto'))
|
||||||
await this.pause(metadata);
|
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<void> {
|
async onAfterCall(metadata: CallMetadata): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.split-view-main {
|
.split-view-main {
|
||||||
|
|
|
||||||
|
|
@ -21,25 +21,28 @@ export interface SplitViewProps {
|
||||||
sidebarSize: number,
|
sidebarSize: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const kMinSidebarSize = 50;
|
||||||
|
|
||||||
export const SplitView: React.FC<SplitViewProps> = ({
|
export const SplitView: React.FC<SplitViewProps> = ({
|
||||||
sidebarSize,
|
sidebarSize,
|
||||||
children
|
children
|
||||||
}) => {
|
}) => {
|
||||||
let [size, setSize] = React.useState<number>(sidebarSize);
|
let [size, setSize] = React.useState<number>(Math.max(kMinSidebarSize, sidebarSize));
|
||||||
const [resizing, setResizing] = React.useState<{ offsetY: number } | null>(null);
|
const [resizing, setResizing] = React.useState<{ offsetY: number, size: number } | null>(null);
|
||||||
if (size < 50)
|
|
||||||
size = 50;
|
|
||||||
|
|
||||||
const childrenArray = React.Children.toArray(children);
|
const childrenArray = React.Children.toArray(children);
|
||||||
return <div className='split-view'>
|
return <div className='split-view'>
|
||||||
<div className='split-view-main'>{childrenArray[0]}</div>
|
<div className='split-view-main'>{childrenArray[0]}</div>
|
||||||
<div style={{flexBasis: size}} className='split-view-sidebar'>{childrenArray[1]}</div>
|
<div style={{flexBasis: size}} className='split-view-sidebar'>{childrenArray[1]}</div>
|
||||||
<div
|
<div
|
||||||
style={{bottom: resizing ? 0 : size - 32, top: resizing ? 0 : undefined, height: resizing ? 'initial' : 32 }}
|
style={{bottom: resizing ? 0 : size - 4, top: resizing ? 0 : undefined, height: resizing ? 'initial' : 8 }}
|
||||||
className='split-view-resizer'
|
className='split-view-resizer'
|
||||||
onMouseDown={event => setResizing({ offsetY: event.clientY - (event.target as HTMLElement).getBoundingClientRect().y })}
|
onMouseDown={event => setResizing({ offsetY: event.clientY, size })}
|
||||||
onMouseUp={() => setResizing(null)}
|
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));
|
||||||
|
}}
|
||||||
></div>
|
></div>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -29,3 +29,21 @@
|
||||||
display: block;
|
display: block;
|
||||||
flex: auto;
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,17 +31,6 @@
|
||||||
align-items: center;
|
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 {
|
.call-log-call {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: none;
|
flex: none;
|
||||||
|
|
|
||||||
|
|
@ -33,37 +33,34 @@ export const CallLogView: React.FC<CallLogProps> = ({
|
||||||
messagesEndRef.current?.scrollIntoView({ block: 'center', inline: 'nearest' });
|
messagesEndRef.current?.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||||
}, [messagesEndRef]);
|
}, [messagesEndRef]);
|
||||||
|
|
||||||
return <div className='vbox'>
|
return <div className='call-log' style={{flex: 'auto'}}>
|
||||||
<div className='call-log-header' style={{flex: 'none'}}>Log</div>
|
{log.map(callLog => {
|
||||||
<div className='call-log' style={{flex: 'auto'}}>
|
const expandOverride = expandOverrides.get(callLog.id);
|
||||||
{log.map(callLog => {
|
const isExpanded = typeof expandOverride === 'boolean' ? expandOverride : callLog.status !== 'done';
|
||||||
const expandOverride = expandOverrides.get(callLog.id);
|
return <div className={`call-log-call ${callLog.status}`} key={callLog.id}>
|
||||||
const isExpanded = typeof expandOverride === 'boolean' ? expandOverride : callLog.status !== 'done';
|
<div className='call-log-call-header'>
|
||||||
return <div className={`call-log-call ${callLog.status}`} key={callLog.id}>
|
<span className={`codicon codicon-chevron-${isExpanded ? 'down' : 'right'}`} style={{ cursor: 'pointer' }}onClick={() => {
|
||||||
<div className='call-log-call-header'>
|
const newOverrides = new Map(expandOverrides);
|
||||||
<span className={`codicon codicon-chevron-${isExpanded ? 'down' : 'right'}`} style={{ cursor: 'pointer' }}onClick={() => {
|
newOverrides.set(callLog.id, !isExpanded);
|
||||||
const newOverrides = new Map(expandOverrides);
|
setExpandOverrides(newOverrides);
|
||||||
newOverrides.set(callLog.id, !isExpanded);
|
}}></span>
|
||||||
setExpandOverrides(newOverrides);
|
{ callLog.title }
|
||||||
}}></span>
|
{ callLog.params.url ? <span>(<span className='call-log-url'>{callLog.params.url}</span>)</span> : undefined }
|
||||||
{ callLog.title }
|
{ callLog.params.selector ? <span>(<span className='call-log-selector'>{callLog.params.selector}</span>)</span> : undefined }
|
||||||
{ callLog.params.url ? <span>(<span className='call-log-url'>{callLog.params.url}</span>)</span> : undefined }
|
<span className={'codicon ' + iconClass(callLog)}></span>
|
||||||
{ callLog.params.selector ? <span>(<span className='call-log-selector'>{callLog.params.selector}</span>)</span> : undefined }
|
{ typeof callLog.duration === 'number' ? <span className='call-log-time'>— {msToString(callLog.duration)}</span> : undefined}
|
||||||
<span className={'codicon ' + iconClass(callLog)}></span>
|
|
||||||
{ typeof callLog.duration === 'number' ? <span className='call-log-time'>— {msToString(callLog.duration)}</span> : undefined}
|
|
||||||
</div>
|
|
||||||
{ (isExpanded ? callLog.messages : []).map((message, i) => {
|
|
||||||
return <div className='call-log-message' key={i}>
|
|
||||||
{ message.trim() }
|
|
||||||
</div>;
|
|
||||||
})}
|
|
||||||
{ callLog.error ? <div className='call-log-message error' hidden={!isExpanded}>
|
|
||||||
{ callLog.error }
|
|
||||||
</div> : undefined }
|
|
||||||
</div>
|
</div>
|
||||||
})}
|
{ (isExpanded ? callLog.messages : []).map((message, i) => {
|
||||||
<div ref={messagesEndRef}></div>
|
return <div className='call-log-message' key={i}>
|
||||||
</div>
|
{ message.trim() }
|
||||||
|
</div>;
|
||||||
|
})}
|
||||||
|
{ callLog.error ? <div className='call-log-message error' hidden={!isExpanded}>
|
||||||
|
{ callLog.error }
|
||||||
|
</div> : undefined }
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
<div ref={messagesEndRef}></div>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,6 @@ import { applyTheme } from '../theme';
|
||||||
import '../common.css';
|
import '../common.css';
|
||||||
import { Main } from './main';
|
import { Main } from './main';
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
applyTheme();
|
applyTheme();
|
||||||
ReactDOM.render(<Main/>, document.querySelector('#root'));
|
ReactDOM.render(<Main/>, document.querySelector('#root'));
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ declare global {
|
||||||
playwrightSetPaused: (paused: boolean) => void;
|
playwrightSetPaused: (paused: boolean) => void;
|
||||||
playwrightSetSources: (sources: Source[]) => void;
|
playwrightSetSources: (sources: Source[]) => void;
|
||||||
playwrightUpdateLogs: (callLogs: CallLog[]) => void;
|
playwrightUpdateLogs: (callLogs: CallLog[]) => void;
|
||||||
dispatch(data: any): Promise<void>;
|
|
||||||
playwrightSourcesEchoForTest: Source[];
|
playwrightSourcesEchoForTest: Source[];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -36,6 +35,7 @@ export const Main: React.FC = ({
|
||||||
const [paused, setPaused] = React.useState(false);
|
const [paused, setPaused] = React.useState(false);
|
||||||
const [log, setLog] = React.useState(new Map<number, CallLog>());
|
const [log, setLog] = React.useState(new Map<number, CallLog>());
|
||||||
const [mode, setMode] = React.useState<Mode>('none');
|
const [mode, setMode] = React.useState<Mode>('none');
|
||||||
|
const [selector, setSelector] = React.useState('');
|
||||||
|
|
||||||
window.playwrightSetMode = setMode;
|
window.playwrightSetMode = setMode;
|
||||||
window.playwrightSetSources = setSources;
|
window.playwrightSetSources = setSources;
|
||||||
|
|
|
||||||
|
|
@ -55,3 +55,7 @@
|
||||||
.recorder .toolbar-button:not([disabled]):hover .codicon-debug-step-over {
|
.recorder .toolbar-button:not([disabled]):hover .codicon-debug-step-over {
|
||||||
color: #41ca1e;
|
color: #41ca1e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recorder .selector-input {
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,3 +83,27 @@ WithLog.args = {
|
||||||
log: exampleCallLog(),
|
log: exampleCallLog(),
|
||||||
mode: 'none'
|
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: '<javascript>',
|
||||||
|
text: `await page.click('button');\n\nawait page.click('button');\n`,
|
||||||
|
language: 'javascript',
|
||||||
|
highlight: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
paused: false,
|
||||||
|
log: [],
|
||||||
|
mode: 'recording',
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ import { CallLogView } from './callLog';
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
playwrightSetFile: (file: string) => void;
|
playwrightSetFile: (file: string) => void;
|
||||||
|
playwrightSetSelector: (selector: string, focus?: boolean) => void;
|
||||||
|
dispatch(data: any): Promise<void>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,15 +35,24 @@ export interface RecorderProps {
|
||||||
sources: Source[],
|
sources: Source[],
|
||||||
paused: boolean,
|
paused: boolean,
|
||||||
log: Map<number, CallLog>,
|
log: Map<number, CallLog>,
|
||||||
mode: Mode
|
mode: Mode,
|
||||||
|
initialSelector?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Recorder: React.FC<RecorderProps> = ({
|
export const Recorder: React.FC<RecorderProps> = ({
|
||||||
sources,
|
sources,
|
||||||
paused,
|
paused,
|
||||||
log,
|
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<string | undefined>();
|
const [f, setFile] = React.useState<string | undefined>();
|
||||||
window.playwrightSetFile = setFile;
|
window.playwrightSetFile = setFile;
|
||||||
const file = f || sources[0]?.file;
|
const file = f || sources[0]?.file;
|
||||||
|
|
@ -57,14 +68,21 @@ export const Recorder: React.FC<RecorderProps> = ({
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
messagesEndRef.current?.scrollIntoView({ block: 'center', inline: 'nearest' });
|
messagesEndRef.current?.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||||
}, [messagesEndRef]);
|
}, [messagesEndRef]);
|
||||||
|
|
||||||
|
const selectorInputRef = React.createRef<HTMLInputElement>();
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
if (focusSelectorInput && selectorInputRef.current) {
|
||||||
|
selectorInputRef.current.select();
|
||||||
|
selectorInputRef.current.focus();
|
||||||
|
setFocusSelectorInput(false);
|
||||||
|
}
|
||||||
|
}, [focusSelectorInput, selectorInputRef]);
|
||||||
|
|
||||||
return <div className='recorder'>
|
return <div className='recorder'>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<ToolbarButton icon='record' title='Record' toggled={mode == 'recording'} onClick={() => {
|
<ToolbarButton icon='record' title='Record' toggled={mode == 'recording'} onClick={() => {
|
||||||
window.dispatch({ event: 'setMode', params: { mode: mode === 'recording' ? 'none' : 'recording' }}).catch(() => { });
|
window.dispatch({ event: 'setMode', params: { mode: mode === 'recording' ? 'none' : 'recording' }}).catch(() => { });
|
||||||
}}>Record</ToolbarButton>
|
}}>Record</ToolbarButton>
|
||||||
<ToolbarButton icon='question' title='Explore' toggled={mode == 'inspecting'} onClick={() => {
|
|
||||||
window.dispatch({ event: 'setMode', params: { mode: mode === 'inspecting' ? 'none' : 'inspecting' }}).catch(() => { });
|
|
||||||
}}>Explore</ToolbarButton>
|
|
||||||
<ToolbarButton icon='files' title='Copy' disabled={!source || !source.text} onClick={() => {
|
<ToolbarButton icon='files' title='Copy' disabled={!source || !source.text} onClick={() => {
|
||||||
copy(source.text);
|
copy(source.text);
|
||||||
}}></ToolbarButton>
|
}}></ToolbarButton>
|
||||||
|
|
@ -93,7 +111,18 @@ export const Recorder: React.FC<RecorderProps> = ({
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
<SplitView sidebarSize={200}>
|
<SplitView sidebarSize={200}>
|
||||||
<SourceView text={source.text} language={source.language} highlight={source.highlight} revealLine={source.revealLine}></SourceView>
|
<SourceView text={source.text} language={source.language} highlight={source.highlight} revealLine={source.revealLine}></SourceView>
|
||||||
<CallLogView log={[...log.values()]}/>
|
<div className='vbox'>
|
||||||
|
<Toolbar>
|
||||||
|
<ToolbarButton icon='question' title='Explore' toggled={mode == 'inspecting'} onClick={() => {
|
||||||
|
window.dispatch({ event: 'setMode', params: { mode: mode === 'inspecting' ? 'none' : 'inspecting' }}).catch(() => { });
|
||||||
|
}}>Explore</ToolbarButton>
|
||||||
|
<input ref={selectorInputRef} className='selector-input' placeholder='Playwright Selector' value={selector} disabled={mode !== 'none'} onChange={event => {
|
||||||
|
setSelector(event.target.value);
|
||||||
|
window.dispatch({ event: 'selectorUpdated', params: { selector: event.target.value } });
|
||||||
|
}} />
|
||||||
|
</Toolbar>
|
||||||
|
<CallLogView log={[...log.values()]}/>
|
||||||
|
</div>
|
||||||
</SplitView>
|
</SplitView>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue