mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	chore: language specific dropdowns in codegen (#16452)
This commit is contained in:
		
							parent
							
								
									bd06d1604f
								
							
						
					
					
						commit
						13596b7be3
					
				@ -251,14 +251,14 @@ export class Recorder implements InstrumentationListener {
 | 
			
		||||
      const { file, line } = metadata.stack[0];
 | 
			
		||||
      let source = this._userSources.get(file);
 | 
			
		||||
      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);
 | 
			
		||||
      }
 | 
			
		||||
      if (line) {
 | 
			
		||||
        const paused = this._debugger.isPaused(metadata);
 | 
			
		||||
        source.highlight.push({ line, type: metadata.error ? 'error' : (paused ? 'paused' : 'running') });
 | 
			
		||||
        source.revealLine = line;
 | 
			
		||||
        fileToSelect = source.file;
 | 
			
		||||
        fileToSelect = source.id;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this._pushAllSources();
 | 
			
		||||
@ -333,7 +333,9 @@ class ContextRecorder extends EventEmitter {
 | 
			
		||||
      for (const languageGenerator of this._orderedLanguages) {
 | 
			
		||||
        const source: Source = {
 | 
			
		||||
          isRecorded: true,
 | 
			
		||||
          file: languageGenerator.fileName,
 | 
			
		||||
          label: languageGenerator.name,
 | 
			
		||||
          group: languageGenerator.groupName,
 | 
			
		||||
          id: languageGenerator.id,
 | 
			
		||||
          text: generator.generateText(languageGenerator),
 | 
			
		||||
          language: languageGenerator.highlighter,
 | 
			
		||||
          highlight: []
 | 
			
		||||
@ -345,7 +347,7 @@ class ContextRecorder extends EventEmitter {
 | 
			
		||||
      }
 | 
			
		||||
      this.emit(ContextRecorder.Events.Change, {
 | 
			
		||||
        sources: this._recorderSources,
 | 
			
		||||
        primaryFileName: this._orderedLanguages[0].fileName
 | 
			
		||||
        primaryFileName: this._orderedLanguages[0].id
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    context.on(BrowserContext.Events.BeforeClose, () => {
 | 
			
		||||
@ -362,9 +364,9 @@ class ContextRecorder extends EventEmitter {
 | 
			
		||||
      new JavaLanguageGenerator(),
 | 
			
		||||
      new JavaScriptLanguageGenerator(false),
 | 
			
		||||
      new JavaScriptLanguageGenerator(true),
 | 
			
		||||
      new PythonLanguageGenerator(false, true),
 | 
			
		||||
      new PythonLanguageGenerator(false, false),
 | 
			
		||||
      new PythonLanguageGenerator(true, false),
 | 
			
		||||
      new PythonLanguageGenerator(false, true),
 | 
			
		||||
      new CSharpLanguageGenerator(),
 | 
			
		||||
    ]);
 | 
			
		||||
    const primaryLanguage = [...languages].find(l => l.id === language);
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,8 @@ import deviceDescriptors from '../deviceDescriptors';
 | 
			
		||||
 | 
			
		||||
export class CSharpLanguageGenerator implements LanguageGenerator {
 | 
			
		||||
  id = 'csharp';
 | 
			
		||||
  fileName = 'C#';
 | 
			
		||||
  groupName = '.NET';
 | 
			
		||||
  name = 'Library C#';
 | 
			
		||||
  highlighter = 'csharp';
 | 
			
		||||
 | 
			
		||||
  generateAction(actionInContext: ActionInContext): string {
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,8 @@ import { escapeWithQuotes } from '../../utils/isomorphic/stringUtils';
 | 
			
		||||
 | 
			
		||||
export class JavaLanguageGenerator implements LanguageGenerator {
 | 
			
		||||
  id = 'java';
 | 
			
		||||
  fileName = 'Java';
 | 
			
		||||
  groupName = 'Java';
 | 
			
		||||
  name = 'Library';
 | 
			
		||||
  highlighter = 'java';
 | 
			
		||||
 | 
			
		||||
  generateAction(actionInContext: ActionInContext): string {
 | 
			
		||||
 | 
			
		||||
@ -27,13 +27,14 @@ import { escapeWithQuotes } from '../../utils/isomorphic/stringUtils';
 | 
			
		||||
 | 
			
		||||
export class JavaScriptLanguageGenerator implements LanguageGenerator {
 | 
			
		||||
  id: string;
 | 
			
		||||
  fileName: string;
 | 
			
		||||
  groupName = 'Node.js';
 | 
			
		||||
  name: string;
 | 
			
		||||
  highlighter = 'javascript';
 | 
			
		||||
  private _isTest: boolean;
 | 
			
		||||
 | 
			
		||||
  constructor(isTest: boolean) {
 | 
			
		||||
    this.id = isTest ? 'test' : 'javascript';
 | 
			
		||||
    this.fileName = isTest ? 'Playwright Test' : 'JavaScript';
 | 
			
		||||
    this.name = isTest ? 'Test Runner' : 'Library';
 | 
			
		||||
    this._isTest = isTest;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,8 @@ export type LanguageGeneratorOptions = {
 | 
			
		||||
 | 
			
		||||
export interface LanguageGenerator {
 | 
			
		||||
  id: string;
 | 
			
		||||
  fileName: string;
 | 
			
		||||
  groupName: string;
 | 
			
		||||
  name: string;
 | 
			
		||||
  highlighter: string;
 | 
			
		||||
  generateHeader(options: LanguageGeneratorOptions): string;
 | 
			
		||||
  generateAction(actionInContext: ActionInContext): string;
 | 
			
		||||
 | 
			
		||||
@ -26,8 +26,9 @@ import { escapeWithQuotes } from '../../utils/isomorphic/stringUtils';
 | 
			
		||||
import deviceDescriptors from '../deviceDescriptors';
 | 
			
		||||
 | 
			
		||||
export class PythonLanguageGenerator implements LanguageGenerator {
 | 
			
		||||
  id = 'python';
 | 
			
		||||
  fileName = 'Python';
 | 
			
		||||
  id: string;
 | 
			
		||||
  groupName = 'Python';
 | 
			
		||||
  name: string;
 | 
			
		||||
  highlighter = 'python';
 | 
			
		||||
 | 
			
		||||
  private _awaitPrefix: '' | 'await ';
 | 
			
		||||
@ -37,7 +38,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
 | 
			
		||||
 | 
			
		||||
  constructor(isAsync: boolean, isPyTest: boolean) {
 | 
			
		||||
    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._isPyTest = isPyTest;
 | 
			
		||||
    this._awaitPrefix = isAsync ? 'await ' : '';
 | 
			
		||||
 | 
			
		||||
@ -53,9 +53,12 @@ export type SourceHighlight = {
 | 
			
		||||
 | 
			
		||||
export type Source = {
 | 
			
		||||
  isRecorded: boolean;
 | 
			
		||||
  file: string;
 | 
			
		||||
  id: string;
 | 
			
		||||
  label: string;
 | 
			
		||||
  text: string;
 | 
			
		||||
  language: string;
 | 
			
		||||
  highlight: SourceHighlight[];
 | 
			
		||||
  revealLine?: number;
 | 
			
		||||
  // used to group the language generators
 | 
			
		||||
  group?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -53,22 +53,27 @@ export const Recorder: React.FC<RecorderProps> = ({
 | 
			
		||||
    setFocusSelectorInput(!!focus);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const [f, setFile] = React.useState<string | undefined>();
 | 
			
		||||
  const file = f || sources[0]?.file;
 | 
			
		||||
  const [fileId, setFileId] = React.useState<string | undefined>();
 | 
			
		||||
 | 
			
		||||
  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,
 | 
			
		||||
    text: '',
 | 
			
		||||
    language: 'javascript',
 | 
			
		||||
    file: '',
 | 
			
		||||
    label: '',
 | 
			
		||||
    highlight: []
 | 
			
		||||
  };
 | 
			
		||||
  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
 | 
			
		||||
    // user did explicitly choose one.
 | 
			
		||||
    if (newSource && !newSource.isRecorded || !source.isRecorded)
 | 
			
		||||
      setFile(value);
 | 
			
		||||
      setFileId(value);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const messagesEndRef = React.createRef<HTMLDivElement>();
 | 
			
		||||
@ -125,15 +130,9 @@ export const Recorder: React.FC<RecorderProps> = ({
 | 
			
		||||
      }}></ToolbarButton>
 | 
			
		||||
      <div style={{ flex: 'auto' }}></div>
 | 
			
		||||
      <div>Target:</div>
 | 
			
		||||
      <select className='recorder-chooser' hidden={!sources.length} value={file} onChange={event => {
 | 
			
		||||
        setFile(event.target.selectedOptions[0].value);
 | 
			
		||||
      }}>{
 | 
			
		||||
          sources.map(s => {
 | 
			
		||||
            const title = s.file.replace(/.*[/\\]([^/\\]+)/, '$1');
 | 
			
		||||
            return <option key={s.file} value={s.file}>{title}</option>;
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
      </select>
 | 
			
		||||
      <select className='recorder-chooser' hidden={!sources.length} value={fileId} onChange={event => {
 | 
			
		||||
        setFileId(event.target.selectedOptions[0].value);
 | 
			
		||||
      }}>{renderSourceOptions(sources)}</select>
 | 
			
		||||
      <ToolbarButton icon='clear-all' title='Clear' disabled={!source || !source.text} onClick={() => {
 | 
			
		||||
        window.dispatch({ event: 'clear' });
 | 
			
		||||
      }}></ToolbarButton>
 | 
			
		||||
@ -159,6 +158,25 @@ export const Recorder: React.FC<RecorderProps> = ({
 | 
			
		||||
  </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) {
 | 
			
		||||
  const textArea = document.createElement('textarea');
 | 
			
		||||
  textArea.style.position = 'absolute';
 | 
			
		||||
 | 
			
		||||
@ -28,6 +28,17 @@ type CLITestArgs = {
 | 
			
		||||
  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();
 | 
			
		||||
 | 
			
		||||
export const test = contextTest.extend<CLITestArgs>({
 | 
			
		||||
@ -115,14 +126,19 @@ class Recorder {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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 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;
 | 
			
		||||
    }, { text, file }, { timeout: 8000, polling: 300 });
 | 
			
		||||
    }, { text, languageId: codegenLang2Id.get(file) }, { timeout: 8000, polling: 300 });
 | 
			
		||||
    const sources: Source[] = await handle.jsonValue();
 | 
			
		||||
    for (const source of sources)
 | 
			
		||||
      this._sources.set(source.file, source);
 | 
			
		||||
    for (const source of sources) {
 | 
			
		||||
      if (!codegenLangId2lang.has(source.id))
 | 
			
		||||
        throw new Error(`Unknown language: ${source.id}`);
 | 
			
		||||
      this._sources.set(codegenLangId2lang.get(source.id), source);
 | 
			
		||||
    }
 | 
			
		||||
    return this._sources;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user