diff --git a/src/server/supplements/recorder/recorderActions.ts b/src/server/supplements/recorder/recorderActions.ts index a430763961..f119e5564f 100644 --- a/src/server/supplements/recorder/recorderActions.ts +++ b/src/server/supplements/recorder/recorderActions.ts @@ -94,7 +94,7 @@ export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageActi export type BaseSignal = { isAsync?: boolean, -} +}; export type NavigationSignal = BaseSignal & { name: 'navigation', diff --git a/src/server/supplements/recorder/recorderApp.ts b/src/server/supplements/recorder/recorderApp.ts index 4d95ba043c..7359f8ac41 100644 --- a/src/server/supplements/recorder/recorderApp.ts +++ b/src/server/supplements/recorder/recorderApp.ts @@ -31,6 +31,7 @@ const readFileAsync = util.promisify(fs.readFile); declare global { interface Window { + playwrightSetFile: (file: string) => void; playwrightSetMode: (mode: Mode) => void; playwrightSetPaused: (paused: boolean) => void; playwrightSetSources: (sources: Source[]) => void; @@ -123,6 +124,12 @@ export class RecorderApp extends EventEmitter { }).toString(), true, mode, 'main').catch(() => {}); } + async setFile(file: string): Promise { + await this._page.mainFrame()._evaluateExpression(((file: string) => { + window.playwrightSetFile(file); + }).toString(), true, file, 'main').catch(() => {}); + } + async setPaused(paused: boolean): Promise { await this._page.mainFrame()._evaluateExpression(((paused: boolean) => { window.playwrightSetPaused(paused); diff --git a/src/server/supplements/recorderSupplement.ts b/src/server/supplements/recorderSupplement.ts index 7fcbcddd4d..250d1f16a9 100644 --- a/src/server/supplements/recorderSupplement.ts +++ b/src/server/supplements/recorderSupplement.ts @@ -49,7 +49,7 @@ export class RecorderSupplement { private _params: channels.BrowserContextRecorderSupplementEnableParams; private _currentCallsMetadata = new Map(); private _pausedCallsMetadata = new Map void>(); - private _pauseOnNextStatement = true; + private _pauseOnNextStatement = false; private _recorderSources: Source[]; private _userSources = new Map(); @@ -104,6 +104,7 @@ export class RecorderSupplement { text = source.text; } this._pushAllSources(); + this._recorderApp?.setFile(primaryLanguage.fileName); }); if (params.outputFile) { context.on(BrowserContext.Events.BeforeClose, () => { @@ -216,12 +217,12 @@ export class RecorderSupplement { private async _resume(step: boolean) { this._pauseOnNextStatement = step; + this._recorderApp?.setPaused(false); for (const callback of this._pausedCallsMetadata.values()) callback(); this._pausedCallsMetadata.clear(); - this._recorderApp?.setPaused(false); this._updateUserSources(); this.updateCallLog([...this._currentCallsMetadata.keys()]); } @@ -369,6 +370,7 @@ export class RecorderSupplement { } // Apply new decorations. + let fileToSelect = undefined; for (const metadata of this._currentCallsMetadata.keys()) { if (!metadata.stack || !metadata.stack[0]) continue; @@ -381,11 +383,13 @@ export class RecorderSupplement { if (line) { const paused = this._pausedCallsMetadata.has(metadata); source.highlight.push({ line, type: metadata.error ? 'error' : (paused ? 'paused' : 'running') }); - if (paused) - source.revealLine = line; + source.revealLine = line; + fileToSelect = source.file; } } this._pushAllSources(); + if (fileToSelect) + this._recorderApp?.setFile(fileToSelect); } private _pushAllSources() { diff --git a/src/web/components/source.css b/src/web/components/source.css index bbe1d98f34..2e0eed549a 100644 --- a/src/web/components/source.css +++ b/src/web/components/source.css @@ -51,12 +51,12 @@ .source-line-paused { background-color: #b3dbff7f; - outline: 1px solid #009aff; + outline: 1px solid #008aff; z-index: 2; } .source-line-error { background-color: #fff0f0; - outline: 1px solid #ffd6d6; + outline: 1px solid #ff5656; z-index: 2; } diff --git a/src/web/components/exampleText.ts b/src/web/components/source.example.ts similarity index 100% rename from src/web/components/exampleText.ts rename to src/web/components/source.example.ts diff --git a/src/web/components/source.stories.tsx b/src/web/components/source.stories.tsx index 82d0760305..b7ea5fda8a 100644 --- a/src/web/components/source.stories.tsx +++ b/src/web/components/source.stories.tsx @@ -17,7 +17,7 @@ import { Story, Meta } from '@storybook/react/types-6-0'; import React from 'react'; import { Source, SourceProps } from './source'; -import { exampleText } from './exampleText'; +import { exampleText } from './source.example'; export default { title: 'Components/Source', @@ -37,9 +37,29 @@ Primary.args = { text: exampleText() }; -export const HighlightLine = Template.bind({}); -HighlightLine.args = { +export const RunningOnLine = Template.bind({}); +RunningOnLine.args = { language: 'javascript', 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' }, + ] }; diff --git a/src/web/recorder/callLog.css b/src/web/recorder/callLog.css new file mode 100644 index 0000000000..7a90ca65c0 --- /dev/null +++ b/src/web/recorder/callLog.css @@ -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; +} diff --git a/src/web/recorder/callLog.example.ts b/src/web/recorder/callLog.example.ts new file mode 100644 index 0000000000..3f241d5f0f --- /dev/null +++ b/src/web/recorder/callLog.example.ts @@ -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 = args => ; + +export const Primary = Template.bind({}); +Primary.args = { + log: exampleCallLog() +}; diff --git a/src/web/recorder/callLog.tsx b/src/web/recorder/callLog.tsx new file mode 100644 index 0000000000..b454e704c2 --- /dev/null +++ b/src/web/recorder/callLog.tsx @@ -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 = ({ + log +}) => { + const messagesEndRef = React.createRef(); + React.useLayoutEffect(() => { + messagesEndRef.current?.scrollIntoView({ block: 'center', inline: 'nearest' }); + }, [messagesEndRef]); + + return
+
Log
+
+ {log.map(callLog => { + return
+
+ { callLog.title } +
+ { callLog.messages.map((message, i) => { + return
+ { message.trim() } +
; + })} + { callLog.error ?
+ { callLog.error } +
: undefined } +
+ })} +
+
+
; +}; + +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'; + } +} diff --git a/src/web/recorder/index.tsx b/src/web/recorder/index.tsx index a84d0e3d2f..8f7b5f2b42 100644 --- a/src/web/recorder/index.tsx +++ b/src/web/recorder/index.tsx @@ -19,7 +19,7 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { applyTheme } from '../theme'; import '../common.css'; -import { Recorder } from './recorder'; +import { Main } from './main'; declare global { interface Window { @@ -28,5 +28,5 @@ declare global { (async () => { applyTheme(); - ReactDOM.render(, document.querySelector('#root')); + ReactDOM.render(
, document.querySelector('#root')); })(); diff --git a/src/web/recorder/main.tsx b/src/web/recorder/main.tsx new file mode 100644 index 0000000000..de063c37b8 --- /dev/null +++ b/src/web/recorder/main.tsx @@ -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; + playwrightSourcesEchoForTest: Source[]; + } +} + +export const Main: React.FC = ({ +}) => { + const [sources, setSources] = React.useState([]); + const [paused, setPaused] = React.useState(false); + const [log, setLog] = React.useState(new Map()); + const [mode, setMode] = React.useState('none'); + + window.playwrightSetMode = setMode; + window.playwrightSetSources = setSources; + window.playwrightSetPaused = setPaused; + window.playwrightUpdateLogs = callLogs => { + const newLog = new Map(log); + for (const callLog of callLogs) + newLog.set(callLog.id, callLog); + setLog(newLog); + }; + + window.playwrightSourcesEchoForTest = sources; + return ; +}; diff --git a/src/web/recorder/recorder.css b/src/web/recorder/recorder.css index a079deec76..1bf20b2ef1 100644 --- a/src/web/recorder/recorder.css +++ b/src/web/recorder/recorder.css @@ -30,65 +30,10 @@ white-space: nowrap; } -.recorder-log { - display: flex; - flex-direction: column; - flex: auto; - 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 { +.recorder-chooser { + border: none; + background: none; + outline: none; 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; -} - -.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; + margin-left: 16px; } diff --git a/src/web/recorder/recorder.stories.tsx b/src/web/recorder/recorder.stories.tsx index 0831f5ca65..9c67ee5948 100644 --- a/src/web/recorder/recorder.stories.tsx +++ b/src/web/recorder/recorder.stories.tsx @@ -16,6 +16,7 @@ import { Story, Meta } from '@storybook/react/types-6-0'; import React from 'react'; +import { exampleCallLog } from './callLog.example'; import { Recorder, RecorderProps } from './recorder'; export default { @@ -32,4 +33,53 @@ const Template: Story = args => ; export const Primary = Template.bind({}); Primary.args = { + sources: [], + paused: false, + log: [], + mode: 'none' +}; + +export const OneSource = Template.bind({}); +OneSource.args = { + sources: [ + { + file: '', + text: '// Text One', + language: 'javascript', + highlight: [], + }, + ], + paused: false, + log: [], + mode: 'none' +}; + +export const TwoSources = Template.bind({}); +TwoSources.args = { + sources: [ + { + file: '', + text: '// Text One', + language: 'javascript', + highlight: [], + }, + { + file: '', + 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' }; diff --git a/src/web/recorder/recorder.tsx b/src/web/recorder/recorder.tsx index cb9341d9dd..e1482fea90 100644 --- a/src/web/recorder/recorder.tsx +++ b/src/web/recorder/recorder.tsx @@ -21,50 +21,35 @@ import { ToolbarButton } from '../components/toolbarButton'; import { Source as SourceView } from '../components/source'; import type { CallLog, Mode, Source } from '../../server/supplements/recorder/recorderTypes'; import { SplitView } from '../components/splitView'; +import { CallLogView } from './callLog'; declare global { interface Window { - playwrightSetMode: (mode: Mode) => void; - playwrightSetPaused: (paused: boolean) => void; - playwrightSetSources: (sources: Source[]) => void; - playwrightUpdateLogs: (callLogs: CallLog[]) => void; - dispatch(data: any): Promise; - playwrightSourcesEchoForTest: Source[]; + playwrightSetFile: (file: string) => void; } } export interface RecorderProps { + sources: Source[], + paused: boolean, + log: Map, + mode: Mode } export const Recorder: React.FC = ({ + sources, + paused, + log, + mode }) => { - const [sources, setSources] = React.useState([]); - const [paused, setPaused] = React.useState(false); - const [log, setLog] = React.useState(new Map()); - const [mode, setMode] = React.useState('none'); + const [f, setFile] = React.useState(); + window.playwrightSetFile = setFile; + const file = f || sources[0]?.file; - window.playwrightSetMode = setMode; - window.playwrightSetSources = setSources; - window.playwrightSetPaused = setPaused; - window.playwrightUpdateLogs = callLogs => { - const newLog = new Map(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', + const source = sources.find(s => s.file === file) || { text: '', language: 'javascript', + file: '', highlight: [] }; @@ -81,48 +66,35 @@ export const Recorder: React.FC = ({ { window.dispatch({ event: 'setMode', params: { mode: mode === 'inspecting' ? 'none' : 'inspecting' }}).catch(() => { }); }}> - { + { copy(source.text); }}> { - setPaused(false); window.dispatch({ event: 'resume' }).catch(() => {}); }}> { window.dispatch({ event: 'pause' }).catch(() => {}); }}> { - setPaused(false); window.dispatch({ event: 'step' }).catch(() => {}); }}> +
- { + { window.dispatch({ event: 'clear' }).catch(() => {}); }}> -
-
Log
-
- {[...log.values()].map(callLog => { - return
-
- { callLog.title } -
- { callLog.messages.map((message, i) => { - return
- { message.trim() } -
; - })} - { callLog.error ?
- { callLog.error } -
: undefined } -
- })} -
-
-
+
; }; @@ -137,12 +109,3 @@ function copy(text: string) { document.execCommand('copy'); 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'; - } -} \ No newline at end of file diff --git a/test/pause.spec.ts b/test/pause.spec.ts index 7537b731b4..7683cc5264 100644 --- a/test/pause.spec.ts +++ b/test/pause.spec.ts @@ -226,7 +226,7 @@ describe('pause', (suite, { mode }) => { }); async function sanitizeLog(recorderPage: Page): Promise { - const text = await recorderPage.innerText('.recorder-log'); + const text = await recorderPage.innerText('.call-log'); return text.split('\n').filter(l => { return l !== 'element is not stable - waiting...'; }).map(l => { diff --git a/utils/check_deps.js b/utils/check_deps.js index 9f57cc87cb..6fb041d49e 100644 --- a/utils/check_deps.js +++ b/utils/check_deps.js @@ -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. 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/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. @@ -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/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/']; checkDeps().catch(e => {