Skip to content

Commit

Permalink
feat(inspector): selector input (#5502)
Browse files Browse the repository at this point in the history
  • Loading branch information
dgozman committed Feb 19, 2021
1 parent a9faa9c commit 8a9048c
Show file tree
Hide file tree
Showing 15 changed files with 172 additions and 89 deletions.
16 changes: 3 additions & 13 deletions src/server/supplements/injected/recorder.ts
Expand Up @@ -28,6 +28,7 @@ declare global {
_playwrightRecorderCommitAction: () => Promise<void>;
_playwrightRecorderState: () => Promise<UIState>;
_playwrightResume: () => Promise<void>;
_playwrightRecorderSetSelector: (selector: string) => Promise<void>;
}
}

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -603,13 +602,4 @@ function removeEventListeners(listeners: (() => void)[]) {
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;
17 changes: 12 additions & 5 deletions src/server/supplements/injected/selectorGenerator.ts
Expand Up @@ -27,11 +27,18 @@ const cacheAllowText = 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[] } {
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[] } {
Expand Down
7 changes: 7 additions & 0 deletions src/server/supplements/recorder/recorderApp.ts
Expand Up @@ -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<void>;
}
Expand Down Expand Up @@ -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> {
await this._page.mainFrame()._evaluateExpression(((callLogs: CallLog[]) => {
window.playwrightUpdateLogs(callLogs);
Expand Down
2 changes: 1 addition & 1 deletion src/server/supplements/recorder/recorderTypes.ts
Expand Up @@ -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;
};

Expand Down
31 changes: 25 additions & 6 deletions src/server/supplements/recorderSupplement.ts
Expand Up @@ -45,6 +45,7 @@ export class RecorderSupplement {
private _timers = new Set<NodeJS.Timeout>();
private _context: BrowserContext;
private _mode: Mode;
private _highlightedSelector = '';
private _recorderApp: RecorderApp | null = null;
private _params: channels.BrowserContextRecorderSupplementEnableParams;
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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(() => {});
});
Expand All @@ -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);
Expand Down Expand Up @@ -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<void> {
Expand Down
1 change: 1 addition & 0 deletions src/web/components/splitView.css
Expand Up @@ -18,6 +18,7 @@
display: flex;
flex: auto;
flex-direction: column;
position: relative;
}

.split-view-main {
Expand Down
17 changes: 10 additions & 7 deletions src/web/components/splitView.tsx
Expand Up @@ -21,25 +21,28 @@ export interface SplitViewProps {
sidebarSize: number,
}

const kMinSidebarSize = 50;

export const SplitView: React.FC<SplitViewProps> = ({
sidebarSize,
children
}) => {
let [size, setSize] = React.useState<number>(sidebarSize);
const [resizing, setResizing] = React.useState<{ offsetY: number } | null>(null);
if (size < 50)
size = 50;
let [size, setSize] = React.useState<number>(Math.max(kMinSidebarSize, sidebarSize));
const [resizing, setResizing] = React.useState<{ offsetY: number, size: number } | null>(null);

const childrenArray = React.Children.toArray(children);
return <div className='split-view'>
<div className='split-view-main'>{childrenArray[0]}</div>
<div style={{flexBasis: size}} className='split-view-sidebar'>{childrenArray[1]}</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'
onMouseDown={event => 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));
}}
></div>
</div>;
};
20 changes: 19 additions & 1 deletion src/web/components/toolbar.css
Expand Up @@ -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);
}
11 changes: 0 additions & 11 deletions src/web/recorder/callLog.css
Expand Up @@ -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;
Expand Down
57 changes: 27 additions & 30 deletions src/web/recorder/callLog.tsx
Expand Up @@ -33,37 +33,34 @@ export const CallLogView: React.FC<CallLogProps> = ({
messagesEndRef.current?.scrollIntoView({ block: 'center', inline: 'nearest' });
}, [messagesEndRef]);

return <div className='vbox'>
<div className='call-log-header' style={{flex: 'none'}}>Log</div>
<div className='call-log' style={{flex: 'auto'}}>
{log.map(callLog => {
const expandOverride = expandOverrides.get(callLog.id);
const isExpanded = typeof expandOverride === 'boolean' ? expandOverride : callLog.status !== 'done';
return <div className={`call-log-call ${callLog.status}`} key={callLog.id}>
<div className='call-log-call-header'>
<span className={`codicon codicon-chevron-${isExpanded ? 'down' : 'right'}`} style={{ cursor: 'pointer' }}onClick={() => {
const newOverrides = new Map(expandOverrides);
newOverrides.set(callLog.id, !isExpanded);
setExpandOverrides(newOverrides);
}}></span>
{ callLog.title }
{ callLog.params.url ? <span>(<span className='call-log-url'>{callLog.params.url}</span>)</span> : undefined }
{ callLog.params.selector ? <span>(<span className='call-log-selector'>{callLog.params.selector}</span>)</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 }
return <div className='call-log' style={{flex: 'auto'}}>
{log.map(callLog => {
const expandOverride = expandOverrides.get(callLog.id);
const isExpanded = typeof expandOverride === 'boolean' ? expandOverride : callLog.status !== 'done';
return <div className={`call-log-call ${callLog.status}`} key={callLog.id}>
<div className='call-log-call-header'>
<span className={`codicon codicon-chevron-${isExpanded ? 'down' : 'right'}`} style={{ cursor: 'pointer' }}onClick={() => {
const newOverrides = new Map(expandOverrides);
newOverrides.set(callLog.id, !isExpanded);
setExpandOverrides(newOverrides);
}}></span>
{ callLog.title }
{ callLog.params.url ? <span>(<span className='call-log-url'>{callLog.params.url}</span>)</span> : undefined }
{ callLog.params.selector ? <span>(<span className='call-log-selector'>{callLog.params.selector}</span>)</span> : undefined }
<span className={'codicon ' + iconClass(callLog)}></span>
{ typeof callLog.duration === 'number' ? <span className='call-log-time'>{msToString(callLog.duration)}</span> : undefined}
</div>
})}
<div ref={messagesEndRef}></div>
</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 ref={messagesEndRef}></div>
</div>;
};

Expand Down
5 changes: 0 additions & 5 deletions src/web/recorder/index.tsx
Expand Up @@ -21,11 +21,6 @@ import { applyTheme } from '../theme';
import '../common.css';
import { Main } from './main';

declare global {
interface Window {
}
}

(async () => {
applyTheme();
ReactDOM.render(<Main/>, document.querySelector('#root'));
Expand Down
2 changes: 1 addition & 1 deletion src/web/recorder/main.tsx
Expand Up @@ -25,7 +25,6 @@ declare global {
playwrightSetPaused: (paused: boolean) => void;
playwrightSetSources: (sources: Source[]) => void;
playwrightUpdateLogs: (callLogs: CallLog[]) => void;
dispatch(data: any): Promise<void>;
playwrightSourcesEchoForTest: Source[];
}
}
Expand All @@ -36,6 +35,7 @@ export const Main: React.FC = ({
const [paused, setPaused] = React.useState(false);
const [log, setLog] = React.useState(new Map<number, CallLog>());
const [mode, setMode] = React.useState<Mode>('none');
const [selector, setSelector] = React.useState('');

window.playwrightSetMode = setMode;
window.playwrightSetSources = setSources;
Expand Down
4 changes: 4 additions & 0 deletions src/web/recorder/recorder.css
Expand Up @@ -55,3 +55,7 @@
.recorder .toolbar-button:not([disabled]):hover .codicon-debug-step-over {
color: #41ca1e;
}

.recorder .selector-input {
flex: auto;
}

0 comments on commit 8a9048c

Please sign in to comment.