mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	chore: enable reused browser autoclose (#16363)
This commit is contained in:
		
							parent
							
								
									0fa20d5d1e
								
							
						
					
					
						commit
						c99d6cdd4c
					
				@ -29,6 +29,7 @@ import { Recorder } from '../server/recorder';
 | 
			
		||||
import { EmptyRecorderApp } from '../server/recorder/recorderApp';
 | 
			
		||||
import type { BrowserContext } from '../server/browserContext';
 | 
			
		||||
import { serverSideCallMetadata } from '../server/instrumentation';
 | 
			
		||||
import type { Mode } from '../server/recorder/recorderTypes';
 | 
			
		||||
 | 
			
		||||
export function printApiJson() {
 | 
			
		||||
  // Note: this file is generated by build-playwright-driver.sh
 | 
			
		||||
@ -83,55 +84,94 @@ function selfDestruct() {
 | 
			
		||||
 | 
			
		||||
const internalMetadata = serverSideCallMetadata();
 | 
			
		||||
 | 
			
		||||
class ProtocolHandler {
 | 
			
		||||
  private _playwright: Playwright;
 | 
			
		||||
  private _autoCloseTimer: NodeJS.Timeout | undefined;
 | 
			
		||||
 | 
			
		||||
  constructor(playwright: Playwright) {
 | 
			
		||||
    this._playwright = playwright;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async setMode(params: { mode: Mode, language?: string, file?: string }) {
 | 
			
		||||
    await gc(this._playwright);
 | 
			
		||||
 | 
			
		||||
    if (params.mode === 'none') {
 | 
			
		||||
      for (const recorder of await allRecorders(this._playwright)) {
 | 
			
		||||
        recorder.setHighlightedSelector('');
 | 
			
		||||
        recorder.setMode('none');
 | 
			
		||||
      }
 | 
			
		||||
      this.setAutoClose({ enabled: true });
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const browsers = this._playwright.allBrowsers();
 | 
			
		||||
    if (!browsers.length)
 | 
			
		||||
      await this._playwright.chromium.launch(internalMetadata, { headless: false });
 | 
			
		||||
    // Create page if none.
 | 
			
		||||
    const pages = this._playwright.allPages();
 | 
			
		||||
    if (!pages.length) {
 | 
			
		||||
      const [browser] = this._playwright.allBrowsers();
 | 
			
		||||
      const { context } = await browser.newContextForReuse({}, internalMetadata);
 | 
			
		||||
      await context.newPage(internalMetadata);
 | 
			
		||||
    }
 | 
			
		||||
    // Toggle the mode.
 | 
			
		||||
    for (const recorder of await allRecorders(this._playwright)) {
 | 
			
		||||
      recorder.setHighlightedSelector('');
 | 
			
		||||
      if (params.mode === 'recording')
 | 
			
		||||
        recorder.setOutput(params.language!, params.file);
 | 
			
		||||
      recorder.setMode(params.mode);
 | 
			
		||||
    }
 | 
			
		||||
    this.setAutoClose({ enabled: true });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async setAutoClose(params: { enabled: boolean }) {
 | 
			
		||||
    if (this._autoCloseTimer)
 | 
			
		||||
      clearTimeout(this._autoCloseTimer);
 | 
			
		||||
    if (!params.enabled)
 | 
			
		||||
      return;
 | 
			
		||||
    const heartBeat = () => {
 | 
			
		||||
      if (!this._playwright.allPages().length)
 | 
			
		||||
        selfDestruct();
 | 
			
		||||
      else
 | 
			
		||||
        this._autoCloseTimer = setTimeout(heartBeat, 5000);
 | 
			
		||||
    };
 | 
			
		||||
    this._autoCloseTimer = setTimeout(heartBeat, 30000);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async highlight(params: { selector: string }) {
 | 
			
		||||
    for (const recorder of await allRecorders(this._playwright))
 | 
			
		||||
      recorder.setHighlightedSelector(params.selector);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async kill() {
 | 
			
		||||
    selfDestruct();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function wireController(playwright: Playwright, wsEndpoint: string) {
 | 
			
		||||
  process.send!({ method: 'ready', params: { wsEndpoint } });
 | 
			
		||||
  const handler = new ProtocolHandler(playwright);
 | 
			
		||||
  process.on('message', async message => {
 | 
			
		||||
    try {
 | 
			
		||||
      if (message.method === 'kill') {
 | 
			
		||||
        selfDestruct();
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (message.method === 'inspect') {
 | 
			
		||||
        if (!message.params.enabled) {
 | 
			
		||||
          for (const recorder of await allRecorders(playwright)) {
 | 
			
		||||
            recorder.setHighlightedSelector('');
 | 
			
		||||
            recorder.setMode('none');
 | 
			
		||||
          }
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Create browser if none.
 | 
			
		||||
        const browsers = playwright.allBrowsers();
 | 
			
		||||
        if (!browsers.length)
 | 
			
		||||
          await playwright.chromium.launch(internalMetadata, { headless: false });
 | 
			
		||||
        // Create page if none.
 | 
			
		||||
        const pages = playwright.allPages();
 | 
			
		||||
        if (!pages.length) {
 | 
			
		||||
          const [browser] = playwright.allBrowsers();
 | 
			
		||||
          const { context } = await browser.newContextForReuse({}, internalMetadata);
 | 
			
		||||
          await context.newPage(internalMetadata);
 | 
			
		||||
        }
 | 
			
		||||
        // Toggle inspect mode.
 | 
			
		||||
        for (const recorder of await allRecorders(playwright)) {
 | 
			
		||||
          recorder.setHighlightedSelector('');
 | 
			
		||||
          recorder.setMode('inspecting');
 | 
			
		||||
        }
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (message.method === 'highlight') {
 | 
			
		||||
        for (const recorder of await allRecorders(playwright))
 | 
			
		||||
          recorder.setHighlightedSelector(message.params.selector);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const result = await (handler as any)[message.method](message.params);
 | 
			
		||||
      process.send!({ id: message.id, result });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      process.send!({ method: 'error', params: { error: e.toString() } });
 | 
			
		||||
      process.send!({ id: message.id, error: e.toString() });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function gc(playwright: Playwright) {
 | 
			
		||||
  for (const browser of playwright.allBrowsers()) {
 | 
			
		||||
    for (const context of browser.contexts()) {
 | 
			
		||||
      if (!context.pages().length)
 | 
			
		||||
        await context.close(serverSideCallMetadata());
 | 
			
		||||
    }
 | 
			
		||||
    if (!browser.contexts())
 | 
			
		||||
      await browser.close();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function allRecorders(playwright: Playwright): Promise<Recorder[]> {
 | 
			
		||||
  const contexts = new Set<BrowserContext>();
 | 
			
		||||
  for (const page of playwright.allPages())
 | 
			
		||||
 | 
			
		||||
@ -40,6 +40,7 @@ import { metadataToCallLog } from './recorder/recorderUtils';
 | 
			
		||||
import { Debugger } from './debugger';
 | 
			
		||||
import { EventEmitter } from 'events';
 | 
			
		||||
import { raceAgainstTimeout } from '../utils/timeoutRunner';
 | 
			
		||||
import type { LanguageGenerator } from './recorder/language';
 | 
			
		||||
 | 
			
		||||
type BindingSource = { frame: Frame, page: Page };
 | 
			
		||||
 | 
			
		||||
@ -204,6 +205,10 @@ export class Recorder implements InstrumentationListener {
 | 
			
		||||
    this._refreshOverlay();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setOutput(language: string, outputFile: string | undefined) {
 | 
			
		||||
    this._contextRecorder.setOutput(language, outputFile);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _refreshOverlay() {
 | 
			
		||||
    for (const page of this._context.pages())
 | 
			
		||||
      page.mainFrame().evaluateExpression('window.__pw_refreshOverlay()', false, undefined, 'main').catch(() => {});
 | 
			
		||||
@ -312,35 +317,20 @@ class ContextRecorder extends EventEmitter {
 | 
			
		||||
  private _context: BrowserContext;
 | 
			
		||||
  private _params: channels.BrowserContextRecorderSupplementEnableParams;
 | 
			
		||||
  private _recorderSources: Source[];
 | 
			
		||||
  private _throttledOutputFile: ThrottledFile | null = null;
 | 
			
		||||
  private _orderedLanguages: LanguageGenerator[] = [];
 | 
			
		||||
 | 
			
		||||
  constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) {
 | 
			
		||||
    super();
 | 
			
		||||
    this._context = context;
 | 
			
		||||
    this._params = params;
 | 
			
		||||
    const language = params.language || context._browser.options.sdkLanguage;
 | 
			
		||||
 | 
			
		||||
    const languages = new Set([
 | 
			
		||||
      new JavaLanguageGenerator(),
 | 
			
		||||
      new JavaScriptLanguageGenerator(false),
 | 
			
		||||
      new JavaScriptLanguageGenerator(true),
 | 
			
		||||
      new PythonLanguageGenerator(false, false),
 | 
			
		||||
      new PythonLanguageGenerator(true, false),
 | 
			
		||||
      new PythonLanguageGenerator(false, true),
 | 
			
		||||
      new CSharpLanguageGenerator(),
 | 
			
		||||
    ]);
 | 
			
		||||
    const primaryLanguage = [...languages].find(l => l.id === language)!;
 | 
			
		||||
    if (!primaryLanguage)
 | 
			
		||||
      throw new Error(`\n===============================\nUnsupported language: '${language}'\n===============================\n`);
 | 
			
		||||
 | 
			
		||||
    languages.delete(primaryLanguage);
 | 
			
		||||
    const orderedLanguages = [primaryLanguage, ...languages];
 | 
			
		||||
 | 
			
		||||
    this._recorderSources = [];
 | 
			
		||||
    const language = params.language || context._browser.options.sdkLanguage;
 | 
			
		||||
    this.setOutput(language, params.outputFile);
 | 
			
		||||
    const generator = new CodeGenerator(context._browser.options.name, params.mode === 'recording', params.launchOptions || {}, params.contextOptions || {}, params.device, params.saveStorage);
 | 
			
		||||
    const throttledOutputFile = params.outputFile ? new ThrottledFile(params.outputFile) : null;
 | 
			
		||||
    generator.on('change', () => {
 | 
			
		||||
      this._recorderSources = [];
 | 
			
		||||
      for (const languageGenerator of orderedLanguages) {
 | 
			
		||||
      for (const languageGenerator of this._orderedLanguages) {
 | 
			
		||||
        const source: Source = {
 | 
			
		||||
          isRecorded: true,
 | 
			
		||||
          file: languageGenerator.fileName,
 | 
			
		||||
@ -350,25 +340,43 @@ class ContextRecorder extends EventEmitter {
 | 
			
		||||
        };
 | 
			
		||||
        source.revealLine = source.text.split('\n').length - 1;
 | 
			
		||||
        this._recorderSources.push(source);
 | 
			
		||||
        if (languageGenerator === orderedLanguages[0])
 | 
			
		||||
          throttledOutputFile?.setContent(source.text);
 | 
			
		||||
        if (languageGenerator === this._orderedLanguages[0])
 | 
			
		||||
          this._throttledOutputFile?.setContent(source.text);
 | 
			
		||||
      }
 | 
			
		||||
      this.emit(ContextRecorder.Events.Change, {
 | 
			
		||||
        sources: this._recorderSources,
 | 
			
		||||
        primaryFileName: primaryLanguage.fileName
 | 
			
		||||
        primaryFileName: this._orderedLanguages[0].fileName
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    if (throttledOutputFile) {
 | 
			
		||||
      context.on(BrowserContext.Events.BeforeClose, () => {
 | 
			
		||||
        throttledOutputFile.flush();
 | 
			
		||||
      });
 | 
			
		||||
      process.on('exit', () => {
 | 
			
		||||
        throttledOutputFile.flush();
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    context.on(BrowserContext.Events.BeforeClose, () => {
 | 
			
		||||
      this._throttledOutputFile?.flush();
 | 
			
		||||
    });
 | 
			
		||||
    process.on('exit', () => {
 | 
			
		||||
      this._throttledOutputFile?.flush();
 | 
			
		||||
    });
 | 
			
		||||
    this._generator = generator;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setOutput(language: string, outputFile: string | undefined) {
 | 
			
		||||
    const languages = new Set([
 | 
			
		||||
      new JavaLanguageGenerator(),
 | 
			
		||||
      new JavaScriptLanguageGenerator(false),
 | 
			
		||||
      new JavaScriptLanguageGenerator(true),
 | 
			
		||||
      new PythonLanguageGenerator(false, false),
 | 
			
		||||
      new PythonLanguageGenerator(true, false),
 | 
			
		||||
      new PythonLanguageGenerator(false, true),
 | 
			
		||||
      new CSharpLanguageGenerator(),
 | 
			
		||||
    ]);
 | 
			
		||||
    const primaryLanguage = [...languages].find(l => l.id === language);
 | 
			
		||||
    if (!primaryLanguage)
 | 
			
		||||
      throw new Error(`\n===============================\nUnsupported language: '${language}'\n===============================\n`);
 | 
			
		||||
 | 
			
		||||
    languages.delete(primaryLanguage);
 | 
			
		||||
    this._orderedLanguages = [primaryLanguage, ...languages];
 | 
			
		||||
    this._throttledOutputFile = outputFile ? new ThrottledFile(outputFile) : null;
 | 
			
		||||
    this._generator?.restart();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async install() {
 | 
			
		||||
    this._context.on(BrowserContext.Events.Page, page => this._onPage(page));
 | 
			
		||||
    for (const page of this._context.pages())
 | 
			
		||||
@ -625,7 +633,7 @@ class ThrottledFile {
 | 
			
		||||
  setContent(text: string) {
 | 
			
		||||
    this._text = text;
 | 
			
		||||
    if (!this._timer)
 | 
			
		||||
      this._timer = setTimeout(() => this.flush(), 1000);
 | 
			
		||||
      this._timer = setTimeout(() => this.flush(), 250);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  flush(): void {
 | 
			
		||||
 | 
			
		||||
@ -33,14 +33,14 @@ export class CodeGenerator extends EventEmitter {
 | 
			
		||||
  private _enabled: boolean;
 | 
			
		||||
  private _options: LanguageGeneratorOptions;
 | 
			
		||||
 | 
			
		||||
  constructor(browserName: string, generateHeaders: boolean, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, deviceName: string | undefined, saveStorage: string | undefined) {
 | 
			
		||||
  constructor(browserName: string, enabled: boolean, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, deviceName: string | undefined, saveStorage: string | undefined) {
 | 
			
		||||
    super();
 | 
			
		||||
 | 
			
		||||
    // Make a copy of options to modify them later.
 | 
			
		||||
    launchOptions = { headless: false, ...launchOptions };
 | 
			
		||||
    contextOptions = { ...contextOptions };
 | 
			
		||||
    this._enabled = generateHeaders;
 | 
			
		||||
    this._options = { browserName, generateHeaders, launchOptions, contextOptions, deviceName, saveStorage };
 | 
			
		||||
    this._enabled = enabled;
 | 
			
		||||
    this._options = { browserName, launchOptions, contextOptions, deviceName, saveStorage };
 | 
			
		||||
    this.restart();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -160,15 +160,13 @@ export class CodeGenerator extends EventEmitter {
 | 
			
		||||
 | 
			
		||||
  generateText(languageGenerator: LanguageGenerator) {
 | 
			
		||||
    const text = [];
 | 
			
		||||
    if (this._options.generateHeaders)
 | 
			
		||||
      text.push(languageGenerator.generateHeader(this._options));
 | 
			
		||||
    text.push(languageGenerator.generateHeader(this._options));
 | 
			
		||||
    for (const action of this._actions) {
 | 
			
		||||
      const actionText = languageGenerator.generateAction(action);
 | 
			
		||||
      if (actionText)
 | 
			
		||||
        text.push(actionText);
 | 
			
		||||
    }
 | 
			
		||||
    if (this._options.generateHeaders)
 | 
			
		||||
      text.push(languageGenerator.generateFooter(this._options.saveStorage));
 | 
			
		||||
    text.push(languageGenerator.generateFooter(this._options.saveStorage));
 | 
			
		||||
    return text.join('\n');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,6 @@ import type { Action, DialogSignal, DownloadSignal, NavigationSignal, PopupSigna
 | 
			
		||||
 | 
			
		||||
export type LanguageGeneratorOptions = {
 | 
			
		||||
  browserName: string;
 | 
			
		||||
  generateHeaders: boolean;
 | 
			
		||||
  launchOptions: LaunchOptions;
 | 
			
		||||
  contextOptions: BrowserContextOptions;
 | 
			
		||||
  deviceName?: string;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user