UNPKG

playwright-core

Version:

A high-level API to automate web browsers

469 lines • 20.4 kB
"use strict"; /** * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.RecorderSupplement = void 0; const fs = __importStar(require("fs")); const codeGenerator_1 = require("./recorder/codeGenerator"); const utils_1 = require("./recorder/utils"); const page_1 = require("../page"); const frames_1 = require("../frames"); const browserContext_1 = require("../browserContext"); const javascript_1 = require("./recorder/javascript"); const csharp_1 = require("./recorder/csharp"); const python_1 = require("./recorder/python"); const recorderSource = __importStar(require("../../generated/recorderSource")); const consoleApiSource = __importStar(require("../../generated/consoleApiSource")); const recorderApp_1 = require("./recorder/recorderApp"); const instrumentation_1 = require("../instrumentation"); const utils_2 = require("../../utils/utils"); const symbol = Symbol('RecorderSupplement'); class RecorderSupplement { constructor(context, params) { this._pageAliases = new Map(); this._lastPopupOrdinal = 0; this._lastDialogOrdinal = 0; this._timers = new Set(); this._highlightedSelector = ''; this._recorderApp = null; this._currentCallsMetadata = new Map(); this._pausedCallsMetadata = new Map(); this._userSources = new Map(); this._context = context; this._params = params; this._mode = params.startRecording ? 'recording' : 'none'; this._pauseOnNextStatement = !!params.pauseOnNextStatement; const language = params.language || context._options.sdkLanguage; const languages = new Set([ new javascript_1.JavaScriptLanguageGenerator(), new python_1.PythonLanguageGenerator(false), new python_1.PythonLanguageGenerator(true), new csharp_1.CSharpLanguageGenerator(), ]); const primaryLanguage = [...languages].find(l => l.id === language); if (!primaryLanguage) throw new Error(`\n===============================\nInvalid target: '${params.language}'\n===============================\n`); languages.delete(primaryLanguage); const orderedLanguages = [primaryLanguage, ...languages]; this._recorderSources = []; const generator = new codeGenerator_1.CodeGenerator(context._browser.options.name, !!params.startRecording, params.launchOptions || {}, params.contextOptions || {}, params.device, params.saveStorage); let text = ''; generator.on('change', () => { var _a; this._recorderSources = []; for (const languageGenerator of orderedLanguages) { const source = { file: languageGenerator.fileName, text: generator.generateText(languageGenerator), language: languageGenerator.highlighter, highlight: [] }; source.revealLine = source.text.split('\n').length - 1; this._recorderSources.push(source); if (languageGenerator === orderedLanguages[0]) text = source.text; } this._pushAllSources(); (_a = this._recorderApp) === null || _a === void 0 ? void 0 : _a.setFile(primaryLanguage.fileName); }); if (params.outputFile) { context.on(browserContext_1.BrowserContext.Events.BeforeClose, () => { fs.writeFileSync(params.outputFile, text); text = ''; }); process.on('exit', () => { if (text) fs.writeFileSync(params.outputFile, text); }); } this._generator = generator; } static getOrCreate(context, params = {}) { let recorderPromise = context[symbol]; if (!recorderPromise) { const recorder = new RecorderSupplement(context, params); recorderPromise = recorder.install().then(() => recorder); context[symbol] = recorderPromise; } return recorderPromise; } static getNoCreate(context) { return context[symbol]; } async install() { const recorderApp = await recorderApp_1.RecorderApp.open(this._context); this._recorderApp = recorderApp; recorderApp.once('close', () => { this._recorderApp = null; }); recorderApp.on('event', (data) => { if (data.event === 'setMode') { this._setMode(data.params.mode); return; } if (data.event === 'selectorUpdated') { this._highlightedSelector = data.params.selector; return; } if (data.event === 'step') { this._resume(true); return; } if (data.event === 'resume') { this._resume(false); return; } if (data.event === 'pause') { this._pauseOnNextStatement = true; return; } if (data.event === 'clear') { this._clearScript(); return; } }); await Promise.all([ recorderApp.setMode(this._mode), recorderApp.setPaused(!!this._pausedCallsMetadata.size), this._pushAllSources() ]); this._context.on(browserContext_1.BrowserContext.Events.Page, page => this._onPage(page)); for (const page of this._context.pages()) this._onPage(page); this._context.once(browserContext_1.BrowserContext.Events.Close, () => { for (const timer of this._timers) clearTimeout(timer); this._timers.clear(); recorderApp.close().catch(() => { }); }); // Input actions that potentially lead to navigation are intercepted on the page and are // performed by the Playwright. await this._context.exposeBinding('_playwrightRecorderPerformAction', false, (source, action) => this._performAction(source.frame, action)); // Other non-essential actions are simply being recorded. await this._context.exposeBinding('_playwrightRecorderRecordAction', false, (source, action) => this._recordAction(source.frame, action)); // Commits last action so that no further signals are added to it. await this._context.exposeBinding('_playwrightRecorderCommitAction', false, (source, action) => this._generator.commitLastAction()); await this._context.exposeBinding('_playwrightRecorderState', false, source => { let actionPoint = undefined; let actionSelector = undefined; for (const [metadata, sdkObject] of this._currentCallsMetadata) { if (source.page === sdkObject.attribution.page) { actionPoint = metadata.point || actionPoint; actionSelector = metadata.params.selector || actionSelector; } } const uiState = { mode: this._mode, actionPoint, actionSelector: this._highlightedSelector || actionSelector }; return uiState; }); await this._context.exposeBinding('_playwrightRecorderSetSelector', false, async (_, selector) => { var _a, _b; this._setMode('none'); await ((_a = this._recorderApp) === null || _a === void 0 ? void 0 : _a.setSelector(selector, true)); await ((_b = this._recorderApp) === null || _b === void 0 ? void 0 : _b.bringToFront()); }); await this._context.exposeBinding('_playwrightResume', false, () => { this._resume(false).catch(() => { }); }); await this._context.extendInjectedScript(recorderSource.source, { isUnderTest: utils_2.isUnderTest() }); await this._context.extendInjectedScript(consoleApiSource.source); this._context.recorderAppForTest = recorderApp; } async pause(metadata) { const result = new Promise(f => { this._pausedCallsMetadata.set(metadata, f); }); this._recorderApp.setPaused(true); metadata.pauseStartTime = utils_2.monotonicTime(); this._updateUserSources(); this.updateCallLog([metadata]); return result; } _setMode(mode) { var _a; this._mode = mode; (_a = this._recorderApp) === null || _a === void 0 ? void 0 : _a.setMode(this._mode); this._generator.setEnabled(this._mode === 'recording'); if (this._mode !== 'none') this._context.pages()[0].bringToFront().catch(() => { }); } async _resume(step) { var _a; this._pauseOnNextStatement = step; (_a = this._recorderApp) === null || _a === void 0 ? void 0 : _a.setPaused(false); const endTime = utils_2.monotonicTime(); for (const [metadata, callback] of this._pausedCallsMetadata) { metadata.pauseEndTime = endTime; callback(); } this._pausedCallsMetadata.clear(); this._updateUserSources(); this.updateCallLog([...this._currentCallsMetadata.keys()]); } async _onPage(page) { // First page is called page, others are called popup1, popup2, etc. const frame = page.mainFrame(); page.on('close', () => { this._pageAliases.delete(page); this._generator.addAction({ pageAlias, ...utils_1.describeFrame(page.mainFrame()), committed: true, action: { name: 'closePage', signals: [], } }); }); frame.on(frames_1.Frame.Events.Navigation, () => this._onFrameNavigated(frame, page)); page.on(page_1.Page.Events.Download, () => this._onDownload(page)); page.on(page_1.Page.Events.Popup, popup => this._onPopup(page, popup)); page.on(page_1.Page.Events.Dialog, () => this._onDialog(page)); const suffix = this._pageAliases.size ? String(++this._lastPopupOrdinal) : ''; const pageAlias = 'page' + suffix; this._pageAliases.set(page, pageAlias); const isPopup = !!await page.opener(); // Could happen due to the await above. if (page.isClosed()) return; if (!isPopup) { this._generator.addAction({ pageAlias, ...utils_1.describeFrame(page.mainFrame()), committed: true, action: { name: 'openPage', url: page.mainFrame().url(), signals: [], } }); } } _clearScript() { this._generator.restart(); if (!!this._params.startRecording) { for (const page of this._context.pages()) this._onFrameNavigated(page.mainFrame(), page); } } async _performAction(frame, action) { const page = frame._page; const actionInContext = { pageAlias: this._pageAliases.get(page), ...utils_1.describeFrame(frame), action }; this._generator.willPerformAction(actionInContext); const noCallMetadata = instrumentation_1.internalCallMetadata(); try { const kActionTimeout = 5000; if (action.name === 'click') { const { options } = utils_1.toClickOptions(action); await frame.click(noCallMetadata, action.selector, { ...options, timeout: kActionTimeout }); } if (action.name === 'press') { const modifiers = utils_1.toModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); await frame.press(noCallMetadata, action.selector, shortcut, { timeout: kActionTimeout }); } if (action.name === 'check') await frame.check(noCallMetadata, action.selector, { timeout: kActionTimeout }); if (action.name === 'uncheck') await frame.uncheck(noCallMetadata, action.selector, { timeout: kActionTimeout }); if (action.name === 'select') await frame.selectOption(noCallMetadata, action.selector, [], action.options.map(value => ({ value })), { timeout: kActionTimeout }); } catch (e) { this._generator.performedActionFailed(actionInContext); return; } const timer = setTimeout(() => { actionInContext.committed = true; this._timers.delete(timer); }, 5000); this._generator.didPerformAction(actionInContext); this._timers.add(timer); } async _recordAction(frame, action) { this._generator.addAction({ pageAlias: this._pageAliases.get(frame._page), ...utils_1.describeFrame(frame), action }); } _onFrameNavigated(frame, page) { const pageAlias = this._pageAliases.get(page); this._generator.signal(pageAlias, frame, { name: 'navigation', url: frame.url() }); } _onPopup(page, popup) { const pageAlias = this._pageAliases.get(page); const popupAlias = this._pageAliases.get(popup); this._generator.signal(pageAlias, page.mainFrame(), { name: 'popup', popupAlias }); } _onDownload(page) { const pageAlias = this._pageAliases.get(page); this._generator.signal(pageAlias, page.mainFrame(), { name: 'download' }); } _onDialog(page) { const pageAlias = this._pageAliases.get(page); this._generator.signal(pageAlias, page.mainFrame(), { name: 'dialog', dialogAlias: String(++this._lastDialogOrdinal) }); } async onBeforeCall(sdkObject, metadata) { var _a; if (this._mode === 'recording') return; this._currentCallsMetadata.set(metadata, sdkObject); this._updateUserSources(); this.updateCallLog([metadata]); if (shouldPauseOnCall(sdkObject, metadata) || (this._pauseOnNextStatement && shouldPauseOnStep(sdkObject, metadata))) await this.pause(metadata); if (metadata.params && metadata.params.selector) { this._highlightedSelector = metadata.params.selector; await ((_a = this._recorderApp) === null || _a === void 0 ? void 0 : _a.setSelector(this._highlightedSelector)); } } async onAfterCall(metadata) { if (this._mode === 'recording') return; if (!metadata.error) this._currentCallsMetadata.delete(metadata); this._pausedCallsMetadata.delete(metadata); this._updateUserSources(); this.updateCallLog([metadata]); } _updateUserSources() { var _a; // Remove old decorations. for (const source of this._userSources.values()) { source.highlight = []; source.revealLine = undefined; } // Apply new decorations. let fileToSelect = undefined; for (const metadata of this._currentCallsMetadata.keys()) { if (!metadata.stack || !metadata.stack[0]) continue; const { file, line } = metadata.stack[0]; let source = this._userSources.get(file); if (!source) { source = { file, text: this._readSource(file), highlight: [], language: languageForFile(file) }; this._userSources.set(file, source); } if (line) { const paused = this._pausedCallsMetadata.has(metadata); source.highlight.push({ line, type: metadata.error ? 'error' : (paused ? 'paused' : 'running') }); source.revealLine = line; fileToSelect = source.file; } } this._pushAllSources(); if (fileToSelect) (_a = this._recorderApp) === null || _a === void 0 ? void 0 : _a.setFile(fileToSelect); } _pushAllSources() { var _a; (_a = this._recorderApp) === null || _a === void 0 ? void 0 : _a.setSources([...this._recorderSources, ...this._userSources.values()]); } async onBeforeInputAction(metadata) { if (this._mode === 'recording') return; if (this._pauseOnNextStatement) await this.pause(metadata); } async updateCallLog(metadatas) { var _a, _b, _c; if (this._mode === 'recording') return; const logs = []; for (const metadata of metadatas) { if (!metadata.method) continue; const title = metadata.apiName || metadata.method; let status = 'done'; if (this._currentCallsMetadata.has(metadata)) status = 'in-progress'; if (this._pausedCallsMetadata.has(metadata)) status = 'paused'; if (metadata.error) status = 'error'; const params = { url: (_a = metadata.params) === null || _a === void 0 ? void 0 : _a.url, selector: (_b = metadata.params) === null || _b === void 0 ? void 0 : _b.selector, }; let duration = metadata.endTime ? metadata.endTime - metadata.startTime : undefined; if (typeof duration === 'number' && metadata.pauseStartTime && metadata.pauseEndTime) { duration -= (metadata.pauseEndTime - metadata.pauseStartTime); duration = Math.max(duration, 0); } logs.push({ id: metadata.id, messages: metadata.log, title, status, error: metadata.error, params, duration }); } (_c = this._recorderApp) === null || _c === void 0 ? void 0 : _c.updateCallLogs(logs); } _readSource(fileName) { try { return fs.readFileSync(fileName, 'utf-8'); } catch (e) { return '// No source available'; } } } exports.RecorderSupplement = RecorderSupplement; function languageForFile(file) { if (file.endsWith('.py')) return 'python'; if (file.endsWith('.java')) return 'java'; if (file.endsWith('.cs')) return 'csharp'; return 'javascript'; } function shouldPauseOnCall(sdkObject, metadata) { var _a; if (!((_a = sdkObject.attribution.browser) === null || _a === void 0 ? void 0 : _a.options.headful) && !utils_2.isUnderTest()) return false; return metadata.method === 'pause'; } function shouldPauseOnStep(sdkObject, metadata) { return metadata.method === 'goto' || metadata.method === 'close'; } //# sourceMappingURL=recorderSupplement.js.map