2021-01-28 23:25:10 +01:00
/ *
Copyright ( c ) Microsoft Corporation .
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
2023-11-01 23:56:49 +01:00
import type { CallLog , Mode , Source } from './recorderTypes' ;
2022-11-03 17:55:23 +01:00
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper' ;
2022-03-25 22:12:00 +01:00
import { SplitView } from '@web/components/splitView' ;
2023-09-07 02:13:25 +02:00
import { TabbedPane } from '@web/components/tabbedPane' ;
2022-03-25 22:12:00 +01:00
import { Toolbar } from '@web/components/toolbar' ;
2023-11-05 05:18:27 +01:00
import { ToolbarButton , ToolbarSeparator } from '@web/components/toolbarButton' ;
2021-01-28 23:25:10 +01:00
import * as React from 'react' ;
2021-02-17 23:05:41 +01:00
import { CallLogView } from './callLog' ;
2022-03-25 22:12:00 +01:00
import './recorder.css' ;
2022-10-19 22:05:52 +02:00
import { asLocator } from '@isomorphic/locatorGenerators' ;
2022-11-11 02:20:09 +01:00
import { toggleTheme } from '@web/theme' ;
2023-02-17 20:19:53 +01:00
import { copy } from '@web/uiUtils' ;
2021-02-05 23:24:27 +01:00
2021-02-01 01:37:13 +01:00
declare global {
interface Window {
2022-05-04 18:16:24 +02:00
playwrightSetFileIfNeeded : ( file : string ) = > void ;
2021-02-19 16:25:08 +01:00
playwrightSetSelector : ( selector : string , focus? : boolean ) = > void ;
dispatch ( data : any ) : Promise < void > ;
2021-02-01 01:37:13 +01:00
}
}
2021-01-28 23:25:10 +01:00
export interface RecorderProps {
2021-02-17 23:05:41 +01:00
sources : Source [ ] ,
paused : boolean ,
2021-04-23 18:28:18 +02:00
log : Map < string , CallLog > ,
2021-02-19 16:25:08 +01:00
mode : Mode ,
2021-01-28 23:25:10 +01:00
}
export const Recorder : React.FC < RecorderProps > = ( {
2021-02-17 23:05:41 +01:00
sources ,
paused ,
log ,
2021-02-19 16:25:08 +01:00
mode ,
2021-01-28 23:25:10 +01:00
} ) = > {
2022-08-15 19:44:46 +02:00
const [ fileId , setFileId ] = React . useState < string | undefined > ( ) ;
2023-09-07 02:13:25 +02:00
const [ selectedTab , setSelectedTab ] = React . useState < string > ( 'log' ) ;
2021-02-05 23:24:27 +01:00
2022-08-15 19:44:46 +02:00
React . useEffect ( ( ) = > {
if ( ! fileId && sources . length > 0 )
setFileId ( sources [ 0 ] . id ) ;
} , [ fileId , sources ] ) ;
const source : Source = sources . find ( s = > s . id === fileId ) || {
id : 'default' ,
2022-05-04 18:16:24 +02:00
isRecorded : false ,
2021-02-17 03:13:26 +01:00
text : '' ,
language : 'javascript' ,
2022-08-15 19:44:46 +02:00
label : '' ,
2021-02-17 03:13:26 +01:00
highlight : [ ]
} ;
2022-11-03 17:55:23 +01:00
const [ locator , setLocator ] = React . useState ( '' ) ;
2023-11-08 04:36:12 +01:00
window . playwrightSetSelector = ( selector : string , focus? : boolean ) = > {
2022-11-03 17:55:23 +01:00
const language = source . language ;
2023-11-08 04:36:12 +01:00
if ( focus )
setSelectedTab ( 'locator' ) ;
2022-11-03 17:55:23 +01:00
setLocator ( asLocator ( language , selector ) ) ;
} ;
2022-05-04 18:16:24 +02:00
window . playwrightSetFileIfNeeded = ( value : string ) = > {
2022-08-15 19:44:46 +02:00
const newSource = sources . find ( s = > s . id === value ) ;
2022-05-04 18:16:24 +02:00
// Do not forcefully switch between two recorded sources, because
// user did explicitly choose one.
if ( newSource && ! newSource . isRecorded || ! source . isRecorded )
2022-08-15 19:44:46 +02:00
setFileId ( value ) ;
2022-05-04 18:16:24 +02:00
} ;
2021-02-13 03:53:46 +01:00
2023-03-10 04:34:05 +01:00
const messagesEndRef = React . useRef < HTMLDivElement > ( null ) ;
2021-02-13 03:53:46 +01:00
React . useLayoutEffect ( ( ) = > {
messagesEndRef . current ? . scrollIntoView ( { block : 'center' , inline : 'nearest' } ) ;
} , [ messagesEndRef ] ) ;
2021-02-19 16:25:08 +01:00
2022-07-07 20:25:48 +02:00
React . useEffect ( ( ) = > {
const handleKeyDown = ( event : KeyboardEvent ) = > {
switch ( event . key ) {
case 'F8' :
event . preventDefault ( ) ;
if ( paused )
window . dispatch ( { event : 'resume' } ) ;
else
window . dispatch ( { event : 'pause' } ) ;
break ;
case 'F10' :
event . preventDefault ( ) ;
if ( paused )
window . dispatch ( { event : 'step' } ) ;
break ;
}
} ;
document . addEventListener ( 'keydown' , handleKeyDown ) ;
return ( ) = > document . removeEventListener ( 'keydown' , handleKeyDown ) ;
} , [ paused ] ) ;
2023-10-22 13:02:14 +02:00
const onEditorChange = React . useCallback ( ( selector : string ) = > {
setLocator ( selector ) ;
window . dispatch ( { event : 'selectorUpdated' , params : { selector } } ) ;
} , [ ] ) ;
2023-04-21 21:38:39 +02:00
2021-02-13 03:53:46 +01:00
return < div className = 'recorder' >
2021-01-28 23:25:10 +01:00
< Toolbar >
2023-11-05 05:18:27 +01:00
< ToolbarButton icon = 'circle-large-filled' title = 'Record' toggled = { mode === 'recording' || mode === 'recording-inspecting' || mode === 'assertingText' } onClick = { ( ) = > {
window . dispatch ( { event : 'setMode' , params : { mode : mode === 'none' || mode === 'inspecting' ? 'recording' : 'none' } } ) ;
2021-02-18 02:28:02 +01:00
} } > Record < / ToolbarButton >
2023-11-05 05:18:27 +01:00
< ToolbarSeparator / >
< ToolbarButton icon = 'inspect' title = 'Pick locator' toggled = { mode === 'inspecting' || mode === 'recording-inspecting' } onClick = { ( ) = > {
const newMode = {
'inspecting' : 'none' ,
'none' : 'inspecting' ,
'recording' : 'recording-inspecting' ,
'recording-inspecting' : 'recording' ,
'assertingText' : 'recording-inspecting' ,
} [ mode ] ;
window . dispatch ( { event : 'setMode' , params : { mode : newMode } } ) . catch ( ( ) = > { } ) ;
} } > Pick locator < / ToolbarButton >
< ToolbarButton icon = 'check-all' title = 'Assert text and values' toggled = { mode === 'assertingText' } disabled = { mode === 'none' || mode === 'inspecting' } onClick = { ( ) = > {
window . dispatch ( { event : 'setMode' , params : { mode : mode === 'assertingText' ? 'recording' : 'assertingText' } } ) ;
2023-10-27 03:49:14 +02:00
} } > Assert < / ToolbarButton >
2023-11-05 05:18:27 +01:00
< ToolbarSeparator / >
2021-02-17 23:05:41 +01:00
< ToolbarButton icon = 'files' title = 'Copy' disabled = { ! source || ! source . text } onClick = { ( ) = > {
2021-02-05 23:24:27 +01:00
copy ( source . text ) ;
2021-02-01 01:37:13 +01:00
} } > < / ToolbarButton >
2022-07-07 20:25:48 +02:00
< ToolbarButton icon = 'debug-continue' title = 'Resume (F8)' disabled = { ! paused } onClick = { ( ) = > {
2021-03-01 21:20:04 +01:00
window . dispatch ( { event : 'resume' } ) ;
2021-02-12 19:11:30 +01:00
} } > < / ToolbarButton >
2022-07-07 20:25:48 +02:00
< ToolbarButton icon = 'debug-pause' title = 'Pause (F8)' disabled = { paused } onClick = { ( ) = > {
2021-03-01 21:20:04 +01:00
window . dispatch ( { event : 'pause' } ) ;
2021-02-12 19:11:30 +01:00
} } > < / ToolbarButton >
2022-07-07 20:25:48 +02:00
< ToolbarButton icon = 'debug-step-over' title = 'Step over (F10)' disabled = { ! paused } onClick = { ( ) = > {
2021-03-01 21:20:04 +01:00
window . dispatch ( { event : 'step' } ) ;
2021-02-12 19:11:30 +01:00
} } > < / ToolbarButton >
2021-09-27 18:58:08 +02:00
< div style = { { flex : 'auto' } } > < / div >
2021-06-11 01:52:59 +02:00
< div > Target : < / div >
2022-08-15 19:44:46 +02:00
< select className = 'recorder-chooser' hidden = { ! sources . length } value = { fileId } onChange = { event = > {
setFileId ( event . target . selectedOptions [ 0 ] . value ) ;
2022-10-06 02:59:34 +02:00
window . dispatch ( { event : 'fileChanged' , params : { file : event.target.selectedOptions [ 0 ] . value } } ) ;
2022-08-15 19:44:46 +02:00
} } > { renderSourceOptions ( sources ) } < / select >
2021-02-17 23:05:41 +01:00
< ToolbarButton icon = 'clear-all' title = 'Clear' disabled = { ! source || ! source . text } onClick = { ( ) = > {
2021-03-01 21:20:04 +01:00
window . dispatch ( { event : 'clear' } ) ;
2021-02-05 23:24:27 +01:00
} } > < / ToolbarButton >
2022-11-11 02:20:09 +01:00
< ToolbarButton icon = 'color-mode' title = 'Toggle color mode' toggled = { false } onClick = { ( ) = > toggleTheme ( ) } > < / ToolbarButton >
2021-01-28 23:25:10 +01:00
< / Toolbar >
2023-11-08 04:36:12 +01:00
< SplitView sidebarSize = { 200 } >
2023-03-11 01:22:19 +01:00
< CodeMirrorWrapper text = { source . text } language = { source . language } highlight = { source . highlight } revealLine = { source . revealLine } readOnly = { true } lineNumbers = { true } / >
2023-09-07 02:13:25 +02:00
< TabbedPane
rightToolbar = { selectedTab === 'locator' ? [ < ToolbarButton icon = 'files' title = 'Copy' onClick = { ( ) = > copy ( locator ) } / > ] : [ ] }
tabs = { [
{
id : 'locator' ,
title : 'Locator' ,
2023-11-08 04:36:12 +01:00
render : ( ) = > < CodeMirrorWrapper text = { locator } language = { source . language } readOnly = { false } focusOnChange = { true } onChange = { onEditorChange } wrapLines = { true } / >
2023-09-07 02:13:25 +02:00
} ,
{
id : 'log' ,
title : 'Log' ,
render : ( ) = > < CallLogView language = { source . language } log = { Array . from ( log . values ( ) ) } / >
} ,
] }
selectedTab = { selectedTab }
setSelectedTab = { setSelectedTab }
/ >
2021-02-13 03:53:46 +01:00
< / SplitView >
2021-01-28 23:25:10 +01:00
< / div > ;
} ;
2021-02-01 01:37:13 +01:00
2022-08-15 19:44:46 +02:00
function renderSourceOptions ( sources : Source [ ] ) : React . ReactNode {
const transformTitle = ( title : string ) : string = > title . replace ( /.*[/\\]([^/\\]+)/ , '$1' ) ;
const renderOption = ( source : Source ) : React . ReactNode = > (
< option key = { source . id } value = { source . id } > { transformTitle ( source . label ) } < / option >
) ;
const hasGroup = sources . some ( s = > s . group ) ;
if ( hasGroup ) {
const groups = new Set ( sources . map ( s = > s . group ) ) ;
2023-05-20 19:15:33 +02:00
return [ . . . groups ] . filter ( Boolean ) . map ( group = > (
2022-08-15 19:44:46 +02:00
< optgroup label = { group } key = { group } >
{ sources . filter ( s = > s . group === group ) . map ( source = > renderOption ( source ) ) }
< / optgroup >
) ) ;
}
return sources . map ( source = > renderOption ( source ) ) ;
}