feat(inspector): allow selecting file (#5483)
This commit is contained in:
parent
8f3a6c6b45
commit
b2227c1bcf
|
|
@ -94,7 +94,7 @@ export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageActi
|
||||||
|
|
||||||
export type BaseSignal = {
|
export type BaseSignal = {
|
||||||
isAsync?: boolean,
|
isAsync?: boolean,
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NavigationSignal = BaseSignal & {
|
export type NavigationSignal = BaseSignal & {
|
||||||
name: 'navigation',
|
name: 'navigation',
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ const readFileAsync = util.promisify(fs.readFile);
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
playwrightSetFile: (file: string) => void;
|
||||||
playwrightSetMode: (mode: Mode) => void;
|
playwrightSetMode: (mode: Mode) => void;
|
||||||
playwrightSetPaused: (paused: boolean) => void;
|
playwrightSetPaused: (paused: boolean) => void;
|
||||||
playwrightSetSources: (sources: Source[]) => void;
|
playwrightSetSources: (sources: Source[]) => void;
|
||||||
|
|
@ -123,6 +124,12 @@ export class RecorderApp extends EventEmitter {
|
||||||
}).toString(), true, mode, 'main').catch(() => {});
|
}).toString(), true, mode, 'main').catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setFile(file: string): Promise<void> {
|
||||||
|
await this._page.mainFrame()._evaluateExpression(((file: string) => {
|
||||||
|
window.playwrightSetFile(file);
|
||||||
|
}).toString(), true, file, 'main').catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
async setPaused(paused: boolean): Promise<void> {
|
async setPaused(paused: boolean): Promise<void> {
|
||||||
await this._page.mainFrame()._evaluateExpression(((paused: boolean) => {
|
await this._page.mainFrame()._evaluateExpression(((paused: boolean) => {
|
||||||
window.playwrightSetPaused(paused);
|
window.playwrightSetPaused(paused);
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export class RecorderSupplement {
|
||||||
private _params: channels.BrowserContextRecorderSupplementEnableParams;
|
private _params: channels.BrowserContextRecorderSupplementEnableParams;
|
||||||
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
|
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
|
||||||
private _pausedCallsMetadata = new Map<CallMetadata, () => void>();
|
private _pausedCallsMetadata = new Map<CallMetadata, () => void>();
|
||||||
private _pauseOnNextStatement = true;
|
private _pauseOnNextStatement = false;
|
||||||
private _recorderSources: Source[];
|
private _recorderSources: Source[];
|
||||||
private _userSources = new Map<string, Source>();
|
private _userSources = new Map<string, Source>();
|
||||||
|
|
||||||
|
|
@ -104,6 +104,7 @@ export class RecorderSupplement {
|
||||||
text = source.text;
|
text = source.text;
|
||||||
}
|
}
|
||||||
this._pushAllSources();
|
this._pushAllSources();
|
||||||
|
this._recorderApp?.setFile(primaryLanguage.fileName);
|
||||||
});
|
});
|
||||||
if (params.outputFile) {
|
if (params.outputFile) {
|
||||||
context.on(BrowserContext.Events.BeforeClose, () => {
|
context.on(BrowserContext.Events.BeforeClose, () => {
|
||||||
|
|
@ -216,12 +217,12 @@ export class RecorderSupplement {
|
||||||
|
|
||||||
private async _resume(step: boolean) {
|
private async _resume(step: boolean) {
|
||||||
this._pauseOnNextStatement = step;
|
this._pauseOnNextStatement = step;
|
||||||
|
this._recorderApp?.setPaused(false);
|
||||||
|
|
||||||
for (const callback of this._pausedCallsMetadata.values())
|
for (const callback of this._pausedCallsMetadata.values())
|
||||||
callback();
|
callback();
|
||||||
this._pausedCallsMetadata.clear();
|
this._pausedCallsMetadata.clear();
|
||||||
|
|
||||||
this._recorderApp?.setPaused(false);
|
|
||||||
this._updateUserSources();
|
this._updateUserSources();
|
||||||
this.updateCallLog([...this._currentCallsMetadata.keys()]);
|
this.updateCallLog([...this._currentCallsMetadata.keys()]);
|
||||||
}
|
}
|
||||||
|
|
@ -369,6 +370,7 @@ export class RecorderSupplement {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply new decorations.
|
// Apply new decorations.
|
||||||
|
let fileToSelect = undefined;
|
||||||
for (const metadata of this._currentCallsMetadata.keys()) {
|
for (const metadata of this._currentCallsMetadata.keys()) {
|
||||||
if (!metadata.stack || !metadata.stack[0])
|
if (!metadata.stack || !metadata.stack[0])
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -381,11 +383,13 @@ export class RecorderSupplement {
|
||||||
if (line) {
|
if (line) {
|
||||||
const paused = this._pausedCallsMetadata.has(metadata);
|
const paused = this._pausedCallsMetadata.has(metadata);
|
||||||
source.highlight.push({ line, type: metadata.error ? 'error' : (paused ? 'paused' : 'running') });
|
source.highlight.push({ line, type: metadata.error ? 'error' : (paused ? 'paused' : 'running') });
|
||||||
if (paused)
|
source.revealLine = line;
|
||||||
source.revealLine = line;
|
fileToSelect = source.file;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._pushAllSources();
|
this._pushAllSources();
|
||||||
|
if (fileToSelect)
|
||||||
|
this._recorderApp?.setFile(fileToSelect);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _pushAllSources() {
|
private _pushAllSources() {
|
||||||
|
|
|
||||||
|
|
@ -51,12 +51,12 @@
|
||||||
|
|
||||||
.source-line-paused {
|
.source-line-paused {
|
||||||
background-color: #b3dbff7f;
|
background-color: #b3dbff7f;
|
||||||
outline: 1px solid #009aff;
|
outline: 1px solid #008aff;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.source-line-error {
|
.source-line-error {
|
||||||
background-color: #fff0f0;
|
background-color: #fff0f0;
|
||||||
outline: 1px solid #ffd6d6;
|
outline: 1px solid #ff5656;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
import { Story, Meta } from '@storybook/react/types-6-0';
|
import { Story, Meta } from '@storybook/react/types-6-0';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Source, SourceProps } from './source';
|
import { Source, SourceProps } from './source';
|
||||||
import { exampleText } from './exampleText';
|
import { exampleText } from './source.example';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Components/Source',
|
title: 'Components/Source',
|
||||||
|
|
@ -37,9 +37,29 @@ Primary.args = {
|
||||||
text: exampleText()
|
text: exampleText()
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HighlightLine = Template.bind({});
|
export const RunningOnLine = Template.bind({});
|
||||||
HighlightLine.args = {
|
RunningOnLine.args = {
|
||||||
language: 'javascript',
|
language: 'javascript',
|
||||||
text: exampleText(),
|
text: exampleText(),
|
||||||
highlightedLine: 11
|
highlight: [
|
||||||
|
{ line: 15, type: 'running' },
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PausedOnLine = Template.bind({});
|
||||||
|
PausedOnLine.args = {
|
||||||
|
language: 'javascript',
|
||||||
|
text: exampleText(),
|
||||||
|
highlight: [
|
||||||
|
{ line: 15, type: 'paused' },
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ErrorOnLine = Template.bind({});
|
||||||
|
ErrorOnLine.args = {
|
||||||
|
language: 'javascript',
|
||||||
|
text: exampleText(),
|
||||||
|
highlight: [
|
||||||
|
{ line: 15, type: 'error' },
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
|
||||||
78
src/web/recorder/callLog.css
Normal file
78
src/web/recorder/callLog.css
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.call-log {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: auto;
|
||||||
|
line-height: 20px;
|
||||||
|
white-space: pre;
|
||||||
|
background: white;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.call-log-message {
|
||||||
|
flex: none;
|
||||||
|
padding: 3px 0 3px 36px;
|
||||||
|
display: flex;
|
||||||
|
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;
|
||||||
|
flex-direction: column;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.call-log-call-header {
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 2px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.call-log-call .codicon {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.call-log .codicon-check {
|
||||||
|
color: #21a945;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.call-log-call.error {
|
||||||
|
background-color: #fff0f0;
|
||||||
|
border-top: 1px solid #ffd6d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.call-log-call.error .call-log-call-header,
|
||||||
|
.call-log-message.error,
|
||||||
|
.call-log .codicon-error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
62
src/web/recorder/callLog.example.ts
Normal file
62
src/web/recorder/callLog.example.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CallLog } from '../../server/supplements/recorder/recorderTypes';
|
||||||
|
|
||||||
|
export function exampleCallLog(): CallLog[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'id': 3,
|
||||||
|
'messages': [],
|
||||||
|
'title': 'newPage',
|
||||||
|
'status': 'done'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 4,
|
||||||
|
'messages': [
|
||||||
|
'navigating to "https://github.com/microsoft", waiting until "load"',
|
||||||
|
],
|
||||||
|
'title': 'goto',
|
||||||
|
'status': 'done'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 5,
|
||||||
|
'messages': [
|
||||||
|
'waiting for selector "input[aria-label="Find a repository…"]"',
|
||||||
|
' selector resolved to visible <input name="q" value=" type="search" autocomplete="of…/>',
|
||||||
|
'attempting click action',
|
||||||
|
' waiting for element to be visible, enabled and stable',
|
||||||
|
' element is visible, enabled and stable',
|
||||||
|
' scrolling into view if needed',
|
||||||
|
' done scrolling',
|
||||||
|
' checking that element receives pointer events at (351.6,291)',
|
||||||
|
' element does receive pointer events',
|
||||||
|
' performing click action'
|
||||||
|
],
|
||||||
|
'title': 'click',
|
||||||
|
'status': 'paused'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 5,
|
||||||
|
'messages': [
|
||||||
|
'navigating to "https://github.com/microsoft", waiting until "load"',
|
||||||
|
],
|
||||||
|
'error': 'Error occured',
|
||||||
|
'title': 'error',
|
||||||
|
'status': 'error'
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
37
src/web/recorder/callLog.stories.tsx
Normal file
37
src/web/recorder/callLog.stories.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Story, Meta } from '@storybook/react/types-6-0';
|
||||||
|
import React from 'react';
|
||||||
|
import { CallLogProps, CallLogView } from './callLog';
|
||||||
|
import { exampleCallLog } from './callLog.example';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Recorder/CallLog',
|
||||||
|
component: CallLogView,
|
||||||
|
parameters: {
|
||||||
|
viewport: {
|
||||||
|
defaultViewport: 'recorder'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
const Template: Story<CallLogProps> = args => <CallLogView {...args} />;
|
||||||
|
|
||||||
|
export const Primary = Template.bind({});
|
||||||
|
Primary.args = {
|
||||||
|
log: exampleCallLog()
|
||||||
|
};
|
||||||
63
src/web/recorder/callLog.tsx
Normal file
63
src/web/recorder/callLog.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import './callLog.css';
|
||||||
|
import * as React from 'react';
|
||||||
|
import type { CallLog } from '../../server/supplements/recorder/recorderTypes';
|
||||||
|
|
||||||
|
export interface CallLogProps {
|
||||||
|
log: CallLog[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CallLogView: React.FC<CallLogProps> = ({
|
||||||
|
log
|
||||||
|
}) => {
|
||||||
|
const messagesEndRef = React.createRef<HTMLDivElement>();
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
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 => {
|
||||||
|
return <div className={`call-log-call ${callLog.status}`} key={callLog.id}>
|
||||||
|
<div className='call-log-call-header'>
|
||||||
|
<span className={'codicon ' + iconClass(callLog)}></span>{ callLog.title }
|
||||||
|
</div>
|
||||||
|
{ callLog.messages.map((message, i) => {
|
||||||
|
return <div className='call-log-message' key={i}>
|
||||||
|
{ message.trim() }
|
||||||
|
</div>;
|
||||||
|
})}
|
||||||
|
{ callLog.error ? <div className='call-log-message error'>
|
||||||
|
{ callLog.error }
|
||||||
|
</div> : undefined }
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
<div ref={messagesEndRef}></div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function iconClass(callLog: CallLog): string {
|
||||||
|
switch (callLog.status) {
|
||||||
|
case 'done': return 'codicon-check';
|
||||||
|
case 'in-progress': return 'codicon-clock';
|
||||||
|
case 'paused': return 'codicon-debug-pause';
|
||||||
|
case 'error': return 'codicon-error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,7 @@ import * as React from 'react';
|
||||||
import * as ReactDOM from 'react-dom';
|
import * as ReactDOM from 'react-dom';
|
||||||
import { applyTheme } from '../theme';
|
import { applyTheme } from '../theme';
|
||||||
import '../common.css';
|
import '../common.css';
|
||||||
import { Recorder } from './recorder';
|
import { Main } from './main';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|
@ -28,5 +28,5 @@ declare global {
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
applyTheme();
|
applyTheme();
|
||||||
ReactDOM.render(<Recorder/>, document.querySelector('#root'));
|
ReactDOM.render(<Main/>, document.querySelector('#root'));
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
52
src/web/recorder/main.tsx
Normal file
52
src/web/recorder/main.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import './recorder.css';
|
||||||
|
import * as React from 'react';
|
||||||
|
import type { CallLog, Mode, Source } from '../../server/supplements/recorder/recorderTypes';
|
||||||
|
import { Recorder } from './recorder';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
playwrightSetMode: (mode: Mode) => void;
|
||||||
|
playwrightSetPaused: (paused: boolean) => void;
|
||||||
|
playwrightSetSources: (sources: Source[]) => void;
|
||||||
|
playwrightUpdateLogs: (callLogs: CallLog[]) => void;
|
||||||
|
dispatch(data: any): Promise<void>;
|
||||||
|
playwrightSourcesEchoForTest: Source[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Main: React.FC = ({
|
||||||
|
}) => {
|
||||||
|
const [sources, setSources] = React.useState<Source[]>([]);
|
||||||
|
const [paused, setPaused] = React.useState(false);
|
||||||
|
const [log, setLog] = React.useState(new Map<number, CallLog>());
|
||||||
|
const [mode, setMode] = React.useState<Mode>('none');
|
||||||
|
|
||||||
|
window.playwrightSetMode = setMode;
|
||||||
|
window.playwrightSetSources = setSources;
|
||||||
|
window.playwrightSetPaused = setPaused;
|
||||||
|
window.playwrightUpdateLogs = callLogs => {
|
||||||
|
const newLog = new Map<number, CallLog>(log);
|
||||||
|
for (const callLog of callLogs)
|
||||||
|
newLog.set(callLog.id, callLog);
|
||||||
|
setLog(newLog);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.playwrightSourcesEchoForTest = sources;
|
||||||
|
return <Recorder sources={sources} paused={paused} log={log} mode={mode}/>;
|
||||||
|
};
|
||||||
|
|
@ -30,65 +30,10 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recorder-log {
|
.recorder-chooser {
|
||||||
display: flex;
|
border: none;
|
||||||
flex-direction: column;
|
background: none;
|
||||||
flex: auto;
|
outline: none;
|
||||||
line-height: 20px;
|
|
||||||
white-space: pre;
|
|
||||||
background: white;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recorder-log-message {
|
|
||||||
flex: none;
|
|
||||||
padding: 3px 0 3px 36px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recorder-log-header {
|
|
||||||
color: var(--toolbar-color);
|
color: var(--toolbar-color);
|
||||||
box-shadow: var(--box-shadow);
|
margin-left: 16px;
|
||||||
background-color: var(--toolbar-bg-color);
|
|
||||||
height: 32px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 9px;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recorder-log-call {
|
|
||||||
display: flex;
|
|
||||||
flex: none;
|
|
||||||
flex-direction: column;
|
|
||||||
border-top: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recorder-log-call-header {
|
|
||||||
height: 24px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 2px;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recorder-log-call .codicon {
|
|
||||||
padding: 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recorder-log .codicon-check {
|
|
||||||
color: #21a945;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recorder-log-call.error {
|
|
||||||
background-color: #fff0f0;
|
|
||||||
border-top: 1px solid #ffd6d6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recorder-log-call.error .recorder-log-call-header,
|
|
||||||
.recorder-log-message.error,
|
|
||||||
.recorder-log .codicon-error {
|
|
||||||
color: red;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import { Story, Meta } from '@storybook/react/types-6-0';
|
import { Story, Meta } from '@storybook/react/types-6-0';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { exampleCallLog } from './callLog.example';
|
||||||
import { Recorder, RecorderProps } from './recorder';
|
import { Recorder, RecorderProps } from './recorder';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -32,4 +33,53 @@ const Template: Story<RecorderProps> = args => <Recorder {...args} />;
|
||||||
|
|
||||||
export const Primary = Template.bind({});
|
export const Primary = Template.bind({});
|
||||||
Primary.args = {
|
Primary.args = {
|
||||||
|
sources: [],
|
||||||
|
paused: false,
|
||||||
|
log: [],
|
||||||
|
mode: 'none'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OneSource = Template.bind({});
|
||||||
|
OneSource.args = {
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
file: '<one>',
|
||||||
|
text: '// Text One',
|
||||||
|
language: 'javascript',
|
||||||
|
highlight: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
paused: false,
|
||||||
|
log: [],
|
||||||
|
mode: 'none'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TwoSources = Template.bind({});
|
||||||
|
TwoSources.args = {
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
file: '<one>',
|
||||||
|
text: '// Text One',
|
||||||
|
language: 'javascript',
|
||||||
|
highlight: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: '<two>',
|
||||||
|
text: '// Text Two',
|
||||||
|
language: 'javascript',
|
||||||
|
highlight: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
paused: false,
|
||||||
|
log: [],
|
||||||
|
mode: 'none'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithLog = Template.bind({});
|
||||||
|
WithLog.args = {
|
||||||
|
sources: [
|
||||||
|
],
|
||||||
|
paused: false,
|
||||||
|
log: exampleCallLog(),
|
||||||
|
mode: 'none'
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -21,50 +21,35 @@ import { ToolbarButton } from '../components/toolbarButton';
|
||||||
import { Source as SourceView } from '../components/source';
|
import { Source as SourceView } from '../components/source';
|
||||||
import type { CallLog, Mode, Source } from '../../server/supplements/recorder/recorderTypes';
|
import type { CallLog, Mode, Source } from '../../server/supplements/recorder/recorderTypes';
|
||||||
import { SplitView } from '../components/splitView';
|
import { SplitView } from '../components/splitView';
|
||||||
|
import { CallLogView } from './callLog';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
playwrightSetMode: (mode: Mode) => void;
|
playwrightSetFile: (file: string) => void;
|
||||||
playwrightSetPaused: (paused: boolean) => void;
|
|
||||||
playwrightSetSources: (sources: Source[]) => void;
|
|
||||||
playwrightUpdateLogs: (callLogs: CallLog[]) => void;
|
|
||||||
dispatch(data: any): Promise<void>;
|
|
||||||
playwrightSourcesEchoForTest: Source[];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecorderProps {
|
export interface RecorderProps {
|
||||||
|
sources: Source[],
|
||||||
|
paused: boolean,
|
||||||
|
log: Map<number, CallLog>,
|
||||||
|
mode: Mode
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Recorder: React.FC<RecorderProps> = ({
|
export const Recorder: React.FC<RecorderProps> = ({
|
||||||
|
sources,
|
||||||
|
paused,
|
||||||
|
log,
|
||||||
|
mode
|
||||||
}) => {
|
}) => {
|
||||||
const [sources, setSources] = React.useState<Source[]>([]);
|
const [f, setFile] = React.useState<string | undefined>();
|
||||||
const [paused, setPaused] = React.useState(false);
|
window.playwrightSetFile = setFile;
|
||||||
const [log, setLog] = React.useState(new Map<number, CallLog>());
|
const file = f || sources[0]?.file;
|
||||||
const [mode, setMode] = React.useState<Mode>('none');
|
|
||||||
|
|
||||||
window.playwrightSetMode = setMode;
|
const source = sources.find(s => s.file === file) || {
|
||||||
window.playwrightSetSources = setSources;
|
|
||||||
window.playwrightSetPaused = setPaused;
|
|
||||||
window.playwrightUpdateLogs = callLogs => {
|
|
||||||
const newLog = new Map<number, CallLog>(log);
|
|
||||||
for (const callLog of callLogs)
|
|
||||||
newLog.set(callLog.id, callLog);
|
|
||||||
setLog(newLog);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.playwrightSourcesEchoForTest = sources;
|
|
||||||
const source = sources.find(source => {
|
|
||||||
let s = sources.find(s => s.revealLine);
|
|
||||||
if (!s)
|
|
||||||
s = sources.find(s => s.file === source.file);
|
|
||||||
if (!s)
|
|
||||||
s = sources[0];
|
|
||||||
return s;
|
|
||||||
}) || {
|
|
||||||
file: 'untitled',
|
|
||||||
text: '',
|
text: '',
|
||||||
language: 'javascript',
|
language: 'javascript',
|
||||||
|
file: '',
|
||||||
highlight: []
|
highlight: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -81,48 +66,35 @@ export const Recorder: React.FC<RecorderProps> = ({
|
||||||
<ToolbarButton icon='question' title='Inspect' toggled={mode == 'inspecting'} onClick={() => {
|
<ToolbarButton icon='question' title='Inspect' toggled={mode == 'inspecting'} onClick={() => {
|
||||||
window.dispatch({ event: 'setMode', params: { mode: mode === 'inspecting' ? 'none' : 'inspecting' }}).catch(() => { });
|
window.dispatch({ event: 'setMode', params: { mode: mode === 'inspecting' ? 'none' : 'inspecting' }}).catch(() => { });
|
||||||
}}></ToolbarButton>
|
}}></ToolbarButton>
|
||||||
<ToolbarButton icon='files' title='Copy' disabled={!source.text} onClick={() => {
|
<ToolbarButton icon='files' title='Copy' disabled={!source || !source.text} onClick={() => {
|
||||||
copy(source.text);
|
copy(source.text);
|
||||||
}}></ToolbarButton>
|
}}></ToolbarButton>
|
||||||
<ToolbarButton icon='debug-continue' title='Resume' disabled={!paused} onClick={() => {
|
<ToolbarButton icon='debug-continue' title='Resume' disabled={!paused} onClick={() => {
|
||||||
setPaused(false);
|
|
||||||
window.dispatch({ event: 'resume' }).catch(() => {});
|
window.dispatch({ event: 'resume' }).catch(() => {});
|
||||||
}}></ToolbarButton>
|
}}></ToolbarButton>
|
||||||
<ToolbarButton icon='debug-pause' title='Pause' disabled={paused} onClick={() => {
|
<ToolbarButton icon='debug-pause' title='Pause' disabled={paused} onClick={() => {
|
||||||
window.dispatch({ event: 'pause' }).catch(() => {});
|
window.dispatch({ event: 'pause' }).catch(() => {});
|
||||||
}}></ToolbarButton>
|
}}></ToolbarButton>
|
||||||
<ToolbarButton icon='debug-step-over' title='Step over' disabled={!paused} onClick={() => {
|
<ToolbarButton icon='debug-step-over' title='Step over' disabled={!paused} onClick={() => {
|
||||||
setPaused(false);
|
|
||||||
window.dispatch({ event: 'step' }).catch(() => {});
|
window.dispatch({ event: 'step' }).catch(() => {});
|
||||||
}}></ToolbarButton>
|
}}></ToolbarButton>
|
||||||
|
<select className='recorder-chooser' hidden={!sources.length} onChange={event => {
|
||||||
|
setFile(event.target.selectedOptions[0].value);
|
||||||
|
}}>{
|
||||||
|
sources.map(s => {
|
||||||
|
const title = s.file.replace(/.*[/\\]([^/\\]+)/, '$1');
|
||||||
|
return <option key={s.file} value={s.file} selected={s.file === file}>{title}</option>;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</select>
|
||||||
<div style={{flex: 'auto'}}></div>
|
<div style={{flex: 'auto'}}></div>
|
||||||
<ToolbarButton icon='clear-all' title='Clear' disabled={!source.text} onClick={() => {
|
<ToolbarButton icon='clear-all' title='Clear' disabled={!source || !source.text} onClick={() => {
|
||||||
window.dispatch({ event: 'clear' }).catch(() => {});
|
window.dispatch({ event: 'clear' }).catch(() => {});
|
||||||
}}></ToolbarButton>
|
}}></ToolbarButton>
|
||||||
</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>
|
||||||
<div className='vbox'>
|
<CallLogView log={[...log.values()]}/>
|
||||||
<div className='recorder-log-header' style={{flex: 'none'}}>Log</div>
|
|
||||||
<div className='recorder-log' style={{flex: 'auto'}}>
|
|
||||||
{[...log.values()].map(callLog => {
|
|
||||||
return <div className={`recorder-log-call ${callLog.status}`} key={callLog.id}>
|
|
||||||
<div className='recorder-log-call-header'>
|
|
||||||
<span className={'codicon ' + iconClass(callLog)}></span>{ callLog.title }
|
|
||||||
</div>
|
|
||||||
{ callLog.messages.map((message, i) => {
|
|
||||||
return <div className='recorder-log-message' key={i}>
|
|
||||||
{ message.trim() }
|
|
||||||
</div>;
|
|
||||||
})}
|
|
||||||
{ callLog.error ? <div className='recorder-log-message error'>
|
|
||||||
{ callLog.error }
|
|
||||||
</div> : undefined }
|
|
||||||
</div>
|
|
||||||
})}
|
|
||||||
<div ref={messagesEndRef}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SplitView>
|
</SplitView>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
@ -137,12 +109,3 @@ function copy(text: string) {
|
||||||
document.execCommand('copy');
|
document.execCommand('copy');
|
||||||
textArea.remove();
|
textArea.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
function iconClass(callLog: CallLog): string {
|
|
||||||
switch (callLog.status) {
|
|
||||||
case 'done': return 'codicon-check';
|
|
||||||
case 'in-progress': return 'codicon-clock';
|
|
||||||
case 'paused': return 'codicon-debug-pause';
|
|
||||||
case 'error': return 'codicon-error';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -226,7 +226,7 @@ describe('pause', (suite, { mode }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
async function sanitizeLog(recorderPage: Page): Promise<string[]> {
|
async function sanitizeLog(recorderPage: Page): Promise<string[]> {
|
||||||
const text = await recorderPage.innerText('.recorder-log');
|
const text = await recorderPage.innerText('.call-log');
|
||||||
return text.split('\n').filter(l => {
|
return text.split('\n').filter(l => {
|
||||||
return l !== 'element is not stable - waiting...';
|
return l !== 'element is not stable - waiting...';
|
||||||
}).map(l => {
|
}).map(l => {
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ DEPS['src/cli/driver.ts'] = DEPS['src/inprocess.ts'] = DEPS['src/browserServerIm
|
||||||
|
|
||||||
// Tracing is a client/server plugin, nothing should depend on it.
|
// Tracing is a client/server plugin, nothing should depend on it.
|
||||||
DEPS['src/trace/'] = ['src/common/', 'src/utils/', 'src/client/**', 'src/server/**'];
|
DEPS['src/trace/'] = ['src/common/', 'src/utils/', 'src/client/**', 'src/server/**'];
|
||||||
DEPS['src/web/recorder/'] = ['src/common/', 'src/web/', 'src/web/components/'];
|
DEPS['src/web/recorder/'] = ['src/common/', 'src/web/', 'src/web/components/', 'src/server/supplements/recorder/recorderTypes.ts'];
|
||||||
DEPS['src/web/traceViewer/'] = ['src/common/', 'src/web/', 'src/cli/traceViewer/'];
|
DEPS['src/web/traceViewer/'] = ['src/common/', 'src/web/', 'src/cli/traceViewer/'];
|
||||||
DEPS['src/web/traceViewer/ui/'] = ['src/common/', 'src/web/traceViewer/', 'src/web/', 'src/cli/traceViewer/', 'src/trace/'];
|
DEPS['src/web/traceViewer/ui/'] = ['src/common/', 'src/web/traceViewer/', 'src/web/', 'src/cli/traceViewer/', 'src/trace/'];
|
||||||
// The service is a cross-cutting feature, and so it depends on a bunch of things.
|
// The service is a cross-cutting feature, and so it depends on a bunch of things.
|
||||||
|
|
@ -156,7 +156,6 @@ DEPS['src/service.ts'] = ['src/remote/'];
|
||||||
DEPS['src/cli/'] = ['src/cli/**', 'src/client/**', 'src/install/**', 'src/generated/', 'src/server/injected/', 'src/debug/injected/', 'src/trace/**', 'src/utils/**'];
|
DEPS['src/cli/'] = ['src/cli/**', 'src/client/**', 'src/install/**', 'src/generated/', 'src/server/injected/', 'src/debug/injected/', 'src/trace/**', 'src/utils/**'];
|
||||||
|
|
||||||
DEPS['src/server/supplements/recorder/recorderApp.ts'] = ['src/common/', 'src/utils/', 'src/server/', 'src/server/chromium/'];
|
DEPS['src/server/supplements/recorder/recorderApp.ts'] = ['src/common/', 'src/utils/', 'src/server/', 'src/server/chromium/'];
|
||||||
DEPS['src/web/recorder/recorder.tsx'] = ['src/server/supplements/recorder/recorderTypes.ts'];
|
|
||||||
DEPS['src/utils/'] = ['src/common/'];
|
DEPS['src/utils/'] = ['src/common/'];
|
||||||
|
|
||||||
checkDeps().catch(e => {
|
checkDeps().catch(e => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue