parent
ecd147ce43
commit
6cfcbe0d6d
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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={() => {
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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[]> {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue