api-web-contents-spec.js 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317
  1. 'use strict';
  2. const ChildProcess = require('child_process');
  3. const fs = require('fs');
  4. const http = require('http');
  5. const path = require('path');
  6. const { closeWindow } = require('./window-helpers');
  7. const { emittedOnce } = require('./events-helpers');
  8. const chai = require('chai');
  9. const dirtyChai = require('dirty-chai');
  10. const features = process.electronBinding('features');
  11. const { ipcRenderer, remote, clipboard } = require('electron');
  12. const { BrowserWindow, webContents, ipcMain, session } = remote;
  13. const { expect } = chai;
  14. const isCi = remote.getGlobal('isCi');
  15. chai.use(dirtyChai);
  16. /* The whole webContents API doesn't use standard callbacks */
  17. /* eslint-disable standard/no-callback-literal */
  18. describe('webContents module', () => {
  19. const fixtures = path.resolve(__dirname, 'fixtures');
  20. let w;
  21. beforeEach(() => {
  22. w = new BrowserWindow({
  23. show: false,
  24. width: 400,
  25. height: 400,
  26. webPreferences: {
  27. backgroundThrottling: false,
  28. nodeIntegration: true,
  29. webviewTag: true
  30. }
  31. });
  32. });
  33. afterEach(() => closeWindow(w).then(() => { w = null; }));
  34. describe('loadURL() promise API', () => {
  35. it('resolves when done loading', async () => {
  36. await expect(w.loadURL('about:blank')).to.eventually.be.fulfilled;
  37. });
  38. it('resolves when done loading a file URL', async () => {
  39. await expect(w.loadFile(path.join(fixtures, 'pages', 'base-page.html'))).to.eventually.be.fulfilled;
  40. });
  41. it('rejects when failing to load a file URL', async () => {
  42. await expect(w.loadURL('file:non-existent')).to.eventually.be.rejected
  43. .and.have.property('code', 'ERR_FILE_NOT_FOUND');
  44. });
  45. // Temporarily disable on WOA until
  46. // https://github.com/electron/electron/issues/20008 is resolved
  47. const testFn = (process.platform === 'win32' && process.arch === 'arm64' ? it.skip : it);
  48. testFn('rejects when loading fails due to DNS not resolved', async () => {
  49. await expect(w.loadURL('https://err.name.not.resolved')).to.eventually.be.rejected
  50. .and.have.property('code', 'ERR_NAME_NOT_RESOLVED');
  51. });
  52. it('rejects when navigation is cancelled due to a bad scheme', async () => {
  53. await expect(w.loadURL('bad-scheme://foo')).to.eventually.be.rejected
  54. .and.have.property('code', 'ERR_FAILED');
  55. });
  56. it('sets appropriate error information on rejection', async () => {
  57. let err;
  58. try {
  59. await w.loadURL('file:non-existent');
  60. } catch (e) {
  61. err = e;
  62. }
  63. expect(err).not.to.be.null();
  64. expect(err.code).to.eql('ERR_FILE_NOT_FOUND');
  65. expect(err.errno).to.eql(-6);
  66. expect(err.url).to.eql(process.platform === 'win32' ? 'file://non-existent/' : 'file:///non-existent');
  67. });
  68. it('rejects if the load is aborted', async () => {
  69. const s = http.createServer((req, res) => { /* never complete the request */ });
  70. await new Promise(resolve => s.listen(0, '127.0.0.1', resolve));
  71. const { port } = s.address();
  72. const p = expect(w.loadURL(`http://127.0.0.1:${port}`)).to.eventually.be.rejectedWith(Error, /ERR_ABORTED/);
  73. // load a different file before the first load completes, causing the
  74. // first load to be aborted.
  75. await w.loadFile(path.join(fixtures, 'pages', 'base-page.html'));
  76. await p;
  77. s.close();
  78. });
  79. it("doesn't reject when a subframe fails to load", async () => {
  80. let resp = null;
  81. const s = http.createServer((req, res) => {
  82. res.writeHead(200, { 'Content-Type': 'text/html' });
  83. res.write('<iframe src="http://err.name.not.resolved"></iframe>');
  84. resp = res;
  85. // don't end the response yet
  86. });
  87. await new Promise(resolve => s.listen(0, '127.0.0.1', resolve));
  88. const { port } = s.address();
  89. const p = new Promise(resolve => {
  90. w.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => {
  91. if (!isMainFrame) {
  92. resolve();
  93. }
  94. });
  95. });
  96. const main = w.loadURL(`http://127.0.0.1:${port}`);
  97. await p;
  98. resp.end();
  99. await main;
  100. s.close();
  101. });
  102. it("doesn't resolve when a subframe loads", async () => {
  103. let resp = null;
  104. const s = http.createServer((req, res) => {
  105. res.writeHead(200, { 'Content-Type': 'text/html' });
  106. res.write('<iframe src="data:text/html,hi"></iframe>');
  107. resp = res;
  108. // don't end the response yet
  109. });
  110. await new Promise(resolve => s.listen(0, '127.0.0.1', resolve));
  111. const { port } = s.address();
  112. const p = new Promise(resolve => {
  113. w.webContents.on('did-frame-finish-load', (event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => {
  114. if (!isMainFrame) {
  115. resolve();
  116. }
  117. });
  118. });
  119. const main = w.loadURL(`http://127.0.0.1:${port}`);
  120. await p;
  121. resp.destroy(); // cause the main request to fail
  122. await expect(main).to.eventually.be.rejected
  123. .and.have.property('errno', -355); // ERR_INCOMPLETE_CHUNKED_ENCODING
  124. s.close();
  125. });
  126. });
  127. describe('getFocusedWebContents() API', () => {
  128. it('returns the focused web contents', (done) => {
  129. if (isCi) return done();
  130. const specWebContents = remote.getCurrentWebContents();
  131. expect(specWebContents.id).to.equal(webContents.getFocusedWebContents().id);
  132. specWebContents.once('devtools-opened', () => {
  133. expect(specWebContents.devToolsWebContents.id).to.equal(webContents.getFocusedWebContents().id);
  134. specWebContents.closeDevTools();
  135. });
  136. specWebContents.once('devtools-closed', () => {
  137. expect(specWebContents.id).to.equal(webContents.getFocusedWebContents().id);
  138. done();
  139. });
  140. specWebContents.openDevTools();
  141. });
  142. it('does not crash when called on a detached dev tools window', (done) => {
  143. const specWebContents = w.webContents;
  144. specWebContents.once('devtools-opened', () => {
  145. expect(() => {
  146. webContents.getFocusedWebContents();
  147. }).to.not.throw();
  148. specWebContents.closeDevTools();
  149. });
  150. specWebContents.once('devtools-closed', () => {
  151. expect(() => {
  152. webContents.getFocusedWebContents();
  153. }).to.not.throw();
  154. done();
  155. });
  156. specWebContents.openDevTools({ mode: 'detach' });
  157. w.inspectElement(100, 100);
  158. });
  159. });
  160. describe('setDevToolsWebContents() API', () => {
  161. it('sets arbitrary webContents as devtools', async () => {
  162. const devtools = new BrowserWindow({ show: false });
  163. const promise = emittedOnce(devtools.webContents, 'dom-ready');
  164. w.webContents.setDevToolsWebContents(devtools.webContents);
  165. w.webContents.openDevTools();
  166. await promise;
  167. expect(devtools.getURL().startsWith('devtools://devtools')).to.be.true();
  168. const result = await devtools.webContents.executeJavaScript('InspectorFrontendHost.constructor.name');
  169. expect(result).to.equal('InspectorFrontendHostImpl');
  170. devtools.destroy();
  171. });
  172. });
  173. describe('isFocused() API', () => {
  174. it('returns false when the window is hidden', () => {
  175. BrowserWindow.getAllWindows().forEach((window) => {
  176. expect(!window.isVisible() && window.webContents.isFocused()).to.be.false();
  177. });
  178. });
  179. });
  180. describe('isCurrentlyAudible() API', () => {
  181. it('returns whether audio is playing', async () => {
  182. const webContents = remote.getCurrentWebContents();
  183. const context = new window.AudioContext();
  184. // Start in suspended state, because of the
  185. // new web audio api policy.
  186. context.suspend();
  187. const oscillator = context.createOscillator();
  188. oscillator.connect(context.destination);
  189. oscillator.start();
  190. let p = emittedOnce(webContents, '-audio-state-changed');
  191. await context.resume();
  192. await p;
  193. expect(webContents.isCurrentlyAudible()).to.be.true();
  194. p = emittedOnce(webContents, '-audio-state-changed');
  195. oscillator.stop();
  196. await p;
  197. expect(webContents.isCurrentlyAudible()).to.be.false();
  198. oscillator.disconnect();
  199. context.close();
  200. });
  201. });
  202. describe('getWebPreferences() API', () => {
  203. it('should not crash when called for devTools webContents', (done) => {
  204. w.webContents.openDevTools();
  205. w.webContents.once('devtools-opened', () => {
  206. expect(w.devToolsWebContents.getWebPreferences()).to.be.null();
  207. done();
  208. });
  209. });
  210. });
  211. describe('openDevTools() API', () => {
  212. it('can show window with activation', async () => {
  213. const focused = emittedOnce(w, 'focus');
  214. w.show();
  215. await focused;
  216. expect(w.isFocused()).to.be.true();
  217. const devtoolsOpened = emittedOnce(w.webContents, 'devtools-opened');
  218. w.webContents.openDevTools({ mode: 'detach', activate: true });
  219. await devtoolsOpened;
  220. expect(w.isFocused()).to.be.false();
  221. });
  222. it('can show window without activation', async () => {
  223. const devtoolsOpened = emittedOnce(w.webContents, 'devtools-opened');
  224. w.webContents.openDevTools({ mode: 'detach', activate: false });
  225. await devtoolsOpened;
  226. expect(w.isDevToolsOpened()).to.be.true();
  227. });
  228. });
  229. describe('before-input-event event', () => {
  230. it('can prevent document keyboard events', async () => {
  231. await w.loadFile(path.join(fixtures, 'pages', 'key-events.html'));
  232. const keyDown = new Promise(resolve => {
  233. ipcMain.once('keydown', (event, key) => resolve(key));
  234. });
  235. ipcRenderer.sendSync('prevent-next-input-event', 'a', w.webContents.id);
  236. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'a' });
  237. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'b' });
  238. expect(await keyDown).to.equal('b');
  239. });
  240. it('has the correct properties', async () => {
  241. await w.loadFile(path.join(fixtures, 'pages', 'base-page.html'));
  242. const testBeforeInput = async (opts) => {
  243. const modifiers = [];
  244. if (opts.shift) modifiers.push('shift');
  245. if (opts.control) modifiers.push('control');
  246. if (opts.alt) modifiers.push('alt');
  247. if (opts.meta) modifiers.push('meta');
  248. if (opts.isAutoRepeat) modifiers.push('isAutoRepeat');
  249. const p = emittedOnce(w.webContents, 'before-input-event');
  250. w.webContents.sendInputEvent({
  251. type: opts.type,
  252. keyCode: opts.keyCode,
  253. modifiers: modifiers
  254. });
  255. const [, input] = await p;
  256. expect(input.type).to.equal(opts.type);
  257. expect(input.key).to.equal(opts.key);
  258. expect(input.code).to.equal(opts.code);
  259. expect(input.isAutoRepeat).to.equal(opts.isAutoRepeat);
  260. expect(input.shift).to.equal(opts.shift);
  261. expect(input.control).to.equal(opts.control);
  262. expect(input.alt).to.equal(opts.alt);
  263. expect(input.meta).to.equal(opts.meta);
  264. };
  265. await testBeforeInput({
  266. type: 'keyDown',
  267. key: 'A',
  268. code: 'KeyA',
  269. keyCode: 'a',
  270. shift: true,
  271. control: true,
  272. alt: true,
  273. meta: true,
  274. isAutoRepeat: true
  275. });
  276. await testBeforeInput({
  277. type: 'keyUp',
  278. key: '.',
  279. code: 'Period',
  280. keyCode: '.',
  281. shift: false,
  282. control: true,
  283. alt: true,
  284. meta: false,
  285. isAutoRepeat: false
  286. });
  287. await testBeforeInput({
  288. type: 'keyUp',
  289. key: '!',
  290. code: 'Digit1',
  291. keyCode: '1',
  292. shift: true,
  293. control: false,
  294. alt: false,
  295. meta: true,
  296. isAutoRepeat: false
  297. });
  298. await testBeforeInput({
  299. type: 'keyUp',
  300. key: 'Tab',
  301. code: 'Tab',
  302. keyCode: 'Tab',
  303. shift: false,
  304. control: true,
  305. alt: false,
  306. meta: false,
  307. isAutoRepeat: true
  308. });
  309. });
  310. });
  311. describe('zoom-changed', () => {
  312. beforeEach(function () {
  313. // On Mac, zooming isn't done with the mouse wheel.
  314. if (process.platform === 'darwin') {
  315. return closeWindow(w).then(() => {
  316. w = null;
  317. this.skip();
  318. });
  319. }
  320. });
  321. it('is emitted with the correct zooming info', async () => {
  322. w.loadFile(path.join(fixtures, 'pages', 'base-page.html'));
  323. await emittedOnce(w.webContents, 'did-finish-load');
  324. const testZoomChanged = async ({ zoomingIn }) => {
  325. const promise = emittedOnce(w.webContents, 'zoom-changed');
  326. w.webContents.sendInputEvent({
  327. type: 'mousewheel',
  328. x: 300,
  329. y: 300,
  330. deltaX: 0,
  331. deltaY: zoomingIn ? 1 : -1,
  332. wheelTicksX: 0,
  333. wheelTicksY: zoomingIn ? 1 : -1,
  334. phase: 'began',
  335. modifiers: ['control', 'meta']
  336. });
  337. const [, zoomDirection] = await promise;
  338. expect(zoomDirection).to.equal(zoomingIn ? 'in' : 'out');
  339. };
  340. await testZoomChanged({ zoomingIn: true });
  341. await testZoomChanged({ zoomingIn: false });
  342. });
  343. });
  344. describe('devtools window', () => {
  345. let testFn = it;
  346. if (process.platform === 'darwin' && isCi) {
  347. testFn = it.skip;
  348. }
  349. if (process.platform === 'win32' && isCi) {
  350. testFn = it.skip;
  351. }
  352. try {
  353. // We have other tests that check if native modules work, if we fail to require
  354. // robotjs let's skip this test to avoid false negatives
  355. require('robotjs');
  356. } catch (err) {
  357. testFn = it.skip;
  358. }
  359. testFn('can receive and handle menu events', async function () {
  360. this.timeout(5000);
  361. w.show();
  362. w.loadFile(path.join(fixtures, 'pages', 'key-events.html'));
  363. // Ensure the devtools are loaded
  364. w.webContents.closeDevTools();
  365. const opened = emittedOnce(w.webContents, 'devtools-opened');
  366. w.webContents.openDevTools();
  367. await opened;
  368. await emittedOnce(w.webContents.devToolsWebContents, 'did-finish-load');
  369. w.webContents.devToolsWebContents.focus();
  370. // Focus an input field
  371. await w.webContents.devToolsWebContents.executeJavaScript(
  372. `const input = document.createElement('input');
  373. document.body.innerHTML = '';
  374. document.body.appendChild(input)
  375. input.focus();`
  376. );
  377. // Write something to the clipboard
  378. clipboard.writeText('test value');
  379. // Fake a paste request using robotjs to emulate a REAL keyboard paste event
  380. require('robotjs').keyTap('v', process.platform === 'darwin' ? ['command'] : ['control']);
  381. const start = Date.now();
  382. let val;
  383. // Check every now and again for the pasted value (paste is async)
  384. while (val !== 'test value' && Date.now() - start <= 1000) {
  385. val = await w.webContents.devToolsWebContents.executeJavaScript(
  386. `document.querySelector('input').value`
  387. );
  388. await new Promise(resolve => setTimeout(resolve, 10));
  389. }
  390. // Once we're done expect the paste to have been successful
  391. expect(val).to.equal('test value', 'value should eventually become the pasted value');
  392. });
  393. });
  394. describe('sendInputEvent(event)', () => {
  395. beforeEach(async () => {
  396. await w.loadFile(path.join(fixtures, 'pages', 'key-events.html'));
  397. });
  398. it('can send keydown events', (done) => {
  399. ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
  400. expect(key).to.equal('a');
  401. expect(code).to.equal('KeyA');
  402. expect(keyCode).to.equal(65);
  403. expect(shiftKey).to.be.false();
  404. expect(ctrlKey).to.be.false();
  405. expect(altKey).to.be.false();
  406. done();
  407. });
  408. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' });
  409. });
  410. it('can send keydown events with modifiers', (done) => {
  411. ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
  412. expect(key).to.equal('Z');
  413. expect(code).to.equal('KeyZ');
  414. expect(keyCode).to.equal(90);
  415. expect(shiftKey).to.be.true();
  416. expect(ctrlKey).to.be.true();
  417. expect(altKey).to.be.false();
  418. done();
  419. });
  420. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z', modifiers: ['shift', 'ctrl'] });
  421. });
  422. it('can send keydown events with special keys', (done) => {
  423. ipcMain.once('keydown', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
  424. expect(key).to.equal('Tab');
  425. expect(code).to.equal('Tab');
  426. expect(keyCode).to.equal(9);
  427. expect(shiftKey).to.be.false();
  428. expect(ctrlKey).to.be.false();
  429. expect(altKey).to.be.true();
  430. done();
  431. });
  432. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Tab', modifiers: ['alt'] });
  433. });
  434. it('can send char events', (done) => {
  435. ipcMain.once('keypress', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
  436. expect(key).to.equal('a');
  437. expect(code).to.equal('KeyA');
  438. expect(keyCode).to.equal(65);
  439. expect(shiftKey).to.be.false();
  440. expect(ctrlKey).to.be.false();
  441. expect(altKey).to.be.false();
  442. done();
  443. });
  444. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A' });
  445. w.webContents.sendInputEvent({ type: 'char', keyCode: 'A' });
  446. });
  447. it('can send char events with modifiers', (done) => {
  448. ipcMain.once('keypress', (event, key, code, keyCode, shiftKey, ctrlKey, altKey) => {
  449. expect(key).to.equal('Z');
  450. expect(code).to.equal('KeyZ');
  451. expect(keyCode).to.equal(90);
  452. expect(shiftKey).to.be.true();
  453. expect(ctrlKey).to.be.true();
  454. expect(altKey).to.be.false();
  455. done();
  456. });
  457. w.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Z' });
  458. w.webContents.sendInputEvent({ type: 'char', keyCode: 'Z', modifiers: ['shift', 'ctrl'] });
  459. });
  460. });
  461. it('supports inserting CSS', async () => {
  462. w.loadURL('about:blank');
  463. await w.webContents.insertCSS('body { background-repeat: round; }');
  464. const result = await w.webContents.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")');
  465. expect(result).to.equal('round');
  466. });
  467. it('supports removing inserted CSS', async () => {
  468. w.loadURL('about:blank');
  469. const key = await w.webContents.insertCSS('body { background-repeat: round; }');
  470. await w.webContents.removeInsertedCSS(key);
  471. const result = await w.webContents.executeJavaScript('window.getComputedStyle(document.body).getPropertyValue("background-repeat")');
  472. expect(result).to.equal('repeat');
  473. });
  474. it('supports inspecting an element in the devtools', (done) => {
  475. w.loadURL('about:blank');
  476. w.webContents.once('devtools-opened', () => { done(); });
  477. w.webContents.inspectElement(10, 10);
  478. });
  479. describe('startDrag({file, icon})', () => {
  480. it('throws errors for a missing file or a missing/empty icon', () => {
  481. expect(() => {
  482. w.webContents.startDrag({ icon: path.join(fixtures, 'assets', 'logo.png') });
  483. }).to.throw(`Must specify either 'file' or 'files' option`);
  484. expect(() => {
  485. w.webContents.startDrag({ file: __filename });
  486. }).to.throw(`Must specify 'icon' option`);
  487. if (process.platform === 'darwin') {
  488. expect(() => {
  489. w.webContents.startDrag({ file: __filename, icon: __filename });
  490. }).to.throw(`Must specify non-empty 'icon' option`);
  491. }
  492. });
  493. });
  494. describe('focus()', () => {
  495. describe('when the web contents is hidden', () => {
  496. it('does not blur the focused window', (done) => {
  497. ipcMain.once('answer', (event, parentFocused, childFocused) => {
  498. expect(parentFocused).to.be.true();
  499. expect(childFocused).to.be.false();
  500. done();
  501. });
  502. w.show();
  503. w.loadFile(path.join(fixtures, 'pages', 'focus-web-contents.html'));
  504. });
  505. });
  506. });
  507. describe('getOSProcessId()', () => {
  508. it('returns a valid procress id', async () => {
  509. expect(w.webContents.getOSProcessId()).to.equal(0);
  510. await w.loadURL('about:blank');
  511. expect(w.webContents.getOSProcessId()).to.be.above(0);
  512. });
  513. });
  514. describe('zoom api', () => {
  515. const zoomScheme = remote.getGlobal('zoomScheme');
  516. const hostZoomMap = {
  517. host1: 0.3,
  518. host2: 0.7,
  519. host3: 0.2
  520. };
  521. before((done) => {
  522. const protocol = session.defaultSession.protocol;
  523. protocol.registerStringProtocol(zoomScheme, (request, callback) => {
  524. const response = `<script>
  525. const {ipcRenderer, remote} = require('electron')
  526. ipcRenderer.send('set-zoom', window.location.hostname)
  527. ipcRenderer.on(window.location.hostname + '-zoom-set', () => {
  528. const { zoomLevel } = remote.getCurrentWebContents()
  529. ipcRenderer.send(window.location.hostname + '-zoom-level', zoomLevel)
  530. })
  531. </script>`;
  532. callback({ data: response, mimeType: 'text/html' });
  533. }, (error) => done(error));
  534. });
  535. after((done) => {
  536. const protocol = session.defaultSession.protocol;
  537. protocol.unregisterProtocol(zoomScheme, (error) => done(error));
  538. });
  539. // TODO(codebytere): remove in Electron v8.0.0
  540. it('can set the correct zoom level (functions)', async () => {
  541. try {
  542. await w.loadURL('about:blank');
  543. const zoomLevel = w.webContents.getZoomLevel();
  544. expect(zoomLevel).to.eql(0.0);
  545. w.webContents.setZoomLevel(0.5);
  546. const newZoomLevel = w.webContents.getZoomLevel();
  547. expect(newZoomLevel).to.eql(0.5);
  548. } finally {
  549. w.webContents.setZoomLevel(0);
  550. }
  551. });
  552. it('can set the correct zoom level', async () => {
  553. try {
  554. await w.loadURL('about:blank');
  555. const zoomLevel = w.webContents.zoomLevel;
  556. expect(zoomLevel).to.eql(0.0);
  557. w.webContents.zoomLevel = 0.5;
  558. const newZoomLevel = w.webContents.zoomLevel;
  559. expect(newZoomLevel).to.eql(0.5);
  560. } finally {
  561. w.webContents.zoomLevel = 0;
  562. }
  563. });
  564. it('can persist zoom level across navigation', (done) => {
  565. let finalNavigation = false;
  566. ipcMain.on('set-zoom', (e, host) => {
  567. const zoomLevel = hostZoomMap[host];
  568. if (!finalNavigation) w.webContents.zoomLevel = zoomLevel;
  569. console.log();
  570. e.sender.send(`${host}-zoom-set`);
  571. });
  572. ipcMain.on('host1-zoom-level', (e, zoomLevel) => {
  573. const expectedZoomLevel = hostZoomMap.host1;
  574. expect(zoomLevel).to.equal(expectedZoomLevel);
  575. if (finalNavigation) {
  576. done();
  577. } else {
  578. w.loadURL(`${zoomScheme}://host2`);
  579. }
  580. });
  581. ipcMain.once('host2-zoom-level', (e, zoomLevel) => {
  582. const expectedZoomLevel = hostZoomMap.host2;
  583. expect(zoomLevel).to.equal(expectedZoomLevel);
  584. finalNavigation = true;
  585. w.webContents.goBack();
  586. });
  587. w.loadURL(`${zoomScheme}://host1`);
  588. });
  589. it('can propagate zoom level across same session', (done) => {
  590. const w2 = new BrowserWindow({
  591. show: false
  592. });
  593. w2.webContents.on('did-finish-load', () => {
  594. const zoomLevel1 = w.webContents.zoomLevel;
  595. expect(zoomLevel1).to.equal(hostZoomMap.host3);
  596. const zoomLevel2 = w2.webContents.zoomLevel;
  597. expect(zoomLevel1).to.equal(zoomLevel2);
  598. w2.setClosable(true);
  599. w2.close();
  600. done();
  601. });
  602. w.webContents.on('did-finish-load', () => {
  603. w.webContents.zoomLevel = hostZoomMap.host3;
  604. w2.loadURL(`${zoomScheme}://host3`);
  605. });
  606. w.loadURL(`${zoomScheme}://host3`);
  607. });
  608. it('cannot propagate zoom level across different session', (done) => {
  609. const w2 = new BrowserWindow({
  610. show: false,
  611. webPreferences: {
  612. partition: 'temp'
  613. }
  614. });
  615. const protocol = w2.webContents.session.protocol;
  616. protocol.registerStringProtocol(zoomScheme, (request, callback) => {
  617. callback('hello');
  618. }, (error) => {
  619. if (error) return done(error);
  620. w2.webContents.on('did-finish-load', () => {
  621. const zoomLevel1 = w.webContents.zoomLevel;
  622. expect(zoomLevel1).to.equal(hostZoomMap.host3);
  623. const zoomLevel2 = w2.webContents.zoomLevel;
  624. expect(zoomLevel2).to.equal(0);
  625. expect(zoomLevel1).to.not.equal(zoomLevel2);
  626. protocol.unregisterProtocol(zoomScheme, (error) => {
  627. if (error) return done(error);
  628. w2.setClosable(true);
  629. w2.close();
  630. done();
  631. });
  632. });
  633. w.webContents.on('did-finish-load', () => {
  634. w.webContents.zoomLevel = hostZoomMap.host3;
  635. w2.loadURL(`${zoomScheme}://host3`);
  636. });
  637. w.loadURL(`${zoomScheme}://host3`);
  638. });
  639. });
  640. it('can persist when it contains iframe', (done) => {
  641. const server = http.createServer((req, res) => {
  642. setTimeout(() => {
  643. res.end();
  644. }, 200);
  645. });
  646. server.listen(0, '127.0.0.1', () => {
  647. const url = 'http://127.0.0.1:' + server.address().port;
  648. const content = `<iframe src=${url}></iframe>`;
  649. w.webContents.on('did-frame-finish-load', (e, isMainFrame) => {
  650. if (!isMainFrame) {
  651. const zoomLevel = w.webContents.zoomLevel;
  652. expect(zoomLevel).to.equal(2.0);
  653. w.webContents.zoomLevel = 0;
  654. server.close();
  655. done();
  656. }
  657. });
  658. w.webContents.on('dom-ready', () => {
  659. w.webContents.zoomLevel = 2.0;
  660. });
  661. w.loadURL(`data:text/html,${content}`);
  662. });
  663. });
  664. it('cannot propagate when used with webframe', (done) => {
  665. let finalZoomLevel = 0;
  666. const w2 = new BrowserWindow({
  667. show: false
  668. });
  669. w2.webContents.on('did-finish-load', () => {
  670. const zoomLevel1 = w.webContents.zoomLevel;
  671. expect(zoomLevel1).to.equal(finalZoomLevel);
  672. const zoomLevel2 = w2.webContents.zoomLevel;
  673. expect(zoomLevel2).to.equal(0);
  674. expect(zoomLevel1).to.not.equal(zoomLevel2);
  675. w2.setClosable(true);
  676. w2.close();
  677. done();
  678. });
  679. ipcMain.once('temporary-zoom-set', (e, zoomLevel) => {
  680. w2.loadFile(path.join(fixtures, 'pages', 'c.html'));
  681. finalZoomLevel = zoomLevel;
  682. });
  683. w.loadFile(path.join(fixtures, 'pages', 'webframe-zoom.html'));
  684. });
  685. it('cannot persist zoom level after navigation with webFrame', (done) => {
  686. let initialNavigation = true;
  687. const source = `
  688. const {ipcRenderer, webFrame} = require('electron')
  689. webFrame.setZoomLevel(0.6)
  690. ipcRenderer.send('zoom-level-set', webFrame.getZoomLevel())
  691. `;
  692. w.webContents.on('did-finish-load', () => {
  693. if (initialNavigation) {
  694. w.webContents.executeJavaScript(source);
  695. } else {
  696. const zoomLevel = w.webContents.zoomLevel;
  697. expect(zoomLevel).to.equal(0);
  698. done();
  699. }
  700. });
  701. ipcMain.once('zoom-level-set', (e, zoomLevel) => {
  702. expect(zoomLevel).to.equal(0.6);
  703. w.loadFile(path.join(fixtures, 'pages', 'd.html'));
  704. initialNavigation = false;
  705. });
  706. w.loadFile(path.join(fixtures, 'pages', 'c.html'));
  707. });
  708. });
  709. describe('webrtc ip policy api', () => {
  710. it('can set and get webrtc ip policies', () => {
  711. const policies = [
  712. 'default',
  713. 'default_public_interface_only',
  714. 'default_public_and_private_interfaces',
  715. 'disable_non_proxied_udp'
  716. ];
  717. policies.forEach((policy) => {
  718. w.webContents.setWebRTCIPHandlingPolicy(policy);
  719. expect(w.webContents.getWebRTCIPHandlingPolicy()).to.equal(policy);
  720. });
  721. });
  722. });
  723. describe('render view deleted events', () => {
  724. let server = null;
  725. before((done) => {
  726. server = http.createServer((req, res) => {
  727. const respond = () => {
  728. if (req.url === '/redirect-cross-site') {
  729. res.setHeader('Location', `${server.cross_site_url}/redirected`);
  730. res.statusCode = 302;
  731. res.end();
  732. } else if (req.url === '/redirected') {
  733. res.end('<html><script>window.localStorage</script></html>');
  734. } else {
  735. res.end();
  736. }
  737. };
  738. setTimeout(respond, 0);
  739. });
  740. server.listen(0, '127.0.0.1', () => {
  741. server.url = `http://127.0.0.1:${server.address().port}`;
  742. server.cross_site_url = `http://localhost:${server.address().port}`;
  743. done();
  744. });
  745. });
  746. after(() => {
  747. server.close();
  748. server = null;
  749. });
  750. it('does not emit current-render-view-deleted when speculative RVHs are deleted', (done) => {
  751. let currentRenderViewDeletedEmitted = false;
  752. w.webContents.once('destroyed', () => {
  753. expect(currentRenderViewDeletedEmitted).to.be.false('current-render-view-deleted was emitted');
  754. done();
  755. });
  756. const renderViewDeletedHandler = () => {
  757. currentRenderViewDeletedEmitted = true;
  758. };
  759. w.webContents.on('current-render-view-deleted', renderViewDeletedHandler);
  760. w.webContents.on('did-finish-load', (e) => {
  761. w.webContents.removeListener('current-render-view-deleted', renderViewDeletedHandler);
  762. w.close();
  763. });
  764. w.loadURL(`${server.url}/redirect-cross-site`);
  765. });
  766. it('emits current-render-view-deleted if the current RVHs are deleted', (done) => {
  767. let currentRenderViewDeletedEmitted = false;
  768. w.webContents.once('destroyed', () => {
  769. expect(currentRenderViewDeletedEmitted).to.be.true('current-render-view-deleted wasn\'t emitted');
  770. done();
  771. });
  772. w.webContents.on('current-render-view-deleted', () => {
  773. currentRenderViewDeletedEmitted = true;
  774. });
  775. w.webContents.on('did-finish-load', (e) => {
  776. w.close();
  777. });
  778. w.loadURL(`${server.url}/redirect-cross-site`);
  779. });
  780. it('emits render-view-deleted if any RVHs are deleted', (done) => {
  781. let rvhDeletedCount = 0;
  782. w.webContents.once('destroyed', () => {
  783. const expectedRenderViewDeletedEventCount = 3; // 1 speculative upon redirection + 2 upon window close.
  784. expect(rvhDeletedCount).to.equal(expectedRenderViewDeletedEventCount, 'render-view-deleted wasn\'t emitted the expected nr. of times');
  785. done();
  786. });
  787. w.webContents.on('render-view-deleted', () => {
  788. rvhDeletedCount++;
  789. });
  790. w.webContents.on('did-finish-load', (e) => {
  791. w.close();
  792. });
  793. w.loadURL(`${server.url}/redirect-cross-site`);
  794. });
  795. });
  796. describe('setIgnoreMenuShortcuts(ignore)', () => {
  797. it('does not throw', () => {
  798. expect(() => {
  799. w.webContents.setIgnoreMenuShortcuts(true);
  800. w.webContents.setIgnoreMenuShortcuts(false);
  801. }).to.not.throw();
  802. });
  803. });
  804. describe('create()', () => {
  805. it('does not crash on exit', async () => {
  806. const appPath = path.join(__dirname, 'fixtures', 'api', 'leak-exit-webcontents.js');
  807. const electronPath = remote.getGlobal('process').execPath;
  808. const appProcess = ChildProcess.spawn(electronPath, [appPath]);
  809. const [code] = await emittedOnce(appProcess, 'close');
  810. expect(code).to.equal(0);
  811. });
  812. });
  813. // Destroying webContents in its event listener is going to crash when
  814. // Electron is built in Debug mode.
  815. xdescribe('destroy()', () => {
  816. let server;
  817. before((done) => {
  818. server = http.createServer((request, response) => {
  819. switch (request.url) {
  820. case '/404':
  821. response.statusCode = '404';
  822. response.end();
  823. break;
  824. case '/301':
  825. response.statusCode = '301';
  826. response.setHeader('Location', '/200');
  827. response.end();
  828. break;
  829. case '/200':
  830. response.statusCode = '200';
  831. response.end('hello');
  832. break;
  833. default:
  834. done('unsupported endpoint');
  835. }
  836. }).listen(0, '127.0.0.1', () => {
  837. server.url = 'http://127.0.0.1:' + server.address().port;
  838. done();
  839. });
  840. });
  841. after(() => {
  842. server.close();
  843. server = null;
  844. });
  845. it('should not crash when invoked synchronously inside navigation observer', (done) => {
  846. const events = [
  847. { name: 'did-start-loading', url: `${server.url}/200` },
  848. { name: 'dom-ready', url: `${server.url}/200` },
  849. { name: 'did-stop-loading', url: `${server.url}/200` },
  850. { name: 'did-finish-load', url: `${server.url}/200` },
  851. // FIXME: Multiple Emit calls inside an observer assume that object
  852. // will be alive till end of the observer. Synchronous `destroy` api
  853. // violates this contract and crashes.
  854. // { name: 'did-frame-finish-load', url: `${server.url}/200` },
  855. { name: 'did-fail-load', url: `${server.url}/404` }
  856. ];
  857. const responseEvent = 'webcontents-destroyed';
  858. function * genNavigationEvent () {
  859. let eventOptions = null;
  860. while ((eventOptions = events.shift()) && events.length) {
  861. eventOptions.responseEvent = responseEvent;
  862. ipcRenderer.send('test-webcontents-navigation-observer', eventOptions);
  863. yield 1;
  864. }
  865. }
  866. const gen = genNavigationEvent();
  867. ipcRenderer.on(responseEvent, () => {
  868. if (!gen.next().value) done();
  869. });
  870. gen.next();
  871. });
  872. });
  873. describe('did-change-theme-color event', () => {
  874. it('is triggered with correct theme color', (done) => {
  875. let count = 0;
  876. w.webContents.on('did-change-theme-color', (e, color) => {
  877. if (count === 0) {
  878. count += 1;
  879. expect(color).to.equal('#FFEEDD');
  880. w.loadFile(path.join(fixtures, 'pages', 'base-page.html'));
  881. } else if (count === 1) {
  882. expect(color).to.be.null();
  883. done();
  884. }
  885. });
  886. w.loadFile(path.join(fixtures, 'pages', 'theme-color.html'));
  887. });
  888. });
  889. describe('console-message event', () => {
  890. it('is triggered with correct log message', (done) => {
  891. w.webContents.on('console-message', (e, level, message) => {
  892. // Don't just assert as Chromium might emit other logs that we should ignore.
  893. if (message === 'a') {
  894. done();
  895. }
  896. });
  897. w.loadFile(path.join(fixtures, 'pages', 'a.html'));
  898. });
  899. });
  900. describe('ipc-message event', () => {
  901. it('emits when the renderer process sends an asynchronous message', async () => {
  902. const webContents = remote.getCurrentWebContents();
  903. const promise = emittedOnce(webContents, 'ipc-message');
  904. ipcRenderer.send('message', 'Hello World!');
  905. const [, channel, message] = await promise;
  906. expect(channel).to.equal('message');
  907. expect(message).to.equal('Hello World!');
  908. });
  909. });
  910. describe('ipc-message-sync event', () => {
  911. it('emits when the renderer process sends a synchronous message', async () => {
  912. const webContents = remote.getCurrentWebContents();
  913. const promise = emittedOnce(webContents, 'ipc-message-sync');
  914. ipcRenderer.send('handle-next-ipc-message-sync', 'foobar');
  915. const result = ipcRenderer.sendSync('message', 'Hello World!');
  916. const [, channel, message] = await promise;
  917. expect(channel).to.equal('message');
  918. expect(message).to.equal('Hello World!');
  919. expect(result).to.equal('foobar');
  920. });
  921. });
  922. describe('referrer', () => {
  923. it('propagates referrer information to new target=_blank windows', (done) => {
  924. const server = http.createServer((req, res) => {
  925. if (req.url === '/should_have_referrer') {
  926. expect(req.headers.referer).to.equal(`http://127.0.0.1:${server.address().port}/`);
  927. return done();
  928. }
  929. res.end('<a id="a" href="/should_have_referrer" target="_blank">link</a>');
  930. });
  931. server.listen(0, '127.0.0.1', () => {
  932. const url = 'http://127.0.0.1:' + server.address().port + '/';
  933. w.webContents.once('did-finish-load', () => {
  934. w.webContents.once('new-window', (event, newUrl, frameName, disposition, options, features, referrer) => {
  935. expect(referrer.url).to.equal(url);
  936. expect(referrer.policy).to.equal('no-referrer-when-downgrade');
  937. });
  938. w.webContents.executeJavaScript('a.click()');
  939. });
  940. w.loadURL(url);
  941. });
  942. });
  943. // TODO(jeremy): window.open() in a real browser passes the referrer, but
  944. // our hacked-up window.open() shim doesn't. It should.
  945. xit('propagates referrer information to windows opened with window.open', (done) => {
  946. const server = http.createServer((req, res) => {
  947. if (req.url === '/should_have_referrer') {
  948. expect(req.headers.referer).to.equal(`http://127.0.0.1:${server.address().port}/`);
  949. return done();
  950. }
  951. res.end('');
  952. });
  953. server.listen(0, '127.0.0.1', () => {
  954. const url = 'http://127.0.0.1:' + server.address().port + '/';
  955. w.webContents.once('did-finish-load', () => {
  956. w.webContents.once('new-window', (event, newUrl, frameName, disposition, options, features, referrer) => {
  957. expect(referrer.url).to.equal(url);
  958. expect(referrer.policy).to.equal('no-referrer-when-downgrade');
  959. });
  960. w.webContents.executeJavaScript('window.open(location.href + "should_have_referrer")');
  961. });
  962. w.loadURL(url);
  963. });
  964. });
  965. });
  966. describe('webframe messages in sandboxed contents', () => {
  967. it('responds to executeJavaScript', async () => {
  968. w.destroy();
  969. w = new BrowserWindow({
  970. show: false,
  971. webPreferences: {
  972. sandbox: true
  973. }
  974. });
  975. await w.loadURL('about:blank');
  976. const result = await w.webContents.executeJavaScript('37 + 5');
  977. expect(result).to.equal(42);
  978. });
  979. });
  980. describe('preload-error event', () => {
  981. const generateSpecs = (description, sandbox) => {
  982. describe(description, () => {
  983. it('is triggered when unhandled exception is thrown', async () => {
  984. const preload = path.join(fixtures, 'module', 'preload-error-exception.js');
  985. w.destroy();
  986. w = new BrowserWindow({
  987. show: false,
  988. webPreferences: {
  989. sandbox,
  990. preload
  991. }
  992. });
  993. const promise = emittedOnce(w.webContents, 'preload-error');
  994. w.loadURL('about:blank');
  995. const [, preloadPath, error] = await promise;
  996. expect(preloadPath).to.equal(preload);
  997. expect(error.message).to.equal('Hello World!');
  998. });
  999. it('is triggered on syntax errors', async () => {
  1000. const preload = path.join(fixtures, 'module', 'preload-error-syntax.js');
  1001. w.destroy();
  1002. w = new BrowserWindow({
  1003. show: false,
  1004. webPreferences: {
  1005. sandbox,
  1006. preload
  1007. }
  1008. });
  1009. const promise = emittedOnce(w.webContents, 'preload-error');
  1010. w.loadURL('about:blank');
  1011. const [, preloadPath, error] = await promise;
  1012. expect(preloadPath).to.equal(preload);
  1013. expect(error.message).to.equal('foobar is not defined');
  1014. });
  1015. it('is triggered when preload script loading fails', async () => {
  1016. const preload = path.join(fixtures, 'module', 'preload-invalid.js');
  1017. w.destroy();
  1018. w = new BrowserWindow({
  1019. show: false,
  1020. webPreferences: {
  1021. sandbox,
  1022. preload
  1023. }
  1024. });
  1025. const promise = emittedOnce(w.webContents, 'preload-error');
  1026. w.loadURL('about:blank');
  1027. const [, preloadPath, error] = await promise;
  1028. expect(preloadPath).to.equal(preload);
  1029. expect(error.message).to.contain('preload-invalid.js');
  1030. });
  1031. });
  1032. };
  1033. generateSpecs('without sandbox', false);
  1034. generateSpecs('with sandbox', true);
  1035. });
  1036. describe('takeHeapSnapshot()', () => {
  1037. it('works with sandboxed renderers', async () => {
  1038. w.destroy();
  1039. w = new BrowserWindow({
  1040. show: false,
  1041. webPreferences: {
  1042. sandbox: true
  1043. }
  1044. });
  1045. await w.loadURL('about:blank');
  1046. const filePath = path.join(remote.app.getPath('temp'), 'test.heapsnapshot');
  1047. const cleanup = () => {
  1048. try {
  1049. fs.unlinkSync(filePath);
  1050. } catch (e) {
  1051. // ignore error
  1052. }
  1053. };
  1054. try {
  1055. await w.webContents.takeHeapSnapshot(filePath);
  1056. const stats = fs.statSync(filePath);
  1057. expect(stats.size).not.to.be.equal(0);
  1058. } finally {
  1059. cleanup();
  1060. }
  1061. });
  1062. it('fails with invalid file path', async () => {
  1063. w.destroy();
  1064. w = new BrowserWindow({
  1065. show: false,
  1066. webPreferences: {
  1067. sandbox: true
  1068. }
  1069. });
  1070. await w.loadURL('about:blank');
  1071. const promise = w.webContents.takeHeapSnapshot('');
  1072. return expect(promise).to.be.eventually.rejectedWith(Error, 'takeHeapSnapshot failed');
  1073. });
  1074. });
  1075. describe('setBackgroundThrottling()', () => {
  1076. it('does not crash when allowing', (done) => {
  1077. w.webContents.setBackgroundThrottling(true);
  1078. done();
  1079. });
  1080. it('does not crash when disallowing', (done) => {
  1081. w.destroy();
  1082. w = new BrowserWindow({
  1083. show: false,
  1084. width: 400,
  1085. height: 400,
  1086. webPreferences: {
  1087. backgroundThrottling: true
  1088. }
  1089. });
  1090. w.webContents.setBackgroundThrottling(false);
  1091. done();
  1092. });
  1093. it('does not crash when called via BrowserWindow', (done) => {
  1094. w.setBackgroundThrottling(true);
  1095. done();
  1096. });
  1097. });
  1098. describe('getPrinterList()', () => {
  1099. before(function () {
  1100. if (!features.isPrintingEnabled()) {
  1101. return closeWindow(w).then(() => {
  1102. w = null;
  1103. this.skip();
  1104. });
  1105. }
  1106. });
  1107. it('can get printer list', async () => {
  1108. w.destroy();
  1109. w = new BrowserWindow({
  1110. show: false,
  1111. webPreferences: {
  1112. sandbox: true
  1113. }
  1114. });
  1115. await w.loadURL('data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E');
  1116. const printers = w.webContents.getPrinters();
  1117. expect(printers).to.be.an('array');
  1118. });
  1119. });
  1120. describe('printToPDF()', () => {
  1121. before(function () {
  1122. if (!features.isPrintingEnabled()) {
  1123. return closeWindow(w).then(() => {
  1124. w = null;
  1125. this.skip();
  1126. });
  1127. }
  1128. });
  1129. it('can print to PDF', async () => {
  1130. w.destroy();
  1131. w = new BrowserWindow({
  1132. show: false,
  1133. webPreferences: {
  1134. sandbox: true
  1135. }
  1136. });
  1137. await w.loadURL('data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E');
  1138. const data = await w.webContents.printToPDF({});
  1139. expect(data).to.be.an.instanceof(Buffer).that.is.not.empty();
  1140. });
  1141. it('does not crash when called multiple times', async () => {
  1142. w.destroy();
  1143. w = new BrowserWindow({
  1144. show: false,
  1145. webPreferences: {
  1146. sandbox: true
  1147. }
  1148. });
  1149. await w.loadURL('data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E');
  1150. const promises = [];
  1151. for (let i = 0; i < 2; i++) {
  1152. promises.push(w.webContents.printToPDF({}));
  1153. }
  1154. const results = await Promise.all(promises);
  1155. for (const data of results) {
  1156. expect(data).to.be.an.instanceof(Buffer).that.is.not.empty();
  1157. }
  1158. });
  1159. });
  1160. describe('PictureInPicture video', () => {
  1161. it('works as expected', (done) => {
  1162. w.destroy();
  1163. w = new BrowserWindow({
  1164. show: false,
  1165. webPreferences: {
  1166. sandbox: true
  1167. }
  1168. });
  1169. w.webContents.once('did-finish-load', async () => {
  1170. const result = await w.webContents.executeJavaScript(
  1171. `runTest(${features.isPictureInPictureEnabled()})`, true);
  1172. expect(result).to.be.true();
  1173. done();
  1174. });
  1175. w.loadFile(path.join(fixtures, 'api', 'picture-in-picture.html'));
  1176. });
  1177. });
  1178. });