recorder.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.Recorder = void 0;
  6. var fs = _interopRequireWildcard(require("fs"));
  7. var _codeGenerator = require("./recorder/codeGenerator");
  8. var _utils = require("./recorder/utils");
  9. var _page = require("./page");
  10. var _frames = require("./frames");
  11. var _browserContext = require("./browserContext");
  12. var _java = require("./recorder/java");
  13. var _javascript = require("./recorder/javascript");
  14. var _jsonl = require("./recorder/jsonl");
  15. var _csharp = require("./recorder/csharp");
  16. var _python = require("./recorder/python");
  17. var recorderSource = _interopRequireWildcard(require("../generated/recorderSource"));
  18. var consoleApiSource = _interopRequireWildcard(require("../generated/consoleApiSource"));
  19. var _recorderApp = require("./recorder/recorderApp");
  20. var _utils2 = require("../utils");
  21. var _recorderUtils = require("./recorder/recorderUtils");
  22. var _debugger = require("./debugger");
  23. var _events = require("events");
  24. var _timeoutRunner = require("../utils/timeoutRunner");
  25. var _locatorParser = require("../utils/isomorphic/locatorParser");
  26. var _stringUtils = require("../utils/isomorphic/stringUtils");
  27. var _eventsHelper = require("./../utils/eventsHelper");
  28. function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
  29. function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
  30. /**
  31. * Copyright (c) Microsoft Corporation.
  32. *
  33. * Licensed under the Apache License, Version 2.0 (the "License");
  34. * you may not use this file except in compliance with the License.
  35. * You may obtain a copy of the License at
  36. *
  37. * http://www.apache.org/licenses/LICENSE-2.0
  38. *
  39. * Unless required by applicable law or agreed to in writing, software
  40. * distributed under the License is distributed on an "AS IS" BASIS,
  41. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  42. * See the License for the specific language governing permissions and
  43. * limitations under the License.
  44. */
  45. const recorderSymbol = Symbol('recorderSymbol');
  46. class Recorder {
  47. static setAppFactory(recorderAppFactory) {
  48. Recorder.recorderAppFactory = recorderAppFactory;
  49. }
  50. static showInspector(context) {
  51. const params = {};
  52. if ((0, _utils2.isUnderTest)()) params.language = process.env.TEST_INSPECTOR_LANGUAGE;
  53. Recorder.show(context, params).catch(() => {});
  54. }
  55. static show(context, params = {}) {
  56. let recorderPromise = context[recorderSymbol];
  57. if (!recorderPromise) {
  58. const recorder = new Recorder(context, params);
  59. recorderPromise = recorder.install().then(() => recorder);
  60. context[recorderSymbol] = recorderPromise;
  61. }
  62. return recorderPromise;
  63. }
  64. constructor(context, params) {
  65. this._context = void 0;
  66. this._mode = void 0;
  67. this._highlightedSelector = '';
  68. this._overlayState = {
  69. offsetX: 0
  70. };
  71. this._recorderApp = null;
  72. this._currentCallsMetadata = new Map();
  73. this._recorderSources = [];
  74. this._userSources = new Map();
  75. this._debugger = void 0;
  76. this._contextRecorder = void 0;
  77. this._handleSIGINT = void 0;
  78. this._omitCallTracking = false;
  79. this._currentLanguage = void 0;
  80. this._mode = params.mode || 'none';
  81. this._contextRecorder = new ContextRecorder(context, params);
  82. this._context = context;
  83. this._omitCallTracking = !!params.omitCallTracking;
  84. this._debugger = context.debugger();
  85. this._handleSIGINT = params.handleSIGINT;
  86. context.instrumentation.addListener(this, context);
  87. this._currentLanguage = this._contextRecorder.languageName();
  88. if ((0, _utils2.isUnderTest)()) {
  89. // Most of our tests put elements at the top left, so get out of the way.
  90. this._overlayState.offsetX = 200;
  91. }
  92. }
  93. static async defaultRecorderAppFactory(recorder) {
  94. if (process.env.PW_CODEGEN_NO_INSPECTOR) return new _recorderApp.EmptyRecorderApp();
  95. return await _recorderApp.RecorderApp.open(recorder, recorder._context, recorder._handleSIGINT);
  96. }
  97. async install() {
  98. const recorderApp = await (Recorder.recorderAppFactory || Recorder.defaultRecorderAppFactory)(this);
  99. this._recorderApp = recorderApp;
  100. recorderApp.once('close', () => {
  101. this._debugger.resume(false);
  102. this._recorderApp = null;
  103. });
  104. recorderApp.on('event', data => {
  105. if (data.event === 'setMode') {
  106. this.setMode(data.params.mode);
  107. return;
  108. }
  109. if (data.event === 'selectorUpdated') {
  110. this.setHighlightedSelector(this._currentLanguage, data.params.selector);
  111. return;
  112. }
  113. if (data.event === 'step') {
  114. this._debugger.resume(true);
  115. return;
  116. }
  117. if (data.event === 'fileChanged') {
  118. this._currentLanguage = this._contextRecorder.languageName(data.params.file);
  119. this._refreshOverlay();
  120. return;
  121. }
  122. if (data.event === 'resume') {
  123. this._debugger.resume(false);
  124. return;
  125. }
  126. if (data.event === 'pause') {
  127. this._debugger.pauseOnNextStatement();
  128. return;
  129. }
  130. if (data.event === 'clear') {
  131. this._contextRecorder.clearScript();
  132. return;
  133. }
  134. });
  135. await Promise.all([recorderApp.setMode(this._mode), recorderApp.setPaused(this._debugger.isPaused()), this._pushAllSources()]);
  136. this._context.once(_browserContext.BrowserContext.Events.Close, () => {
  137. this._contextRecorder.dispose();
  138. this._context.instrumentation.removeListener(this);
  139. recorderApp.close().catch(() => {});
  140. });
  141. this._contextRecorder.on(ContextRecorder.Events.Change, data => {
  142. var _this$_recorderApp;
  143. this._recorderSources = data.sources;
  144. this._pushAllSources();
  145. (_this$_recorderApp = this._recorderApp) === null || _this$_recorderApp === void 0 ? void 0 : _this$_recorderApp.setFileIfNeeded(data.primaryFileName);
  146. });
  147. await this._context.exposeBinding('__pw_recorderState', false, source => {
  148. let actionSelector = '';
  149. let actionPoint;
  150. const hasActiveScreenshotCommand = [...this._currentCallsMetadata.keys()].some(isScreenshotCommand);
  151. if (!hasActiveScreenshotCommand) {
  152. actionSelector = this._highlightedSelector;
  153. for (const [metadata, sdkObject] of this._currentCallsMetadata) {
  154. if (source.page === sdkObject.attribution.page) {
  155. actionPoint = metadata.point || actionPoint;
  156. actionSelector = actionSelector || metadata.params.selector;
  157. }
  158. }
  159. }
  160. const uiState = {
  161. mode: this._mode,
  162. actionPoint,
  163. actionSelector,
  164. language: this._currentLanguage,
  165. testIdAttributeName: this._contextRecorder.testIdAttributeName(),
  166. overlay: this._overlayState
  167. };
  168. return uiState;
  169. });
  170. await this._context.exposeBinding('__pw_recorderSetSelector', false, async ({
  171. frame
  172. }, selector) => {
  173. var _this$_recorderApp2;
  174. const selectorPromises = [];
  175. let currentFrame = frame;
  176. while (currentFrame) {
  177. selectorPromises.push(findFrameSelector(currentFrame));
  178. currentFrame = currentFrame.parentFrame();
  179. }
  180. const fullSelector = (await Promise.all(selectorPromises)).filter(Boolean);
  181. fullSelector.push(selector);
  182. await ((_this$_recorderApp2 = this._recorderApp) === null || _this$_recorderApp2 === void 0 ? void 0 : _this$_recorderApp2.setSelector(fullSelector.join(' >> internal:control=enter-frame >> '), true));
  183. });
  184. await this._context.exposeBinding('__pw_recorderSetMode', false, async ({
  185. frame
  186. }, mode) => {
  187. if (frame.parentFrame()) return;
  188. this.setMode(mode);
  189. });
  190. await this._context.exposeBinding('__pw_recorderSetOverlayState', false, async ({
  191. frame
  192. }, state) => {
  193. if (frame.parentFrame()) return;
  194. this._overlayState = state;
  195. });
  196. await this._context.exposeBinding('__pw_resume', false, () => {
  197. this._debugger.resume(false);
  198. });
  199. await this._context.extendInjectedScript(consoleApiSource.source);
  200. await this._contextRecorder.install();
  201. if (this._debugger.isPaused()) this._pausedStateChanged();
  202. this._debugger.on(_debugger.Debugger.Events.PausedStateChanged, () => this._pausedStateChanged());
  203. this._context.recorderAppForTest = recorderApp;
  204. }
  205. _pausedStateChanged() {
  206. var _this$_recorderApp3;
  207. // If we are called upon page.pause, we don't have metadatas, populate them.
  208. for (const {
  209. metadata,
  210. sdkObject
  211. } of this._debugger.pausedDetails()) {
  212. if (!this._currentCallsMetadata.has(metadata)) this.onBeforeCall(sdkObject, metadata);
  213. }
  214. (_this$_recorderApp3 = this._recorderApp) === null || _this$_recorderApp3 === void 0 ? void 0 : _this$_recorderApp3.setPaused(this._debugger.isPaused());
  215. this._updateUserSources();
  216. this.updateCallLog([...this._currentCallsMetadata.keys()]);
  217. }
  218. setMode(mode) {
  219. var _this$_recorderApp4;
  220. if (this._mode === mode) return;
  221. this._highlightedSelector = '';
  222. this._mode = mode;
  223. (_this$_recorderApp4 = this._recorderApp) === null || _this$_recorderApp4 === void 0 ? void 0 : _this$_recorderApp4.setMode(this._mode);
  224. this._contextRecorder.setEnabled(this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility' || this._mode === 'assertingValue');
  225. this._debugger.setMuted(this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility' || this._mode === 'assertingValue');
  226. if (this._mode !== 'none' && this._mode !== 'standby' && this._context.pages().length === 1) this._context.pages()[0].bringToFront().catch(() => {});
  227. this._refreshOverlay();
  228. }
  229. resume() {
  230. this._debugger.resume(false);
  231. }
  232. mode() {
  233. return this._mode;
  234. }
  235. setHighlightedSelector(language, selector) {
  236. this._highlightedSelector = (0, _locatorParser.locatorOrSelectorAsSelector)(language, selector, this._context.selectors().testIdAttributeName());
  237. this._refreshOverlay();
  238. }
  239. hideHighlightedSelector() {
  240. this._highlightedSelector = '';
  241. this._refreshOverlay();
  242. }
  243. setOutput(codegenId, outputFile) {
  244. this._contextRecorder.setOutput(codegenId, outputFile);
  245. }
  246. _refreshOverlay() {
  247. for (const page of this._context.pages()) page.mainFrame().evaluateExpression('window.__pw_refreshOverlay()').catch(() => {});
  248. }
  249. async onBeforeCall(sdkObject, metadata) {
  250. if (this._omitCallTracking || this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility' || this._mode === 'assertingValue') return;
  251. this._currentCallsMetadata.set(metadata, sdkObject);
  252. this._updateUserSources();
  253. this.updateCallLog([metadata]);
  254. if (isScreenshotCommand(metadata)) {
  255. this.hideHighlightedSelector();
  256. } else if (metadata.params && metadata.params.selector) {
  257. var _this$_recorderApp5;
  258. this._highlightedSelector = metadata.params.selector;
  259. (_this$_recorderApp5 = this._recorderApp) === null || _this$_recorderApp5 === void 0 ? void 0 : _this$_recorderApp5.setSelector(this._highlightedSelector).catch(() => {});
  260. }
  261. }
  262. async onAfterCall(sdkObject, metadata) {
  263. if (this._omitCallTracking || this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility' || this._mode === 'assertingValue') return;
  264. if (!metadata.error) this._currentCallsMetadata.delete(metadata);
  265. this._updateUserSources();
  266. this.updateCallLog([metadata]);
  267. }
  268. _updateUserSources() {
  269. var _this$_recorderApp6;
  270. // Remove old decorations.
  271. for (const source of this._userSources.values()) {
  272. source.highlight = [];
  273. source.revealLine = undefined;
  274. }
  275. // Apply new decorations.
  276. let fileToSelect = undefined;
  277. for (const metadata of this._currentCallsMetadata.keys()) {
  278. if (!metadata.location) continue;
  279. const {
  280. file,
  281. line
  282. } = metadata.location;
  283. let source = this._userSources.get(file);
  284. if (!source) {
  285. source = {
  286. isRecorded: false,
  287. label: file,
  288. id: file,
  289. text: this._readSource(file),
  290. highlight: [],
  291. language: languageForFile(file)
  292. };
  293. this._userSources.set(file, source);
  294. }
  295. if (line) {
  296. const paused = this._debugger.isPaused(metadata);
  297. source.highlight.push({
  298. line,
  299. type: metadata.error ? 'error' : paused ? 'paused' : 'running'
  300. });
  301. source.revealLine = line;
  302. fileToSelect = source.id;
  303. }
  304. }
  305. this._pushAllSources();
  306. if (fileToSelect) (_this$_recorderApp6 = this._recorderApp) === null || _this$_recorderApp6 === void 0 ? void 0 : _this$_recorderApp6.setFileIfNeeded(fileToSelect);
  307. }
  308. _pushAllSources() {
  309. var _this$_recorderApp7;
  310. (_this$_recorderApp7 = this._recorderApp) === null || _this$_recorderApp7 === void 0 ? void 0 : _this$_recorderApp7.setSources([...this._recorderSources, ...this._userSources.values()]);
  311. }
  312. async onBeforeInputAction(sdkObject, metadata) {}
  313. async onCallLog(sdkObject, metadata, logName, message) {
  314. this.updateCallLog([metadata]);
  315. }
  316. updateCallLog(metadatas) {
  317. var _this$_recorderApp8;
  318. if (this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility' || this._mode === 'assertingValue') return;
  319. const logs = [];
  320. for (const metadata of metadatas) {
  321. if (!metadata.method || metadata.internal) continue;
  322. let status = 'done';
  323. if (this._currentCallsMetadata.has(metadata)) status = 'in-progress';
  324. if (this._debugger.isPaused(metadata)) status = 'paused';
  325. logs.push((0, _recorderUtils.metadataToCallLog)(metadata, status));
  326. }
  327. (_this$_recorderApp8 = this._recorderApp) === null || _this$_recorderApp8 === void 0 ? void 0 : _this$_recorderApp8.updateCallLogs(logs);
  328. }
  329. _readSource(fileName) {
  330. try {
  331. return fs.readFileSync(fileName, 'utf-8');
  332. } catch (e) {
  333. return '// No source available';
  334. }
  335. }
  336. }
  337. exports.Recorder = Recorder;
  338. Recorder.recorderAppFactory = void 0;
  339. class ContextRecorder extends _events.EventEmitter {
  340. constructor(context, params) {
  341. super();
  342. this._generator = void 0;
  343. this._pageAliases = new Map();
  344. this._lastPopupOrdinal = 0;
  345. this._lastDialogOrdinal = -1;
  346. this._lastDownloadOrdinal = -1;
  347. this._timers = new Set();
  348. this._context = void 0;
  349. this._params = void 0;
  350. this._recorderSources = void 0;
  351. this._throttledOutputFile = null;
  352. this._orderedLanguages = [];
  353. this._listeners = [];
  354. this._context = context;
  355. this._params = params;
  356. this._recorderSources = [];
  357. const language = params.language || context.attribution.playwright.options.sdkLanguage;
  358. this.setOutput(language, params.outputFile);
  359. const generator = new _codeGenerator.CodeGenerator(context._browser.options.name, params.mode === 'recording', params.launchOptions || {}, params.contextOptions || {}, params.device, params.saveStorage);
  360. generator.on('change', () => {
  361. this._recorderSources = [];
  362. for (const languageGenerator of this._orderedLanguages) {
  363. var _this$_throttledOutpu;
  364. const {
  365. header,
  366. footer,
  367. actions,
  368. text
  369. } = generator.generateStructure(languageGenerator);
  370. const source = {
  371. isRecorded: true,
  372. label: languageGenerator.name,
  373. group: languageGenerator.groupName,
  374. id: languageGenerator.id,
  375. text,
  376. header,
  377. footer,
  378. actions,
  379. language: languageGenerator.highlighter,
  380. highlight: []
  381. };
  382. source.revealLine = text.split('\n').length - 1;
  383. this._recorderSources.push(source);
  384. if (languageGenerator === this._orderedLanguages[0]) (_this$_throttledOutpu = this._throttledOutputFile) === null || _this$_throttledOutpu === void 0 ? void 0 : _this$_throttledOutpu.setContent(source.text);
  385. }
  386. this.emit(ContextRecorder.Events.Change, {
  387. sources: this._recorderSources,
  388. primaryFileName: this._orderedLanguages[0].id
  389. });
  390. });
  391. context.on(_browserContext.BrowserContext.Events.BeforeClose, () => {
  392. var _this$_throttledOutpu2;
  393. (_this$_throttledOutpu2 = this._throttledOutputFile) === null || _this$_throttledOutpu2 === void 0 ? void 0 : _this$_throttledOutpu2.flush();
  394. });
  395. this._listeners.push(_eventsHelper.eventsHelper.addEventListener(process, 'exit', () => {
  396. var _this$_throttledOutpu3;
  397. (_this$_throttledOutpu3 = this._throttledOutputFile) === null || _this$_throttledOutpu3 === void 0 ? void 0 : _this$_throttledOutpu3.flush();
  398. }));
  399. this._generator = generator;
  400. }
  401. setOutput(codegenId, outputFile) {
  402. var _this$_generator;
  403. const languages = new Set([new _java.JavaLanguageGenerator(), new _javascript.JavaScriptLanguageGenerator( /* isPlaywrightTest */false), new _javascript.JavaScriptLanguageGenerator( /* isPlaywrightTest */true), new _python.PythonLanguageGenerator( /* isAsync */false, /* isPytest */true), new _python.PythonLanguageGenerator( /* isAsync */false, /* isPytest */false), new _python.PythonLanguageGenerator( /* isAsync */true, /* isPytest */false), new _csharp.CSharpLanguageGenerator('mstest'), new _csharp.CSharpLanguageGenerator('nunit'), new _csharp.CSharpLanguageGenerator('library'), new _jsonl.JsonlLanguageGenerator()]);
  404. const primaryLanguage = [...languages].find(l => l.id === codegenId);
  405. if (!primaryLanguage) throw new Error(`\n===============================\nUnsupported language: '${codegenId}'\n===============================\n`);
  406. languages.delete(primaryLanguage);
  407. this._orderedLanguages = [primaryLanguage, ...languages];
  408. this._throttledOutputFile = outputFile ? new ThrottledFile(outputFile) : null;
  409. (_this$_generator = this._generator) === null || _this$_generator === void 0 ? void 0 : _this$_generator.restart();
  410. }
  411. languageName(id) {
  412. for (const lang of this._orderedLanguages) {
  413. if (!id || lang.id === id) return lang.highlighter;
  414. }
  415. return 'javascript';
  416. }
  417. async install() {
  418. this._context.on(_browserContext.BrowserContext.Events.Page, page => this._onPage(page));
  419. for (const page of this._context.pages()) this._onPage(page);
  420. this._context.on(_browserContext.BrowserContext.Events.Dialog, dialog => this._onDialog(dialog.page()));
  421. // Input actions that potentially lead to navigation are intercepted on the page and are
  422. // performed by the Playwright.
  423. await this._context.exposeBinding('__pw_recorderPerformAction', false, (source, action) => this._performAction(source.frame, action));
  424. // Other non-essential actions are simply being recorded.
  425. await this._context.exposeBinding('__pw_recorderRecordAction', false, (source, action) => this._recordAction(source.frame, action));
  426. await this._context.extendInjectedScript(recorderSource.source);
  427. }
  428. setEnabled(enabled) {
  429. this._generator.setEnabled(enabled);
  430. }
  431. dispose() {
  432. for (const timer of this._timers) clearTimeout(timer);
  433. this._timers.clear();
  434. _eventsHelper.eventsHelper.removeEventListeners(this._listeners);
  435. }
  436. async _onPage(page) {
  437. // First page is called page, others are called popup1, popup2, etc.
  438. const frame = page.mainFrame();
  439. page.on('close', () => {
  440. this._generator.addAction({
  441. frame: this._describeMainFrame(page),
  442. committed: true,
  443. action: {
  444. name: 'closePage',
  445. signals: []
  446. }
  447. });
  448. this._pageAliases.delete(page);
  449. });
  450. frame.on(_frames.Frame.Events.InternalNavigation, event => {
  451. if (event.isPublic) this._onFrameNavigated(frame, page);
  452. });
  453. page.on(_page.Page.Events.Download, () => this._onDownload(page));
  454. const suffix = this._pageAliases.size ? String(++this._lastPopupOrdinal) : '';
  455. const pageAlias = 'page' + suffix;
  456. this._pageAliases.set(page, pageAlias);
  457. if (page.opener()) {
  458. this._onPopup(page.opener(), page);
  459. } else {
  460. this._generator.addAction({
  461. frame: this._describeMainFrame(page),
  462. committed: true,
  463. action: {
  464. name: 'openPage',
  465. url: page.mainFrame().url(),
  466. signals: []
  467. }
  468. });
  469. }
  470. }
  471. clearScript() {
  472. this._generator.restart();
  473. if (this._params.mode === 'recording') {
  474. for (const page of this._context.pages()) this._onFrameNavigated(page.mainFrame(), page);
  475. }
  476. }
  477. _describeMainFrame(page) {
  478. return {
  479. pageAlias: this._pageAliases.get(page),
  480. isMainFrame: true
  481. };
  482. }
  483. async _describeFrame(frame) {
  484. const page = frame._page;
  485. const pageAlias = this._pageAliases.get(page);
  486. const chain = [];
  487. for (let ancestor = frame; ancestor; ancestor = ancestor.parentFrame()) chain.push(ancestor);
  488. chain.reverse();
  489. if (chain.length === 1) return this._describeMainFrame(page);
  490. const selectorPromises = [];
  491. for (let i = 0; i < chain.length - 1; i++) selectorPromises.push(findFrameSelector(chain[i + 1]));
  492. const result = await (0, _timeoutRunner.raceAgainstDeadline)(() => Promise.all(selectorPromises), (0, _utils2.monotonicTime)() + 2000);
  493. if (!result.timedOut && result.result.every(selector => !!selector)) {
  494. return {
  495. pageAlias,
  496. isMainFrame: false,
  497. selectorsChain: result.result
  498. };
  499. }
  500. // Best effort to find a selector for the frame.
  501. const selectorsChain = [];
  502. for (let i = 0; i < chain.length - 1; i++) {
  503. if (chain[i].name()) selectorsChain.push(`iframe[name=${(0, _stringUtils.quoteCSSAttributeValue)(chain[i].name())}]`);else selectorsChain.push(`iframe[src=${(0, _stringUtils.quoteCSSAttributeValue)(chain[i].url())}]`);
  504. }
  505. return {
  506. pageAlias,
  507. isMainFrame: false,
  508. selectorsChain
  509. };
  510. }
  511. testIdAttributeName() {
  512. return this._params.testIdAttributeName || this._context.selectors().testIdAttributeName() || 'data-testid';
  513. }
  514. async _performAction(frame, action) {
  515. // Commit last action so that no further signals are added to it.
  516. this._generator.commitLastAction();
  517. const frameDescription = await this._describeFrame(frame);
  518. const actionInContext = {
  519. frame: frameDescription,
  520. action
  521. };
  522. const perform = async (action, params, cb) => {
  523. const callMetadata = {
  524. id: `call@${(0, _utils2.createGuid)()}`,
  525. apiName: 'frame.' + action,
  526. objectId: frame.guid,
  527. pageId: frame._page.guid,
  528. frameId: frame.guid,
  529. startTime: (0, _utils2.monotonicTime)(),
  530. endTime: 0,
  531. wallTime: Date.now(),
  532. type: 'Frame',
  533. method: action,
  534. params,
  535. log: []
  536. };
  537. this._generator.willPerformAction(actionInContext);
  538. try {
  539. await frame.instrumentation.onBeforeCall(frame, callMetadata);
  540. await cb(callMetadata);
  541. } catch (e) {
  542. callMetadata.endTime = (0, _utils2.monotonicTime)();
  543. await frame.instrumentation.onAfterCall(frame, callMetadata);
  544. this._generator.performedActionFailed(actionInContext);
  545. return;
  546. }
  547. callMetadata.endTime = (0, _utils2.monotonicTime)();
  548. await frame.instrumentation.onAfterCall(frame, callMetadata);
  549. const timer = setTimeout(() => {
  550. // Commit the action after 5 seconds so that no further signals are added to it.
  551. actionInContext.committed = true;
  552. this._timers.delete(timer);
  553. }, 5000);
  554. this._generator.didPerformAction(actionInContext);
  555. this._timers.add(timer);
  556. };
  557. const kActionTimeout = 5000;
  558. if (action.name === 'click') {
  559. const {
  560. options
  561. } = (0, _utils.toClickOptions)(action);
  562. await perform('click', {
  563. selector: action.selector
  564. }, callMetadata => frame.click(callMetadata, action.selector, {
  565. ...options,
  566. timeout: kActionTimeout,
  567. strict: true
  568. }));
  569. }
  570. if (action.name === 'press') {
  571. const modifiers = (0, _utils.toModifiers)(action.modifiers);
  572. const shortcut = [...modifiers, action.key].join('+');
  573. await perform('press', {
  574. selector: action.selector,
  575. key: shortcut
  576. }, callMetadata => frame.press(callMetadata, action.selector, shortcut, {
  577. timeout: kActionTimeout,
  578. strict: true
  579. }));
  580. }
  581. if (action.name === 'check') await perform('check', {
  582. selector: action.selector
  583. }, callMetadata => frame.check(callMetadata, action.selector, {
  584. timeout: kActionTimeout,
  585. strict: true
  586. }));
  587. if (action.name === 'uncheck') await perform('uncheck', {
  588. selector: action.selector
  589. }, callMetadata => frame.uncheck(callMetadata, action.selector, {
  590. timeout: kActionTimeout,
  591. strict: true
  592. }));
  593. if (action.name === 'select') {
  594. const values = action.options.map(value => ({
  595. value
  596. }));
  597. await perform('selectOption', {
  598. selector: action.selector,
  599. values
  600. }, callMetadata => frame.selectOption(callMetadata, action.selector, [], values, {
  601. timeout: kActionTimeout,
  602. strict: true
  603. }));
  604. }
  605. }
  606. async _recordAction(frame, action) {
  607. // Commit last action so that no further signals are added to it.
  608. this._generator.commitLastAction();
  609. const frameDescription = await this._describeFrame(frame);
  610. const actionInContext = {
  611. frame: frameDescription,
  612. action
  613. };
  614. this._generator.addAction(actionInContext);
  615. }
  616. _onFrameNavigated(frame, page) {
  617. const pageAlias = this._pageAliases.get(page);
  618. this._generator.signal(pageAlias, frame, {
  619. name: 'navigation',
  620. url: frame.url()
  621. });
  622. }
  623. _onPopup(page, popup) {
  624. const pageAlias = this._pageAliases.get(page);
  625. const popupAlias = this._pageAliases.get(popup);
  626. this._generator.signal(pageAlias, page.mainFrame(), {
  627. name: 'popup',
  628. popupAlias
  629. });
  630. }
  631. _onDownload(page) {
  632. const pageAlias = this._pageAliases.get(page);
  633. ++this._lastDownloadOrdinal;
  634. this._generator.signal(pageAlias, page.mainFrame(), {
  635. name: 'download',
  636. downloadAlias: this._lastDownloadOrdinal ? String(this._lastDownloadOrdinal) : ''
  637. });
  638. }
  639. _onDialog(page) {
  640. const pageAlias = this._pageAliases.get(page);
  641. ++this._lastDialogOrdinal;
  642. this._generator.signal(pageAlias, page.mainFrame(), {
  643. name: 'dialog',
  644. dialogAlias: this._lastDialogOrdinal ? String(this._lastDialogOrdinal) : ''
  645. });
  646. }
  647. }
  648. ContextRecorder.Events = {
  649. Change: 'change'
  650. };
  651. function languageForFile(file) {
  652. if (file.endsWith('.py')) return 'python';
  653. if (file.endsWith('.java')) return 'java';
  654. if (file.endsWith('.cs')) return 'csharp';
  655. return 'javascript';
  656. }
  657. class ThrottledFile {
  658. constructor(file) {
  659. this._file = void 0;
  660. this._timer = void 0;
  661. this._text = void 0;
  662. this._file = file;
  663. }
  664. setContent(text) {
  665. this._text = text;
  666. if (!this._timer) this._timer = setTimeout(() => this.flush(), 250);
  667. }
  668. flush() {
  669. if (this._timer) {
  670. clearTimeout(this._timer);
  671. this._timer = undefined;
  672. }
  673. if (this._text) fs.writeFileSync(this._file, this._text);
  674. this._text = undefined;
  675. }
  676. }
  677. function isScreenshotCommand(metadata) {
  678. return metadata.method.toLowerCase().includes('screenshot');
  679. }
  680. async function findFrameSelector(frame) {
  681. try {
  682. const parent = frame.parentFrame();
  683. const frameElement = await frame.frameElement();
  684. if (!frameElement || !parent) return;
  685. const utility = await parent._utilityContext();
  686. const injected = await utility.injectedScript();
  687. const selector = await injected.evaluate((injected, element) => {
  688. return injected.generateSelector(element, {
  689. testIdAttributeName: '',
  690. omitInternalEngines: true
  691. });
  692. }, frameElement);
  693. return selector;
  694. } catch (e) {}
  695. }