chore: fix codegen selector while debugging (#33099)

Fixes #33052
This commit is contained in:
Pavel Feldman 2024-10-14 14:04:24 -07:00 committed by GitHub
parent ecd147ce43
commit 6cfcbe0d6d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 50 additions and 28 deletions

View file

@ -140,6 +140,7 @@ export class Recorder implements InstrumentationListener, IRecorder {
this._contextRecorder.on(ContextRecorder.Events.Change, (data: { sources: Source[], actions: actions.ActionInContext[] }) => { this._contextRecorder.on(ContextRecorder.Events.Change, (data: { sources: Source[], actions: actions.ActionInContext[] }) => {
this._recorderSources = data.sources; this._recorderSources = data.sources;
recorderApp.setActions(data.actions, data.sources); recorderApp.setActions(data.actions, data.sources);
recorderApp.setRunningFile(undefined);
this._pushAllSources(); this._pushAllSources();
}); });
@ -299,7 +300,7 @@ export class Recorder implements InstrumentationListener, IRecorder {
} }
this._pushAllSources(); this._pushAllSources();
if (fileToSelect) if (fileToSelect)
this._recorderApp?.setFile(fileToSelect); this._recorderApp?.setRunningFile(fileToSelect);
} }
private _pushAllSources() { private _pushAllSources() {

View file

@ -34,7 +34,7 @@ export class EmptyRecorderApp extends EventEmitter implements IRecorderApp {
async close(): Promise<void> {} async close(): Promise<void> {}
async setPaused(paused: boolean): Promise<void> {} async setPaused(paused: boolean): Promise<void> {}
async setMode(mode: Mode): Promise<void> {} async setMode(mode: Mode): Promise<void> {}
async setFile(file: string): Promise<void> {} async setRunningFile(file: string | undefined): Promise<void> {}
async setSelector(selector: string, userGesture?: boolean): Promise<void> {} async setSelector(selector: string, userGesture?: boolean): Promise<void> {}
async updateCallLogs(callLogs: CallLog[]): Promise<void> {} async updateCallLogs(callLogs: CallLog[]): Promise<void> {}
async setSources(sources: Source[]): Promise<void> {} async setSources(sources: Source[]): Promise<void> {}
@ -131,9 +131,9 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
}).toString(), { isFunction: true }, mode).catch(() => {}); }).toString(), { isFunction: true }, mode).catch(() => {});
} }
async setFile(file: string): Promise<void> { async setRunningFile(file: string | undefined): Promise<void> {
await this._page.mainFrame().evaluateExpression(((file: string) => { await this._page.mainFrame().evaluateExpression(((file: string) => {
window.playwrightSetFile(file); window.playwrightSetRunningFile(file);
}).toString(), { isFunction: true }, file).catch(() => {}); }).toString(), { isFunction: true }, file).catch(() => {});
} }

View file

@ -28,7 +28,7 @@ export interface IRecorderApp extends EventEmitter {
close(): Promise<void>; close(): Promise<void>;
setPaused(paused: boolean): Promise<void>; setPaused(paused: boolean): Promise<void>;
setMode(mode: Mode): Promise<void>; setMode(mode: Mode): Promise<void>;
setFile(file: string): Promise<void>; setRunningFile(file: string | undefined): Promise<void>;
setSelector(selector: string, userGesture?: boolean): Promise<void>; setSelector(selector: string, userGesture?: boolean): Promise<void>;
updateCallLogs(callLogs: CallLog[]): Promise<void>; updateCallLogs(callLogs: CallLog[]): Promise<void>;
setSources(sources: Source[]): Promise<void>; setSources(sources: Source[]): Promise<void>;

View file

@ -66,8 +66,8 @@ export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp
this._transport.deliverEvent('setMode', { mode }); this._transport.deliverEvent('setMode', { mode });
} }
async setFile(file: string): Promise<void> { async setRunningFile(file: string | undefined): Promise<void> {
this._transport.deliverEvent('setFileIfNeeded', { file }); this._transport.deliverEvent('setRunningFile', { file });
} }
async setSelector(selector: string, userGesture?: boolean): Promise<void> { async setSelector(selector: string, userGesture?: boolean): Promise<void> {

View file

@ -41,13 +41,11 @@ export const Recorder: React.FC<RecorderProps> = ({
log, log,
mode, mode,
}) => { }) => {
const [fileId, setFileId] = React.useState<string | undefined>(); const [selectedFileId, setSelectedFileId] = React.useState<string | undefined>();
const [runningFileId, setRunningFileId] = React.useState<string | undefined>();
const [selectedTab, setSelectedTab] = React.useState<string>('log'); const [selectedTab, setSelectedTab] = React.useState<string>('log');
React.useEffect(() => { const fileId = selectedFileId || runningFileId || sources[0]?.id;
if (!fileId && sources.length > 0)
setFileId(sources[0].id);
}, [fileId, sources]);
const source = React.useMemo(() => { const source = React.useMemo(() => {
if (fileId) { if (fileId) {
@ -66,7 +64,7 @@ export const Recorder: React.FC<RecorderProps> = ({
setLocator(asLocator(language, selector)); setLocator(asLocator(language, selector));
}; };
window.playwrightSetFile = setFileId; window.playwrightSetRunningFile = setRunningFileId;
const messagesEndRef = React.useRef<HTMLDivElement>(null); const messagesEndRef = React.useRef<HTMLDivElement>(null);
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
@ -134,19 +132,19 @@ export const Recorder: React.FC<RecorderProps> = ({
<ToolbarButton icon='files' title='Copy' disabled={!source || !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 (F8)' disabled={!paused} onClick={() => { <ToolbarButton icon='debug-continue' title='Resume (F8)' ariaLabel='Resume' disabled={!paused} onClick={() => {
window.dispatch({ event: 'resume' }); window.dispatch({ event: 'resume' });
}}></ToolbarButton> }}></ToolbarButton>
<ToolbarButton icon='debug-pause' title='Pause (F8)' disabled={paused} onClick={() => { <ToolbarButton icon='debug-pause' title='Pause (F8)' ariaLabel='Pause' disabled={paused} onClick={() => {
window.dispatch({ event: 'pause' }); window.dispatch({ event: 'pause' });
}}></ToolbarButton> }}></ToolbarButton>
<ToolbarButton icon='debug-step-over' title='Step over (F10)' disabled={!paused} onClick={() => { <ToolbarButton icon='debug-step-over' title='Step over (F10)' ariaLabel='Step over' disabled={!paused} onClick={() => {
window.dispatch({ event: 'step' }); window.dispatch({ event: 'step' });
}}></ToolbarButton> }}></ToolbarButton>
<div style={{ flex: 'auto' }}></div> <div style={{ flex: 'auto' }}></div>
<div>Target:</div> <div>Target:</div>
<SourceChooser fileId={fileId} sources={sources} setFileId={fileId => { <SourceChooser fileId={fileId} sources={sources} setFileId={fileId => {
setFileId(fileId); setSelectedFileId(fileId);
window.dispatch({ event: 'fileChanged', params: { file: fileId } }); window.dispatch({ event: 'fileChanged', params: { file: fileId } });
}} /> }} />
<ToolbarButton icon='clear-all' title='Clear' disabled={!source || !source.text} onClick={() => { <ToolbarButton icon='clear-all' title='Clear' disabled={!source || !source.text} onClick={() => {

View file

@ -96,7 +96,7 @@ declare global {
playwrightSetSources: (sources: Source[]) => void; playwrightSetSources: (sources: Source[]) => void;
playwrightSetOverlayVisible: (visible: boolean) => void; playwrightSetOverlayVisible: (visible: boolean) => void;
playwrightUpdateLogs: (callLogs: CallLog[]) => void; playwrightUpdateLogs: (callLogs: CallLog[]) => void;
playwrightSetFile: (file: string) => void; playwrightSetRunningFile: (file: string | undefined) => void;
playwrightSetSelector: (selector: string, focus?: boolean) => void; playwrightSetSelector: (selector: string, focus?: boolean) => void;
playwrightSourcesEchoForTest: Source[]; playwrightSourcesEchoForTest: Source[];
dispatch(data: any): Promise<void>; dispatch(data: any): Promise<void>;

View file

@ -22,7 +22,7 @@ export const SourceChooser: React.FC<{
fileId: string | undefined, fileId: string | undefined,
setFileId: (fileId: string) => void, setFileId: (fileId: string) => void,
}> = ({ sources, fileId, setFileId }) => { }> = ({ sources, fileId, setFileId }) => {
return <select className='source-chooser' hidden={!sources.length} value={fileId} onChange={event => { return <select className='source-chooser' hidden={!sources.length} title='Source chooser' value={fileId} onChange={event => {
setFileId(event.target.selectedOptions[0].value); setFileId(event.target.selectedOptions[0].value);
}}>{renderSourceOptions(sources)}</select>; }}>{renderSourceOptions(sources)}</select>;
}; };
@ -33,17 +33,21 @@ function renderSourceOptions(sources: Source[]): React.ReactNode {
<option key={source.id} value={source.id}>{transformTitle(source.label)}</option> <option key={source.id} value={source.id}>{transformTitle(source.label)}</option>
); );
const hasGroup = sources.some(s => s.group); const sourcesByGroups = new Map<string, Source[]>();
if (hasGroup) { for (const source of sources) {
const groups = new Set(sources.map(s => s.group)); let list = sourcesByGroups.get(source.group || 'Debugger');
return [...groups].filter(Boolean).map(group => ( if (!list) {
<optgroup label={group} key={group}> list = [];
{sources.filter(s => s.group === group).map(source => renderOption(source))} sourcesByGroups.set(source.group || 'Debugger', list);
</optgroup> }
)); list.push(source);
} }
return sources.map(source => renderOption(source)); return [...sourcesByGroups.entries()].map(([group, sources]) => (
<optgroup label={group} key={group}>
{sources.filter(s => (s.group || 'Debugger') === group).map(source => renderOption(source))}
</optgroup>
));
} }
export function emptySource(): Source { export function emptySource(): Source {

View file

@ -28,6 +28,7 @@ export interface ToolbarButtonProps {
style?: React.CSSProperties, style?: React.CSSProperties,
testId?: string, testId?: string,
className?: string, className?: string,
ariaLabel?: string,
} }
export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>> = ({ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>> = ({
@ -40,6 +41,7 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
style, style,
testId, testId,
className, className,
ariaLabel,
}) => { }) => {
return <button return <button
className={clsx(className, 'toolbar-button', icon, toggled && 'toggled')} className={clsx(className, 'toolbar-button', icon, toggled && 'toggled')}
@ -50,6 +52,7 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
disabled={!!disabled} disabled={!!disabled}
style={style} style={style}
data-testid={testId} data-testid={testId}
aria-label={ariaLabel}
> >
{icon && <span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>} {icon && <span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>}
{children} {children}

View file

@ -103,6 +103,7 @@ it.describe('pause', () => {
await page.pause(); await page.pause();
})(); })();
const recorderPage = await recorderPageGetter(); const recorderPage = await recorderPageGetter();
await expect(recorderPage.getByRole('combobox', { name: 'Source chooser' })).toHaveValue(/pause\.spec\.ts/);
const source = await recorderPage.textContent('.source-line-paused'); const source = await recorderPage.textContent('.source-line-paused');
expect(source).toContain('page.pause()'); expect(source).toContain('page.pause()');
await recorderPage.click('[title="Resume (F8)"]'); await recorderPage.click('[title="Resume (F8)"]');
@ -480,6 +481,21 @@ it.describe('pause', () => {
await recorderPage.click('[title="Resume (F8)"]'); await recorderPage.click('[title="Resume (F8)"]');
await scriptPromise; await scriptPromise;
}); });
it('should record from debugger', async ({ page, recorderPageGetter }) => {
const scriptPromise = (async () => {
await page.pause();
})();
const recorderPage = await recorderPageGetter();
await expect(recorderPage.getByRole('combobox', { name: 'Source chooser' })).toHaveValue(/pause\.spec\.ts/);
await expect(recorderPage.locator('.source-line-paused')).toHaveText(/await page\.pause\(\)/);
await recorderPage.getByRole('button', { name: 'Record' }).click();
await page.locator('body').click();
await expect(recorderPage.getByRole('combobox', { name: 'Source chooser' })).toHaveValue('javascript');
await expect(recorderPage.locator('.cm-wrapper')).toContainText(`await page.locator('body').click();`);
await recorderPage.getByRole('button', { name: 'Resume' }).click();
await scriptPromise;
});
}); });
async function sanitizeLog(recorderPage: Page): Promise<string[]> { async function sanitizeLog(recorderPage: Page): Promise<string[]> {