diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 4aab712b9bd642..293af11d6a3f04 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -140,6 +140,8 @@ export class Recorder implements InstrumentationListener, IRecorder { this._contextRecorder.on(ContextRecorder.Events.Change, (data: { sources: Source[], actions: actions.ActionInContext[] }) => { this._recorderSources = data.sources; recorderApp.setActions(data.actions, data.sources); + if (data.sources[0]) + recorderApp.setFile(data.sources[0].id, 'codegen'); this._pushAllSources(); }); @@ -299,7 +301,7 @@ export class Recorder implements InstrumentationListener, IRecorder { } this._pushAllSources(); if (fileToSelect) - this._recorderApp?.setFile(fileToSelect); + this._recorderApp?.setFile(fileToSelect, 'breakpoint'); } private _pushAllSources() { diff --git a/packages/playwright-core/src/server/recorder/recorderApp.ts b/packages/playwright-core/src/server/recorder/recorderApp.ts index 30149f9816ca2b..7baf91885e9353 100644 --- a/packages/playwright-core/src/server/recorder/recorderApp.ts +++ b/packages/playwright-core/src/server/recorder/recorderApp.ts @@ -34,7 +34,7 @@ export class EmptyRecorderApp extends EventEmitter implements IRecorderApp { async close(): Promise {} async setPaused(paused: boolean): Promise {} async setMode(mode: Mode): Promise {} - async setFile(file: string): Promise {} + async setFile(file: string, mode: 'breakpoint' | 'codegen'): Promise {} async setSelector(selector: string, userGesture?: boolean): Promise {} async updateCallLogs(callLogs: CallLog[]): Promise {} async setSources(sources: Source[]): Promise {} @@ -131,10 +131,10 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { }).toString(), { isFunction: true }, mode).catch(() => {}); } - async setFile(file: string): Promise { - await this._page.mainFrame().evaluateExpression(((file: string) => { - window.playwrightSetFile(file); - }).toString(), { isFunction: true }, file).catch(() => {}); + async setFile(file: string, mode: 'breakpoint' | 'codegen'): Promise { + await this._page.mainFrame().evaluateExpression(((params: { file: string, mode: 'breakpoint' | 'codegen' }) => { + window.playwrightSetFile(params.file, params.mode); + }).toString(), { isFunction: true }, { file, mode }).catch(() => {}); } async setPaused(paused: boolean): Promise { diff --git a/packages/playwright-core/src/server/recorder/recorderFrontend.ts b/packages/playwright-core/src/server/recorder/recorderFrontend.ts index 97df1d3ceb6c49..f856bd08d55f7b 100644 --- a/packages/playwright-core/src/server/recorder/recorderFrontend.ts +++ b/packages/playwright-core/src/server/recorder/recorderFrontend.ts @@ -28,7 +28,7 @@ export interface IRecorderApp extends EventEmitter { close(): Promise; setPaused(paused: boolean): Promise; setMode(mode: Mode): Promise; - setFile(file: string): Promise; + setFile(file: string, mode: 'breakpoint' | 'codegen'): Promise; setSelector(selector: string, userGesture?: boolean): Promise; updateCallLogs(callLogs: CallLog[]): Promise; setSources(sources: Source[]): Promise; diff --git a/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts b/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts index ab67fe562cf7f3..6ab381fc2f5b0b 100644 --- a/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts +++ b/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts @@ -66,8 +66,8 @@ export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp this._transport.deliverEvent('setMode', { mode }); } - async setFile(file: string): Promise { - this._transport.deliverEvent('setFileIfNeeded', { file }); + async setFile(file: string, mode: 'breakpoint' | 'codegen'): Promise { + this._transport.deliverEvent('setFile', { file, mode }); } async setSelector(selector: string, userGesture?: boolean): Promise { diff --git a/packages/recorder/src/recorder.tsx b/packages/recorder/src/recorder.tsx index 9d5c0feebac946..11d0af176ced2a 100644 --- a/packages/recorder/src/recorder.tsx +++ b/packages/recorder/src/recorder.tsx @@ -41,13 +41,11 @@ export const Recorder: React.FC = ({ log, mode, }) => { - const [fileId, setFileId] = React.useState(); + const [preferredLanguage, setPreferredLanguage] = React.useState(); + const [selectedFileId, setSelectedFileId] = React.useState(); const [selectedTab, setSelectedTab] = React.useState('log'); - React.useEffect(() => { - if (!fileId && sources.length > 0) - setFileId(sources[0].id); - }, [fileId, sources]); + const fileId = selectedFileId || preferredLanguage || sources[0]?.id; const source = React.useMemo(() => { if (fileId) { @@ -66,7 +64,14 @@ export const Recorder: React.FC = ({ setLocator(asLocator(language, selector)); }; - window.playwrightSetFile = setFileId; + window.playwrightSetFile = React.useCallback((fileId: string, mode: 'breakpoint' | 'codegen') => { + if (mode === 'breakpoint') { + setSelectedFileId(fileId); + } else { + setPreferredLanguage(fileId); + setSelectedFileId(undefined); + } + }, []); const messagesEndRef = React.useRef(null); React.useLayoutEffect(() => { @@ -134,19 +139,19 @@ export const Recorder: React.FC = ({ { copy(source.text); }}> - { + { window.dispatch({ event: 'resume' }); }}> - { + { window.dispatch({ event: 'pause' }); }}> - { + { window.dispatch({ event: 'step' }); }}>
Target:
{ - setFileId(fileId); + setSelectedFileId(fileId); window.dispatch({ event: 'fileChanged', params: { file: fileId } }); }} /> { diff --git a/packages/recorder/src/recorderTypes.ts b/packages/recorder/src/recorderTypes.ts index a5791e2306522c..c65fe874862a96 100644 --- a/packages/recorder/src/recorderTypes.ts +++ b/packages/recorder/src/recorderTypes.ts @@ -96,7 +96,7 @@ declare global { playwrightSetSources: (sources: Source[]) => void; playwrightSetOverlayVisible: (visible: boolean) => void; playwrightUpdateLogs: (callLogs: CallLog[]) => void; - playwrightSetFile: (file: string) => void; + playwrightSetFile: (file: string, mode: 'breakpoint' | 'codegen') => void; playwrightSetSelector: (selector: string, focus?: boolean) => void; playwrightSourcesEchoForTest: Source[]; dispatch(data: any): Promise; diff --git a/packages/web/src/components/sourceChooser.tsx b/packages/web/src/components/sourceChooser.tsx index 0645480a03c65e..22b91c61a511b3 100644 --- a/packages/web/src/components/sourceChooser.tsx +++ b/packages/web/src/components/sourceChooser.tsx @@ -22,7 +22,7 @@ export const SourceChooser: React.FC<{ fileId: string | undefined, setFileId: (fileId: string) => void, }> = ({ sources, fileId, setFileId }) => { - return { setFileId(event.target.selectedOptions[0].value); }}>{renderSourceOptions(sources)}; }; @@ -33,17 +33,21 @@ function renderSourceOptions(sources: Source[]): React.ReactNode { ); - const hasGroup = sources.some(s => s.group); - if (hasGroup) { - const groups = new Set(sources.map(s => s.group)); - return [...groups].filter(Boolean).map(group => ( - - {sources.filter(s => s.group === group).map(source => renderOption(source))} - - )); + const sourcesByGroups = new Map(); + for (const source of sources) { + let list = sourcesByGroups.get(source.group || 'Debugger'); + if (!list) { + list = []; + sourcesByGroups.set(source.group || 'Debugger', list); + } + list.push(source); } - return sources.map(source => renderOption(source)); + return [...sourcesByGroups.entries()].map(([group, sources]) => ( + + {sources.filter(s => (s.group || 'Debugger') === group).map(source => renderOption(source))} + + )); } export function emptySource(): Source { diff --git a/packages/web/src/components/toolbarButton.tsx b/packages/web/src/components/toolbarButton.tsx index 00b9babd599401..184642b395e7f6 100644 --- a/packages/web/src/components/toolbarButton.tsx +++ b/packages/web/src/components/toolbarButton.tsx @@ -28,6 +28,7 @@ export interface ToolbarButtonProps { style?: React.CSSProperties, testId?: string, className?: string, + ariaLabel?: string, } export const ToolbarButton: React.FC> = ({ @@ -40,6 +41,7 @@ export const ToolbarButton: React.FC style, testId, className, + ariaLabel, }) => { return