electron.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.ElectronApplication = exports.Electron = void 0;
  6. var _fs = _interopRequireDefault(require("fs"));
  7. var _os = _interopRequireDefault(require("os"));
  8. var _path = _interopRequireDefault(require("path"));
  9. var _crBrowser = require("../chromium/crBrowser");
  10. var _crConnection = require("../chromium/crConnection");
  11. var _crExecutionContext = require("../chromium/crExecutionContext");
  12. var js = _interopRequireWildcard(require("../javascript"));
  13. var _timeoutSettings = require("../../common/timeoutSettings");
  14. var _utils = require("../../utils");
  15. var _transport = require("../transport");
  16. var _processLauncher = require("../../utils/processLauncher");
  17. var _browserContext = require("../browserContext");
  18. var _progress = require("../progress");
  19. var _helper = require("../helper");
  20. var _eventsHelper = require("../../utils/eventsHelper");
  21. var readline = _interopRequireWildcard(require("readline"));
  22. var _debugLogger = require("../../common/debugLogger");
  23. var _instrumentation = require("../instrumentation");
  24. 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); }
  25. 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; }
  26. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  27. /**
  28. * Copyright (c) Microsoft Corporation.
  29. *
  30. * Licensed under the Apache License, Version 2.0 (the "License");
  31. * you may not use this file except in compliance with the License.
  32. * You may obtain a copy of the License at
  33. *
  34. * http://www.apache.org/licenses/LICENSE-2.0
  35. *
  36. * Unless required by applicable law or agreed to in writing, software
  37. * distributed under the License is distributed on an "AS IS" BASIS,
  38. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  39. * See the License for the specific language governing permissions and
  40. * limitations under the License.
  41. */
  42. const ARTIFACTS_FOLDER = _path.default.join(_os.default.tmpdir(), 'playwright-artifacts-');
  43. class ElectronApplication extends _instrumentation.SdkObject {
  44. constructor(parent, browser, nodeConnection, process) {
  45. super(parent, 'electron-app');
  46. this._browserContext = void 0;
  47. this._nodeConnection = void 0;
  48. this._nodeSession = void 0;
  49. this._nodeExecutionContext = void 0;
  50. this._nodeElectronHandlePromise = new _utils.ManualPromise();
  51. this._timeoutSettings = new _timeoutSettings.TimeoutSettings();
  52. this._process = void 0;
  53. this._process = process;
  54. this._browserContext = browser._defaultContext;
  55. this._browserContext.on(_browserContext.BrowserContext.Events.Close, () => {
  56. // Emit application closed after context closed.
  57. Promise.resolve().then(() => this.emit(ElectronApplication.Events.Close));
  58. });
  59. this._nodeConnection = nodeConnection;
  60. this._nodeSession = nodeConnection.rootSession;
  61. this._nodeSession.on('Runtime.executionContextCreated', async event => {
  62. if (!event.context.auxData || !event.context.auxData.isDefault) return;
  63. const crExecutionContext = new _crExecutionContext.CRExecutionContext(this._nodeSession, event.context);
  64. this._nodeExecutionContext = new js.ExecutionContext(this, crExecutionContext, 'electron');
  65. const {
  66. result: remoteObject
  67. } = await crExecutionContext._client.send('Runtime.evaluate', {
  68. expression: `require('electron')`,
  69. contextId: event.context.id,
  70. // Needed after Electron 28 to get access to require: https://github.com/microsoft/playwright/issues/28048
  71. includeCommandLineAPI: true
  72. });
  73. this._nodeElectronHandlePromise.resolve(new js.JSHandle(this._nodeExecutionContext, 'object', 'ElectronModule', remoteObject.objectId));
  74. });
  75. this._browserContext.setCustomCloseHandler(async () => {
  76. await this._browserContext.stopVideoRecording();
  77. const electronHandle = await this._nodeElectronHandlePromise;
  78. await electronHandle.evaluate(({
  79. app
  80. }) => app.quit()).catch(() => {});
  81. });
  82. }
  83. async initialize() {
  84. await this._nodeSession.send('Runtime.enable', {});
  85. // Delay loading the app until browser is started and the browser targets are configured to auto-attach.
  86. await this._nodeSession.send('Runtime.evaluate', {
  87. expression: '__playwright_run()'
  88. });
  89. }
  90. process() {
  91. return this._process;
  92. }
  93. context() {
  94. return this._browserContext;
  95. }
  96. async close() {
  97. const progressController = new _progress.ProgressController((0, _instrumentation.serverSideCallMetadata)(), this);
  98. const closed = progressController.run(progress => _helper.helper.waitForEvent(progress, this, ElectronApplication.Events.Close).promise);
  99. await this._browserContext.close({
  100. reason: 'Application exited'
  101. });
  102. this._nodeConnection.close();
  103. await closed;
  104. }
  105. async browserWindow(page) {
  106. // Assume CRPage as Electron is always Chromium.
  107. const targetId = page._delegate._targetId;
  108. const electronHandle = await this._nodeElectronHandlePromise;
  109. return await electronHandle.evaluateHandle(({
  110. BrowserWindow,
  111. webContents
  112. }, targetId) => {
  113. const wc = webContents.fromDevToolsTargetId(targetId);
  114. return BrowserWindow.fromWebContents(wc);
  115. }, targetId);
  116. }
  117. }
  118. exports.ElectronApplication = ElectronApplication;
  119. ElectronApplication.Events = {
  120. Close: 'close'
  121. };
  122. class Electron extends _instrumentation.SdkObject {
  123. constructor(playwright) {
  124. super(playwright, 'electron');
  125. }
  126. async launch(options) {
  127. const {
  128. args = []
  129. } = options;
  130. const controller = new _progress.ProgressController((0, _instrumentation.serverSideCallMetadata)(), this);
  131. controller.setLogName('browser');
  132. return controller.run(async progress => {
  133. let app = undefined;
  134. const electronArguments = ['--inspect=0', '--remote-debugging-port=0', ...args];
  135. if (_os.default.platform() === 'linux') {
  136. const runningAsRoot = process.geteuid && process.geteuid() === 0;
  137. if (runningAsRoot && electronArguments.indexOf('--no-sandbox') === -1) electronArguments.push('--no-sandbox');
  138. }
  139. const artifactsDir = await _fs.default.promises.mkdtemp(ARTIFACTS_FOLDER);
  140. const browserLogsCollector = new _debugLogger.RecentLogsCollector();
  141. const env = options.env ? (0, _processLauncher.envArrayToObject)(options.env) : process.env;
  142. let command;
  143. if (options.executablePath) {
  144. command = options.executablePath;
  145. } else {
  146. try {
  147. // By default we fallback to the Electron App executable path.
  148. // 'electron/index.js' resolves to the actual Electron App.
  149. command = require('electron/index.js');
  150. } catch (error) {
  151. if ((error === null || error === void 0 ? void 0 : error.code) === 'MODULE_NOT_FOUND') {
  152. throw new Error('\n' + (0, _utils.wrapInASCIIBox)(['Electron executablePath not found!', 'Please install it using `npm install -D electron` or set the executablePath to your Electron executable.'].join('\n'), 1));
  153. }
  154. throw error;
  155. }
  156. // Only use our own loader for non-packaged apps.
  157. // Packaged apps might have their own command line handling.
  158. electronArguments.unshift('-r', require.resolve('./loader'));
  159. }
  160. // When debugging Playwright test that runs Electron, NODE_OPTIONS
  161. // will make the debugger attach to Electron's Node. But Playwright
  162. // also needs to attach to drive the automation. Disable external debugging.
  163. delete env.NODE_OPTIONS;
  164. const {
  165. launchedProcess,
  166. gracefullyClose,
  167. kill
  168. } = await (0, _processLauncher.launchProcess)({
  169. command,
  170. args: electronArguments,
  171. env,
  172. log: message => {
  173. progress.log(message);
  174. browserLogsCollector.log(message);
  175. },
  176. stdio: 'pipe',
  177. cwd: options.cwd,
  178. tempDirectories: [artifactsDir],
  179. attemptToGracefullyClose: () => app.close(),
  180. handleSIGINT: true,
  181. handleSIGTERM: true,
  182. handleSIGHUP: true,
  183. onExit: () => {}
  184. });
  185. const waitForXserverError = new Promise(async (resolve, reject) => {
  186. waitForLine(progress, launchedProcess, /Unable to open X display/).then(() => reject(new Error(['Unable to open X display!', `================================`, 'Most likely this is because there is no X server available.', "Use 'xvfb-run' on Linux to launch your tests with an emulated display server.", "For example: 'xvfb-run npm run test:e2e'", `================================`, progress.metadata.log].join('\n')))).catch(() => {});
  187. });
  188. const nodeMatch = await waitForLine(progress, launchedProcess, /^Debugger listening on (ws:\/\/.*)$/);
  189. const nodeTransport = await _transport.WebSocketTransport.connect(progress, nodeMatch[1]);
  190. const nodeConnection = new _crConnection.CRConnection(nodeTransport, _helper.helper.debugProtocolLogger(), browserLogsCollector);
  191. // Immediately release exiting process under debug.
  192. waitForLine(progress, launchedProcess, /Waiting for the debugger to disconnect\.\.\./).then(() => {
  193. nodeTransport.close();
  194. }).catch(() => {});
  195. const chromeMatch = await Promise.race([waitForLine(progress, launchedProcess, /^DevTools listening on (ws:\/\/.*)$/), waitForXserverError]);
  196. const chromeTransport = await _transport.WebSocketTransport.connect(progress, chromeMatch[1]);
  197. const browserProcess = {
  198. onclose: undefined,
  199. process: launchedProcess,
  200. close: gracefullyClose,
  201. kill
  202. };
  203. const contextOptions = {
  204. ...options,
  205. noDefaultViewport: true
  206. };
  207. const browserOptions = {
  208. name: 'electron',
  209. isChromium: true,
  210. headful: true,
  211. persistent: contextOptions,
  212. browserProcess,
  213. protocolLogger: _helper.helper.debugProtocolLogger(),
  214. browserLogsCollector,
  215. artifactsDir,
  216. downloadsPath: artifactsDir,
  217. tracesDir: options.tracesDir || artifactsDir,
  218. originalLaunchOptions: {}
  219. };
  220. (0, _browserContext.validateBrowserContextOptions)(contextOptions, browserOptions);
  221. const browser = await _crBrowser.CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions);
  222. app = new ElectronApplication(this, browser, nodeConnection, launchedProcess);
  223. await app.initialize();
  224. return app;
  225. }, _timeoutSettings.TimeoutSettings.launchTimeout(options));
  226. }
  227. }
  228. exports.Electron = Electron;
  229. function waitForLine(progress, process, regex) {
  230. return new Promise((resolve, reject) => {
  231. const rl = readline.createInterface({
  232. input: process.stderr
  233. });
  234. const failError = new Error('Process failed to launch!');
  235. const listeners = [_eventsHelper.eventsHelper.addEventListener(rl, 'line', onLine), _eventsHelper.eventsHelper.addEventListener(rl, 'close', reject.bind(null, failError)), _eventsHelper.eventsHelper.addEventListener(process, 'exit', reject.bind(null, failError)),
  236. // It is Ok to remove error handler because we did not create process and there is another listener.
  237. _eventsHelper.eventsHelper.addEventListener(process, 'error', reject.bind(null, failError))];
  238. progress.cleanupWhenAborted(cleanup);
  239. function onLine(line) {
  240. const match = line.match(regex);
  241. if (!match) return;
  242. cleanup();
  243. resolve(match);
  244. }
  245. function cleanup() {
  246. _eventsHelper.eventsHelper.removeEventListeners(listeners);
  247. }
  248. });
  249. }