chore: language specific dropdowns in codegen (#16452)

This commit is contained in:
Max Schmitt 2022-08-15 19:44:46 +02:00 committed by GitHub
parent bd06d1604f
commit 13596b7be3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 78 additions and 34 deletions

View File

@ -251,14 +251,14 @@ export class Recorder implements InstrumentationListener {
const { file, line } = metadata.stack[0]; const { file, line } = metadata.stack[0];
let source = this._userSources.get(file); let source = this._userSources.get(file);
if (!source) { if (!source) {
source = { isRecorded: false, file, text: this._readSource(file), highlight: [], language: languageForFile(file) }; source = { isRecorded: false, label: file, id: file, text: this._readSource(file), highlight: [], language: languageForFile(file) };
this._userSources.set(file, source); this._userSources.set(file, source);
} }
if (line) { if (line) {
const paused = this._debugger.isPaused(metadata); const paused = this._debugger.isPaused(metadata);
source.highlight.push({ line, type: metadata.error ? 'error' : (paused ? 'paused' : 'running') }); source.highlight.push({ line, type: metadata.error ? 'error' : (paused ? 'paused' : 'running') });
source.revealLine = line; source.revealLine = line;
fileToSelect = source.file; fileToSelect = source.id;
} }
} }
this._pushAllSources(); this._pushAllSources();
@ -333,7 +333,9 @@ class ContextRecorder extends EventEmitter {
for (const languageGenerator of this._orderedLanguages) { for (const languageGenerator of this._orderedLanguages) {
const source: Source = { const source: Source = {
isRecorded: true, isRecorded: true,
file: languageGenerator.fileName, label: languageGenerator.name,
group: languageGenerator.groupName,
id: languageGenerator.id,
text: generator.generateText(languageGenerator), text: generator.generateText(languageGenerator),
language: languageGenerator.highlighter, language: languageGenerator.highlighter,
highlight: [] highlight: []
@ -345,7 +347,7 @@ class ContextRecorder extends EventEmitter {
} }
this.emit(ContextRecorder.Events.Change, { this.emit(ContextRecorder.Events.Change, {
sources: this._recorderSources, sources: this._recorderSources,
primaryFileName: this._orderedLanguages[0].fileName primaryFileName: this._orderedLanguages[0].id
}); });
}); });
context.on(BrowserContext.Events.BeforeClose, () => { context.on(BrowserContext.Events.BeforeClose, () => {
@ -362,9 +364,9 @@ class ContextRecorder extends EventEmitter {
new JavaLanguageGenerator(), new JavaLanguageGenerator(),
new JavaScriptLanguageGenerator(false), new JavaScriptLanguageGenerator(false),
new JavaScriptLanguageGenerator(true), new JavaScriptLanguageGenerator(true),
new PythonLanguageGenerator(false, true),
new PythonLanguageGenerator(false, false), new PythonLanguageGenerator(false, false),
new PythonLanguageGenerator(true, false), new PythonLanguageGenerator(true, false),
new PythonLanguageGenerator(false, true),
new CSharpLanguageGenerator(), new CSharpLanguageGenerator(),
]); ]);
const primaryLanguage = [...languages].find(l => l.id === language); const primaryLanguage = [...languages].find(l => l.id === language);

View File

@ -27,7 +27,8 @@ import deviceDescriptors from '../deviceDescriptors';
export class CSharpLanguageGenerator implements LanguageGenerator { export class CSharpLanguageGenerator implements LanguageGenerator {
id = 'csharp'; id = 'csharp';
fileName = 'C#'; groupName = '.NET';
name = 'Library C#';
highlighter = 'csharp'; highlighter = 'csharp';
generateAction(actionInContext: ActionInContext): string { generateAction(actionInContext: ActionInContext): string {

View File

@ -28,7 +28,8 @@ import { escapeWithQuotes } from '../../utils/isomorphic/stringUtils';
export class JavaLanguageGenerator implements LanguageGenerator { export class JavaLanguageGenerator implements LanguageGenerator {
id = 'java'; id = 'java';
fileName = 'Java'; groupName = 'Java';
name = 'Library';
highlighter = 'java'; highlighter = 'java';
generateAction(actionInContext: ActionInContext): string { generateAction(actionInContext: ActionInContext): string {

View File

@ -27,13 +27,14 @@ import { escapeWithQuotes } from '../../utils/isomorphic/stringUtils';
export class JavaScriptLanguageGenerator implements LanguageGenerator { export class JavaScriptLanguageGenerator implements LanguageGenerator {
id: string; id: string;
fileName: string; groupName = 'Node.js';
name: string;
highlighter = 'javascript'; highlighter = 'javascript';
private _isTest: boolean; private _isTest: boolean;
constructor(isTest: boolean) { constructor(isTest: boolean) {
this.id = isTest ? 'test' : 'javascript'; this.id = isTest ? 'test' : 'javascript';
this.fileName = isTest ? 'Playwright Test' : 'JavaScript'; this.name = isTest ? 'Test Runner' : 'Library';
this._isTest = isTest; this._isTest = isTest;
} }

View File

@ -28,7 +28,8 @@ export type LanguageGeneratorOptions = {
export interface LanguageGenerator { export interface LanguageGenerator {
id: string; id: string;
fileName: string; groupName: string;
name: string;
highlighter: string; highlighter: string;
generateHeader(options: LanguageGeneratorOptions): string; generateHeader(options: LanguageGeneratorOptions): string;
generateAction(actionInContext: ActionInContext): string; generateAction(actionInContext: ActionInContext): string;

View File

@ -26,8 +26,9 @@ import { escapeWithQuotes } from '../../utils/isomorphic/stringUtils';
import deviceDescriptors from '../deviceDescriptors'; import deviceDescriptors from '../deviceDescriptors';
export class PythonLanguageGenerator implements LanguageGenerator { export class PythonLanguageGenerator implements LanguageGenerator {
id = 'python'; id: string;
fileName = 'Python'; groupName = 'Python';
name: string;
highlighter = 'python'; highlighter = 'python';
private _awaitPrefix: '' | 'await '; private _awaitPrefix: '' | 'await ';
@ -37,7 +38,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
constructor(isAsync: boolean, isPyTest: boolean) { constructor(isAsync: boolean, isPyTest: boolean) {
this.id = isPyTest ? 'pytest' : (isAsync ? 'python-async' : 'python'); this.id = isPyTest ? 'pytest' : (isAsync ? 'python-async' : 'python');
this.fileName = isPyTest ? 'Pytest' : (isAsync ? 'Python Async' : 'Python'); this.name = isPyTest ? 'Pytest' : (isAsync ? 'Library Async' : 'Library');
this._isAsync = isAsync; this._isAsync = isAsync;
this._isPyTest = isPyTest; this._isPyTest = isPyTest;
this._awaitPrefix = isAsync ? 'await ' : ''; this._awaitPrefix = isAsync ? 'await ' : '';

View File

@ -53,9 +53,12 @@ export type SourceHighlight = {
export type Source = { export type Source = {
isRecorded: boolean; isRecorded: boolean;
file: string; id: string;
label: string;
text: string; text: string;
language: string; language: string;
highlight: SourceHighlight[]; highlight: SourceHighlight[];
revealLine?: number; revealLine?: number;
// used to group the language generators
group?: string;
}; };

View File

@ -53,22 +53,27 @@ export const Recorder: React.FC<RecorderProps> = ({
setFocusSelectorInput(!!focus); setFocusSelectorInput(!!focus);
}; };
const [f, setFile] = React.useState<string | undefined>(); const [fileId, setFileId] = React.useState<string | undefined>();
const file = f || sources[0]?.file;
const source = sources.find(s => s.file === file) || { React.useEffect(() => {
if (!fileId && sources.length > 0)
setFileId(sources[0].id);
}, [fileId, sources]);
const source: Source = sources.find(s => s.id === fileId) || {
id: 'default',
isRecorded: false, isRecorded: false,
text: '', text: '',
language: 'javascript', language: 'javascript',
file: '', label: '',
highlight: [] highlight: []
}; };
window.playwrightSetFileIfNeeded = (value: string) => { window.playwrightSetFileIfNeeded = (value: string) => {
const newSource = sources.find(s => s.file === value); const newSource = sources.find(s => s.id === value);
// Do not forcefully switch between two recorded sources, because // Do not forcefully switch between two recorded sources, because
// user did explicitly choose one. // user did explicitly choose one.
if (newSource && !newSource.isRecorded || !source.isRecorded) if (newSource && !newSource.isRecorded || !source.isRecorded)
setFile(value); setFileId(value);
}; };
const messagesEndRef = React.createRef<HTMLDivElement>(); const messagesEndRef = React.createRef<HTMLDivElement>();
@ -125,15 +130,9 @@ export const Recorder: React.FC<RecorderProps> = ({
}}></ToolbarButton> }}></ToolbarButton>
<div style={{ flex: 'auto' }}></div> <div style={{ flex: 'auto' }}></div>
<div>Target:</div> <div>Target:</div>
<select className='recorder-chooser' hidden={!sources.length} value={file} onChange={event => { <select className='recorder-chooser' hidden={!sources.length} value={fileId} onChange={event => {
setFile(event.target.selectedOptions[0].value); setFileId(event.target.selectedOptions[0].value);
}}>{ }}>{renderSourceOptions(sources)}</select>
sources.map(s => {
const title = s.file.replace(/.*[/\\]([^/\\]+)/, '$1');
return <option key={s.file} value={s.file}>{title}</option>;
})
}
</select>
<ToolbarButton icon='clear-all' title='Clear' disabled={!source || !source.text} onClick={() => { <ToolbarButton icon='clear-all' title='Clear' disabled={!source || !source.text} onClick={() => {
window.dispatch({ event: 'clear' }); window.dispatch({ event: 'clear' });
}}></ToolbarButton> }}></ToolbarButton>
@ -159,6 +158,25 @@ export const Recorder: React.FC<RecorderProps> = ({
</div>; </div>;
}; };
function renderSourceOptions(sources: Source[]): React.ReactNode {
const transformTitle = (title: string): string => title.replace(/.*[/\\]([^/\\]+)/, '$1');
const renderOption = (source: Source): React.ReactNode => (
<option key={source.id} value={source.id}>{transformTitle(source.label)}</option>
);
const hasGroup = sources.some(s => s.group);
if (hasGroup) {
const groups = new Set(sources.map(s => s.group));
return Array.from(groups).map(group => (
<optgroup label={group} key={group}>
{sources.filter(s => s.group === group).map(source => renderOption(source))}
</optgroup>
));
}
return sources.map(source => renderOption(source));
}
function copy(text: string) { function copy(text: string) {
const textArea = document.createElement('textarea'); const textArea = document.createElement('textarea');
textArea.style.position = 'absolute'; textArea.style.position = 'absolute';

View File

@ -28,6 +28,17 @@ type CLITestArgs = {
runCLI: (args: string[], options?: { noAutoExit?: boolean }) => CLIMock; runCLI: (args: string[], options?: { noAutoExit?: boolean }) => CLIMock;
}; };
const codegenLang2Id: Map<string, string> = new Map([
['JavaScript', 'javascript'],
['Java', 'java'],
['Python', 'python'],
['Python Async', 'python-async'],
['Pytest', 'pytest'],
['C#', 'csharp'],
['Playwright Test', 'test'],
]);
const codegenLangId2lang = new Map([...codegenLang2Id.entries()].map(([lang, langId]) => [langId, lang]));
const playwrightToAutomateInspector = require('../../../packages/playwright-core/lib/inProcessFactory').createInProcessPlaywright(); const playwrightToAutomateInspector = require('../../../packages/playwright-core/lib/inProcessFactory').createInProcessPlaywright();
export const test = contextTest.extend<CLITestArgs>({ export const test = contextTest.extend<CLITestArgs>({
@ -115,14 +126,19 @@ class Recorder {
} }
async waitForOutput(file: string, text: string): Promise<Map<string, Source>> { async waitForOutput(file: string, text: string): Promise<Map<string, Source>> {
const handle = await this.recorderPage.waitForFunction((params: { text: string, file: string }) => { if (!codegenLang2Id.has(file))
throw new Error(`Unknown language: ${file}`);
const handle = await this.recorderPage.waitForFunction((params: { text: string, languageId: string }) => {
const w = window as any; const w = window as any;
const source = (w.playwrightSourcesEchoForTest || []).find((s: Source) => s.file === params.file); const source = (w.playwrightSourcesEchoForTest || []).find((s: Source) => s.id === params.languageId);
return source && source.text.includes(params.text) ? w.playwrightSourcesEchoForTest : null; return source && source.text.includes(params.text) ? w.playwrightSourcesEchoForTest : null;
}, { text, file }, { timeout: 8000, polling: 300 }); }, { text, languageId: codegenLang2Id.get(file) }, { timeout: 8000, polling: 300 });
const sources: Source[] = await handle.jsonValue(); const sources: Source[] = await handle.jsonValue();
for (const source of sources) for (const source of sources) {
this._sources.set(source.file, source); if (!codegenLangId2lang.has(source.id))
throw new Error(`Unknown language: ${source.id}`);
this._sources.set(codegenLangId2lang.get(source.id), source);
}
return this._sources; return this._sources;
} }