webview-spec.ts 75 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117
  1. import * as path from 'path';
  2. import * as url from 'url';
  3. import { BrowserWindow, session, ipcMain, app, WebContents } from 'electron/main';
  4. import { closeAllWindows } from './lib/window-helpers';
  5. import { emittedUntil } from './lib/events-helpers';
  6. import { ifit, ifdescribe, defer, itremote, useRemoteContext, listen } from './lib/spec-helpers';
  7. import { expect } from 'chai';
  8. import * as http from 'http';
  9. import * as auth from 'basic-auth';
  10. import { once } from 'events';
  11. import { setTimeout } from 'timers/promises';
  12. declare let WebView: any;
  13. const features = process._linkedBinding('electron_common_features');
  14. async function loadWebView (w: WebContents, attributes: Record<string, string>, opts?: {openDevTools?: boolean}): Promise<void> {
  15. const { openDevTools } = {
  16. openDevTools: false,
  17. ...opts
  18. };
  19. await w.executeJavaScript(`
  20. new Promise((resolve, reject) => {
  21. const webview = new WebView()
  22. webview.id = 'webview'
  23. for (const [k, v] of Object.entries(${JSON.stringify(attributes)})) {
  24. webview.setAttribute(k, v)
  25. }
  26. document.body.appendChild(webview)
  27. webview.addEventListener('dom-ready', () => {
  28. if (${openDevTools}) {
  29. webview.openDevTools()
  30. }
  31. })
  32. webview.addEventListener('did-finish-load', () => {
  33. resolve()
  34. })
  35. })
  36. `);
  37. }
  38. async function loadWebViewAndWaitForEvent (w: WebContents, attributes: Record<string, string>, eventName: string): Promise<any> {
  39. return await w.executeJavaScript(`new Promise((resolve, reject) => {
  40. const webview = new WebView()
  41. webview.id = 'webview'
  42. for (const [k, v] of Object.entries(${JSON.stringify(attributes)})) {
  43. webview.setAttribute(k, v)
  44. }
  45. webview.addEventListener(${JSON.stringify(eventName)}, (e) => resolve({...e}), {once: true})
  46. document.body.appendChild(webview)
  47. })`);
  48. };
  49. async function loadWebViewAndWaitForMessage (w: WebContents, attributes: Record<string, string>): Promise<string> {
  50. const { message } = await loadWebViewAndWaitForEvent(w, attributes, 'console-message');
  51. return message;
  52. };
  53. describe('<webview> tag', function () {
  54. const fixtures = path.join(__dirname, 'fixtures');
  55. const blankPageUrl = url.pathToFileURL(path.join(fixtures, 'pages', 'blank.html')).toString();
  56. function hideChildWindows (e: any, wc: WebContents) {
  57. wc.setWindowOpenHandler(() => ({
  58. action: 'allow',
  59. overrideBrowserWindowOptions: {
  60. show: false
  61. }
  62. }));
  63. }
  64. before(() => {
  65. app.on('web-contents-created', hideChildWindows);
  66. });
  67. after(() => {
  68. app.off('web-contents-created', hideChildWindows);
  69. });
  70. describe('behavior', () => {
  71. afterEach(closeAllWindows);
  72. it('works without script tag in page', async () => {
  73. const w = new BrowserWindow({
  74. show: false,
  75. webPreferences: {
  76. webviewTag: true,
  77. nodeIntegration: true,
  78. contextIsolation: false
  79. }
  80. });
  81. w.loadFile(path.join(fixtures, 'pages', 'webview-no-script.html'));
  82. await once(ipcMain, 'pong');
  83. });
  84. it('works with sandbox', async () => {
  85. const w = new BrowserWindow({
  86. show: false,
  87. webPreferences: {
  88. webviewTag: true,
  89. sandbox: true
  90. }
  91. });
  92. w.loadFile(path.join(fixtures, 'pages', 'webview-isolated.html'));
  93. await once(ipcMain, 'pong');
  94. });
  95. it('works with contextIsolation', async () => {
  96. const w = new BrowserWindow({
  97. show: false,
  98. webPreferences: {
  99. webviewTag: true,
  100. contextIsolation: true
  101. }
  102. });
  103. w.loadFile(path.join(fixtures, 'pages', 'webview-isolated.html'));
  104. await once(ipcMain, 'pong');
  105. });
  106. it('works with contextIsolation + sandbox', async () => {
  107. const w = new BrowserWindow({
  108. show: false,
  109. webPreferences: {
  110. webviewTag: true,
  111. contextIsolation: true,
  112. sandbox: true
  113. }
  114. });
  115. w.loadFile(path.join(fixtures, 'pages', 'webview-isolated.html'));
  116. await once(ipcMain, 'pong');
  117. });
  118. it('works with Trusted Types', async () => {
  119. const w = new BrowserWindow({
  120. show: false,
  121. webPreferences: {
  122. webviewTag: true
  123. }
  124. });
  125. w.loadFile(path.join(fixtures, 'pages', 'webview-trusted-types.html'));
  126. await once(ipcMain, 'pong');
  127. });
  128. it('is disabled by default', async () => {
  129. const w = new BrowserWindow({
  130. show: false,
  131. webPreferences: {
  132. preload: path.join(fixtures, 'module', 'preload-webview.js'),
  133. nodeIntegration: true
  134. }
  135. });
  136. const webview = once(ipcMain, 'webview');
  137. w.loadFile(path.join(fixtures, 'pages', 'webview-no-script.html'));
  138. const [, type] = await webview;
  139. expect(type).to.equal('undefined', 'WebView still exists');
  140. });
  141. });
  142. // FIXME(deepak1556): Ch69 follow up.
  143. xdescribe('document.visibilityState/hidden', () => {
  144. afterEach(() => {
  145. ipcMain.removeAllListeners('pong');
  146. });
  147. afterEach(closeAllWindows);
  148. it('updates when the window is shown after the ready-to-show event', async () => {
  149. const w = new BrowserWindow({ show: false });
  150. const readyToShowSignal = once(w, 'ready-to-show');
  151. const pongSignal1 = once(ipcMain, 'pong');
  152. w.loadFile(path.join(fixtures, 'pages', 'webview-visibilitychange.html'));
  153. await pongSignal1;
  154. const pongSignal2 = once(ipcMain, 'pong');
  155. await readyToShowSignal;
  156. w.show();
  157. const [, visibilityState, hidden] = await pongSignal2;
  158. expect(visibilityState).to.equal('visible');
  159. expect(hidden).to.be.false();
  160. });
  161. it('inherits the parent window visibility state and receives visibilitychange events', async () => {
  162. const w = new BrowserWindow({ show: false });
  163. w.loadFile(path.join(fixtures, 'pages', 'webview-visibilitychange.html'));
  164. const [, visibilityState, hidden] = await once(ipcMain, 'pong');
  165. expect(visibilityState).to.equal('hidden');
  166. expect(hidden).to.be.true();
  167. // We have to start waiting for the event
  168. // before we ask the webContents to resize.
  169. const getResponse = once(ipcMain, 'pong');
  170. w.webContents.emit('-window-visibility-change', 'visible');
  171. return getResponse.then(([, visibilityState, hidden]) => {
  172. expect(visibilityState).to.equal('visible');
  173. expect(hidden).to.be.false();
  174. });
  175. });
  176. });
  177. describe('did-attach-webview event', () => {
  178. afterEach(closeAllWindows);
  179. it('is emitted when a webview has been attached', async () => {
  180. const w = new BrowserWindow({
  181. show: false,
  182. webPreferences: {
  183. webviewTag: true,
  184. nodeIntegration: true,
  185. contextIsolation: false
  186. }
  187. });
  188. const didAttachWebview = once(w.webContents, 'did-attach-webview');
  189. const webviewDomReady = once(ipcMain, 'webview-dom-ready');
  190. w.loadFile(path.join(fixtures, 'pages', 'webview-did-attach-event.html'));
  191. const [, webContents] = await didAttachWebview;
  192. const [, id] = await webviewDomReady;
  193. expect(webContents.id).to.equal(id);
  194. });
  195. });
  196. describe('did-attach event', () => {
  197. afterEach(closeAllWindows);
  198. it('is emitted when a webview has been attached', async () => {
  199. const w = new BrowserWindow({
  200. webPreferences: {
  201. webviewTag: true
  202. }
  203. });
  204. await w.loadURL('about:blank');
  205. const message = await w.webContents.executeJavaScript(`new Promise((resolve, reject) => {
  206. const webview = new WebView()
  207. webview.setAttribute('src', 'about:blank')
  208. webview.addEventListener('did-attach', (e) => {
  209. resolve('ok')
  210. })
  211. document.body.appendChild(webview)
  212. })`);
  213. expect(message).to.equal('ok');
  214. });
  215. });
  216. describe('did-change-theme-color event', () => {
  217. afterEach(closeAllWindows);
  218. it('emits when theme color changes', async () => {
  219. const w = new BrowserWindow({
  220. webPreferences: {
  221. webviewTag: true
  222. }
  223. });
  224. await w.loadURL('about:blank');
  225. const src = url.format({
  226. pathname: `${fixtures.replace(/\\/g, '/')}/pages/theme-color.html`,
  227. protocol: 'file',
  228. slashes: true
  229. });
  230. const message = await w.webContents.executeJavaScript(`new Promise((resolve, reject) => {
  231. const webview = new WebView()
  232. webview.setAttribute('src', '${src}')
  233. webview.addEventListener('did-change-theme-color', (e) => {
  234. resolve('ok')
  235. })
  236. document.body.appendChild(webview)
  237. })`);
  238. expect(message).to.equal('ok');
  239. });
  240. });
  241. describe('devtools', () => {
  242. afterEach(closeAllWindows);
  243. // FIXME: This test is flaky on WOA, so skip it there.
  244. ifit(process.platform !== 'win32' || process.arch !== 'arm64')('loads devtools extensions registered on the parent window', async () => {
  245. const w = new BrowserWindow({
  246. show: false,
  247. webPreferences: {
  248. webviewTag: true,
  249. nodeIntegration: true,
  250. contextIsolation: false
  251. }
  252. });
  253. w.webContents.session.removeExtension('foo');
  254. const extensionPath = path.join(__dirname, 'fixtures', 'devtools-extensions', 'foo');
  255. await w.webContents.session.loadExtension(extensionPath, {
  256. allowFileAccess: true
  257. });
  258. w.loadFile(path.join(__dirname, 'fixtures', 'pages', 'webview-devtools.html'));
  259. loadWebView(w.webContents, {
  260. nodeintegration: 'on',
  261. webpreferences: 'contextIsolation=no',
  262. src: `file://${path.join(__dirname, 'fixtures', 'blank.html')}`
  263. }, { openDevTools: true });
  264. let childWebContentsId = 0;
  265. app.once('web-contents-created', (e, webContents) => {
  266. childWebContentsId = webContents.id;
  267. webContents.on('devtools-opened', function () {
  268. const showPanelIntervalId = setInterval(function () {
  269. if (!webContents.isDestroyed() && webContents.devToolsWebContents) {
  270. webContents.devToolsWebContents.executeJavaScript('(' + function () {
  271. const { UI } = (window as any);
  272. const tabs = UI.inspectorView.tabbedPane.tabs;
  273. const lastPanelId: any = tabs[tabs.length - 1].id;
  274. UI.inspectorView.showPanel(lastPanelId);
  275. }.toString() + ')()');
  276. } else {
  277. clearInterval(showPanelIntervalId);
  278. }
  279. }, 100);
  280. });
  281. });
  282. const [, { runtimeId, tabId }] = await once(ipcMain, 'answer');
  283. expect(runtimeId).to.match(/^[a-z]{32}$/);
  284. expect(tabId).to.equal(childWebContentsId);
  285. await w.webContents.executeJavaScript('webview.closeDevTools()');
  286. });
  287. });
  288. describe('zoom behavior', () => {
  289. const zoomScheme = standardScheme;
  290. const webviewSession = session.fromPartition('webview-temp');
  291. afterEach(closeAllWindows);
  292. before(() => {
  293. const protocol = webviewSession.protocol;
  294. protocol.registerStringProtocol(zoomScheme, (request, callback) => {
  295. callback('hello');
  296. });
  297. });
  298. after(() => {
  299. const protocol = webviewSession.protocol;
  300. protocol.unregisterProtocol(zoomScheme);
  301. });
  302. it('inherits the zoomFactor of the parent window', async () => {
  303. const w = new BrowserWindow({
  304. show: false,
  305. webPreferences: {
  306. webviewTag: true,
  307. nodeIntegration: true,
  308. zoomFactor: 1.2,
  309. contextIsolation: false
  310. }
  311. });
  312. const zoomEventPromise = once(ipcMain, 'webview-parent-zoom-level');
  313. w.loadFile(path.join(fixtures, 'pages', 'webview-zoom-factor.html'));
  314. const [, zoomFactor, zoomLevel] = await zoomEventPromise;
  315. expect(zoomFactor).to.equal(1.2);
  316. expect(zoomLevel).to.equal(1);
  317. });
  318. it('maintains zoom level on navigation', async () => {
  319. const w = new BrowserWindow({
  320. show: false,
  321. webPreferences: {
  322. webviewTag: true,
  323. nodeIntegration: true,
  324. zoomFactor: 1.2,
  325. contextIsolation: false
  326. }
  327. });
  328. const promise = new Promise<void>((resolve) => {
  329. ipcMain.on('webview-zoom-level', (event, zoomLevel, zoomFactor, newHost, final) => {
  330. if (!newHost) {
  331. expect(zoomFactor).to.equal(1.44);
  332. expect(zoomLevel).to.equal(2.0);
  333. } else {
  334. expect(zoomFactor).to.equal(1.2);
  335. expect(zoomLevel).to.equal(1);
  336. }
  337. if (final) {
  338. resolve();
  339. }
  340. });
  341. });
  342. w.loadFile(path.join(fixtures, 'pages', 'webview-custom-zoom-level.html'));
  343. await promise;
  344. });
  345. it('maintains zoom level when navigating within same page', async () => {
  346. const w = new BrowserWindow({
  347. show: false,
  348. webPreferences: {
  349. webviewTag: true,
  350. nodeIntegration: true,
  351. zoomFactor: 1.2,
  352. contextIsolation: false
  353. }
  354. });
  355. const promise = new Promise<void>((resolve) => {
  356. ipcMain.on('webview-zoom-in-page', (event, zoomLevel, zoomFactor, final) => {
  357. expect(zoomFactor).to.equal(1.44);
  358. expect(zoomLevel).to.equal(2.0);
  359. if (final) {
  360. resolve();
  361. }
  362. });
  363. });
  364. w.loadFile(path.join(fixtures, 'pages', 'webview-in-page-navigate.html'));
  365. await promise;
  366. });
  367. it('inherits zoom level for the origin when available', async () => {
  368. const w = new BrowserWindow({
  369. show: false,
  370. webPreferences: {
  371. webviewTag: true,
  372. nodeIntegration: true,
  373. zoomFactor: 1.2,
  374. contextIsolation: false
  375. }
  376. });
  377. w.loadFile(path.join(fixtures, 'pages', 'webview-origin-zoom-level.html'));
  378. const [, zoomLevel] = await once(ipcMain, 'webview-origin-zoom-level');
  379. expect(zoomLevel).to.equal(2.0);
  380. });
  381. it('does not crash when navigating with zoom level inherited from parent', async () => {
  382. const w = new BrowserWindow({
  383. show: false,
  384. webPreferences: {
  385. webviewTag: true,
  386. nodeIntegration: true,
  387. zoomFactor: 1.2,
  388. session: webviewSession,
  389. contextIsolation: false
  390. }
  391. });
  392. const attachPromise = once(w.webContents, 'did-attach-webview');
  393. const readyPromise = once(ipcMain, 'dom-ready');
  394. w.loadFile(path.join(fixtures, 'pages', 'webview-zoom-inherited.html'));
  395. const [, webview] = await attachPromise;
  396. await readyPromise;
  397. expect(webview.getZoomFactor()).to.equal(1.2);
  398. await w.loadURL(`${zoomScheme}://host1`);
  399. });
  400. it('does not crash when changing zoom level after webview is destroyed', async () => {
  401. const w = new BrowserWindow({
  402. show: false,
  403. webPreferences: {
  404. webviewTag: true,
  405. nodeIntegration: true,
  406. session: webviewSession,
  407. contextIsolation: false
  408. }
  409. });
  410. const attachPromise = once(w.webContents, 'did-attach-webview');
  411. await w.loadFile(path.join(fixtures, 'pages', 'webview-zoom-inherited.html'));
  412. await attachPromise;
  413. await w.webContents.executeJavaScript('view.remove()');
  414. w.webContents.setZoomLevel(0.5);
  415. });
  416. });
  417. describe('requestFullscreen from webview', () => {
  418. afterEach(closeAllWindows);
  419. const loadWebViewWindow = async () => {
  420. const w = new BrowserWindow({
  421. webPreferences: {
  422. webviewTag: true,
  423. nodeIntegration: true,
  424. contextIsolation: false
  425. }
  426. });
  427. const attachPromise = once(w.webContents, 'did-attach-webview');
  428. const loadPromise = once(w.webContents, 'did-finish-load');
  429. const readyPromise = once(ipcMain, 'webview-ready');
  430. w.loadFile(path.join(__dirname, 'fixtures', 'webview', 'fullscreen', 'main.html'));
  431. const [, webview] = await attachPromise;
  432. await Promise.all([readyPromise, loadPromise]);
  433. return [w, webview];
  434. };
  435. afterEach(async () => {
  436. // The leaving animation is un-observable but can interfere with future tests
  437. // Specifically this is async on macOS but can be on other platforms too
  438. await setTimeout(1000);
  439. closeAllWindows();
  440. });
  441. ifit(process.platform !== 'darwin')('should make parent frame element fullscreen too (non-macOS)', async () => {
  442. const [w, webview] = await loadWebViewWindow();
  443. expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.false();
  444. const parentFullscreen = once(ipcMain, 'fullscreenchange');
  445. await webview.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
  446. await parentFullscreen;
  447. expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.true();
  448. const close = once(w, 'closed');
  449. w.close();
  450. await close;
  451. });
  452. ifit(process.platform === 'darwin')('should make parent frame element fullscreen too (macOS)', async () => {
  453. const [w, webview] = await loadWebViewWindow();
  454. expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.false();
  455. const parentFullscreen = once(ipcMain, 'fullscreenchange');
  456. const enterHTMLFS = once(w.webContents, 'enter-html-full-screen');
  457. const leaveHTMLFS = once(w.webContents, 'leave-html-full-screen');
  458. await webview.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
  459. expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.true();
  460. await webview.executeJavaScript('document.exitFullscreen()');
  461. await Promise.all([enterHTMLFS, leaveHTMLFS, parentFullscreen]);
  462. const close = once(w, 'closed');
  463. w.close();
  464. await close;
  465. });
  466. // FIXME(zcbenz): Fullscreen events do not work on Linux.
  467. ifit(process.platform !== 'linux')('exiting fullscreen should unfullscreen window', async () => {
  468. const [w, webview] = await loadWebViewWindow();
  469. const enterFullScreen = once(w, 'enter-full-screen');
  470. await webview.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
  471. await enterFullScreen;
  472. const leaveFullScreen = once(w, 'leave-full-screen');
  473. await webview.executeJavaScript('document.exitFullscreen()', true);
  474. await leaveFullScreen;
  475. await setTimeout();
  476. expect(w.isFullScreen()).to.be.false();
  477. const close = once(w, 'closed');
  478. w.close();
  479. await close;
  480. });
  481. it('pressing ESC should unfullscreen window', async () => {
  482. const [w, webview] = await loadWebViewWindow();
  483. const enterFullScreen = once(w, 'enter-full-screen');
  484. await webview.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
  485. await enterFullScreen;
  486. const leaveFullScreen = once(w, 'leave-full-screen');
  487. webview.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' });
  488. await leaveFullScreen;
  489. await setTimeout(1000);
  490. expect(w.isFullScreen()).to.be.false();
  491. const close = once(w, 'closed');
  492. w.close();
  493. await close;
  494. });
  495. it('pressing ESC should emit the leave-html-full-screen event', async () => {
  496. const w = new BrowserWindow({
  497. show: false,
  498. webPreferences: {
  499. webviewTag: true,
  500. nodeIntegration: true,
  501. contextIsolation: false
  502. }
  503. });
  504. const didAttachWebview = once(w.webContents, 'did-attach-webview');
  505. w.loadFile(path.join(fixtures, 'pages', 'webview-did-attach-event.html'));
  506. const [, webContents] = await didAttachWebview;
  507. const enterFSWindow = once(w, 'enter-html-full-screen');
  508. const enterFSWebview = once(webContents, 'enter-html-full-screen');
  509. await webContents.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
  510. await enterFSWindow;
  511. await enterFSWebview;
  512. const leaveFSWindow = once(w, 'leave-html-full-screen');
  513. const leaveFSWebview = once(webContents, 'leave-html-full-screen');
  514. webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' });
  515. await leaveFSWebview;
  516. await leaveFSWindow;
  517. const close = once(w, 'closed');
  518. w.close();
  519. await close;
  520. });
  521. it('should support user gesture', async () => {
  522. const [w, webview] = await loadWebViewWindow();
  523. const waitForEnterHtmlFullScreen = once(webview, 'enter-html-full-screen');
  524. const jsScript = "document.querySelector('video').webkitRequestFullscreen()";
  525. webview.executeJavaScript(jsScript, true);
  526. await waitForEnterHtmlFullScreen;
  527. const close = once(w, 'closed');
  528. w.close();
  529. await close;
  530. });
  531. });
  532. describe('child windows', () => {
  533. let w: BrowserWindow;
  534. beforeEach(async () => {
  535. w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webviewTag: true, contextIsolation: false } });
  536. await w.loadURL('about:blank');
  537. });
  538. afterEach(closeAllWindows);
  539. it('opens window of about:blank with cross-scripting enabled', async () => {
  540. // Don't wait for loading to finish.
  541. loadWebView(w.webContents, {
  542. allowpopups: 'on',
  543. nodeintegration: 'on',
  544. webpreferences: 'contextIsolation=no',
  545. src: `file://${path.join(fixtures, 'api', 'native-window-open-blank.html')}`
  546. });
  547. const [, content] = await once(ipcMain, 'answer');
  548. expect(content).to.equal('Hello');
  549. });
  550. it('opens window of same domain with cross-scripting enabled', async () => {
  551. // Don't wait for loading to finish.
  552. loadWebView(w.webContents, {
  553. allowpopups: 'on',
  554. nodeintegration: 'on',
  555. webpreferences: 'contextIsolation=no',
  556. src: `file://${path.join(fixtures, 'api', 'native-window-open-file.html')}`
  557. });
  558. const [, content] = await once(ipcMain, 'answer');
  559. expect(content).to.equal('Hello');
  560. });
  561. it('returns null from window.open when allowpopups is not set', async () => {
  562. // Don't wait for loading to finish.
  563. loadWebView(w.webContents, {
  564. nodeintegration: 'on',
  565. webpreferences: 'contextIsolation=no',
  566. src: `file://${path.join(fixtures, 'api', 'native-window-open-no-allowpopups.html')}`
  567. });
  568. const [, { windowOpenReturnedNull }] = await once(ipcMain, 'answer');
  569. expect(windowOpenReturnedNull).to.be.true();
  570. });
  571. it('blocks accessing cross-origin frames', async () => {
  572. // Don't wait for loading to finish.
  573. loadWebView(w.webContents, {
  574. allowpopups: 'on',
  575. nodeintegration: 'on',
  576. webpreferences: 'contextIsolation=no',
  577. src: `file://${path.join(fixtures, 'api', 'native-window-open-cross-origin.html')}`
  578. });
  579. const [, content] = await once(ipcMain, 'answer');
  580. const expectedContent =
  581. 'Blocked a frame with origin "file://" from accessing a cross-origin frame.';
  582. expect(content).to.equal(expectedContent);
  583. });
  584. it('emits a browser-window-created event', async () => {
  585. // Don't wait for loading to finish.
  586. loadWebView(w.webContents, {
  587. allowpopups: 'on',
  588. webpreferences: 'contextIsolation=no',
  589. src: `file://${fixtures}/pages/window-open.html`
  590. });
  591. await once(app, 'browser-window-created');
  592. });
  593. it('emits a web-contents-created event', async () => {
  594. const webContentsCreated = emittedUntil(app, 'web-contents-created',
  595. (event: Electron.Event, contents: Electron.WebContents) => contents.getType() === 'window');
  596. loadWebView(w.webContents, {
  597. allowpopups: 'on',
  598. webpreferences: 'contextIsolation=no',
  599. src: `file://${fixtures}/pages/window-open.html`
  600. });
  601. await webContentsCreated;
  602. });
  603. it('does not crash when creating window with noopener', async () => {
  604. loadWebView(w.webContents, {
  605. allowpopups: 'on',
  606. src: `file://${path.join(fixtures, 'api', 'native-window-open-noopener.html')}`
  607. });
  608. await once(app, 'browser-window-created');
  609. });
  610. });
  611. describe('webpreferences attribute', () => {
  612. let w: BrowserWindow;
  613. beforeEach(async () => {
  614. w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webviewTag: true } });
  615. await w.loadURL('about:blank');
  616. });
  617. afterEach(closeAllWindows);
  618. it('can enable context isolation', async () => {
  619. loadWebView(w.webContents, {
  620. allowpopups: 'yes',
  621. preload: `file://${fixtures}/api/isolated-preload.js`,
  622. src: `file://${fixtures}/api/isolated.html`,
  623. webpreferences: 'contextIsolation=yes'
  624. });
  625. const [, data] = await once(ipcMain, 'isolated-world');
  626. expect(data).to.deep.equal({
  627. preloadContext: {
  628. preloadProperty: 'number',
  629. pageProperty: 'undefined',
  630. typeofRequire: 'function',
  631. typeofProcess: 'object',
  632. typeofArrayPush: 'function',
  633. typeofFunctionApply: 'function',
  634. typeofPreloadExecuteJavaScriptProperty: 'undefined'
  635. },
  636. pageContext: {
  637. preloadProperty: 'undefined',
  638. pageProperty: 'string',
  639. typeofRequire: 'undefined',
  640. typeofProcess: 'undefined',
  641. typeofArrayPush: 'number',
  642. typeofFunctionApply: 'boolean',
  643. typeofPreloadExecuteJavaScriptProperty: 'number',
  644. typeofOpenedWindow: 'object'
  645. }
  646. });
  647. });
  648. });
  649. describe('permission request handlers', () => {
  650. let w: BrowserWindow;
  651. beforeEach(async () => {
  652. w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webviewTag: true, contextIsolation: false } });
  653. await w.loadURL('about:blank');
  654. });
  655. afterEach(closeAllWindows);
  656. const partition = 'permissionTest';
  657. function setUpRequestHandler (webContentsId: number, requestedPermission: string) {
  658. return new Promise<void>((resolve, reject) => {
  659. session.fromPartition(partition).setPermissionRequestHandler(function (webContents, permission, callback) {
  660. if (webContents.id === webContentsId) {
  661. // requestMIDIAccess with sysex requests both midi and midiSysex so
  662. // grant the first midi one and then reject the midiSysex one
  663. if (requestedPermission === 'midiSysex' && permission === 'midi') {
  664. return callback(true);
  665. }
  666. try {
  667. expect(permission).to.equal(requestedPermission);
  668. } catch (e) {
  669. return reject(e);
  670. }
  671. callback(false);
  672. resolve();
  673. }
  674. });
  675. });
  676. }
  677. afterEach(() => {
  678. session.fromPartition(partition).setPermissionRequestHandler(null);
  679. });
  680. // This is disabled because CI machines don't have cameras or microphones,
  681. // so Chrome responds with "NotFoundError" instead of
  682. // "PermissionDeniedError". It should be re-enabled if we find a way to mock
  683. // the presence of a microphone & camera.
  684. xit('emits when using navigator.getUserMedia api', async () => {
  685. const errorFromRenderer = once(ipcMain, 'message');
  686. loadWebView(w.webContents, {
  687. src: `file://${fixtures}/pages/permissions/media.html`,
  688. partition,
  689. nodeintegration: 'on'
  690. });
  691. const [, webViewContents] = await once(app, 'web-contents-created');
  692. setUpRequestHandler(webViewContents.id, 'media');
  693. const [, errorName] = await errorFromRenderer;
  694. expect(errorName).to.equal('PermissionDeniedError');
  695. });
  696. it('emits when using navigator.geolocation api', async () => {
  697. const errorFromRenderer = once(ipcMain, 'message');
  698. loadWebView(w.webContents, {
  699. src: `file://${fixtures}/pages/permissions/geolocation.html`,
  700. partition,
  701. nodeintegration: 'on',
  702. webpreferences: 'contextIsolation=no'
  703. });
  704. const [, webViewContents] = await once(app, 'web-contents-created');
  705. setUpRequestHandler(webViewContents.id, 'geolocation');
  706. const [, error] = await errorFromRenderer;
  707. expect(error).to.equal('User denied Geolocation');
  708. });
  709. it('emits when using navigator.requestMIDIAccess without sysex api', async () => {
  710. const errorFromRenderer = once(ipcMain, 'message');
  711. loadWebView(w.webContents, {
  712. src: `file://${fixtures}/pages/permissions/midi.html`,
  713. partition,
  714. nodeintegration: 'on',
  715. webpreferences: 'contextIsolation=no'
  716. });
  717. const [, webViewContents] = await once(app, 'web-contents-created');
  718. setUpRequestHandler(webViewContents.id, 'midi');
  719. const [, error] = await errorFromRenderer;
  720. expect(error).to.equal('SecurityError');
  721. });
  722. it('emits when using navigator.requestMIDIAccess with sysex api', async () => {
  723. const errorFromRenderer = once(ipcMain, 'message');
  724. loadWebView(w.webContents, {
  725. src: `file://${fixtures}/pages/permissions/midi-sysex.html`,
  726. partition,
  727. nodeintegration: 'on',
  728. webpreferences: 'contextIsolation=no'
  729. });
  730. const [, webViewContents] = await once(app, 'web-contents-created');
  731. setUpRequestHandler(webViewContents.id, 'midiSysex');
  732. const [, error] = await errorFromRenderer;
  733. expect(error).to.equal('SecurityError');
  734. });
  735. it('emits when accessing external protocol', async () => {
  736. loadWebView(w.webContents, {
  737. src: 'magnet:test',
  738. partition
  739. });
  740. const [, webViewContents] = await once(app, 'web-contents-created');
  741. await setUpRequestHandler(webViewContents.id, 'openExternal');
  742. });
  743. it('emits when using Notification.requestPermission', async () => {
  744. const errorFromRenderer = once(ipcMain, 'message');
  745. loadWebView(w.webContents, {
  746. src: `file://${fixtures}/pages/permissions/notification.html`,
  747. partition,
  748. nodeintegration: 'on',
  749. webpreferences: 'contextIsolation=no'
  750. });
  751. const [, webViewContents] = await once(app, 'web-contents-created');
  752. await setUpRequestHandler(webViewContents.id, 'notifications');
  753. const [, error] = await errorFromRenderer;
  754. expect(error).to.equal('denied');
  755. });
  756. });
  757. describe('DOM events', () => {
  758. afterEach(closeAllWindows);
  759. it('receives extra properties on DOM events when contextIsolation is enabled', async () => {
  760. const w = new BrowserWindow({
  761. show: false,
  762. webPreferences: {
  763. webviewTag: true,
  764. contextIsolation: true
  765. }
  766. });
  767. await w.loadURL('about:blank');
  768. const message = await w.webContents.executeJavaScript(`new Promise((resolve, reject) => {
  769. const webview = new WebView()
  770. webview.setAttribute('src', 'data:text/html,<script>console.log("hi")</script>')
  771. webview.addEventListener('console-message', (e) => {
  772. resolve(e.message)
  773. })
  774. document.body.appendChild(webview)
  775. })`);
  776. expect(message).to.equal('hi');
  777. });
  778. it('emits focus event when contextIsolation is enabled', async () => {
  779. const w = new BrowserWindow({
  780. show: false,
  781. webPreferences: {
  782. webviewTag: true,
  783. contextIsolation: true
  784. }
  785. });
  786. await w.loadURL('about:blank');
  787. await w.webContents.executeJavaScript(`new Promise((resolve, reject) => {
  788. const webview = new WebView()
  789. webview.setAttribute('src', 'about:blank')
  790. webview.addEventListener('dom-ready', () => {
  791. webview.focus()
  792. })
  793. webview.addEventListener('focus', () => {
  794. resolve();
  795. })
  796. document.body.appendChild(webview)
  797. })`);
  798. });
  799. });
  800. describe('attributes', () => {
  801. let w: WebContents;
  802. before(async () => {
  803. const window = new BrowserWindow({
  804. show: false,
  805. webPreferences: {
  806. webviewTag: true,
  807. nodeIntegration: true,
  808. contextIsolation: false
  809. }
  810. });
  811. await window.loadURL(`file://${fixtures}/pages/blank.html`);
  812. w = window.webContents;
  813. });
  814. afterEach(async () => {
  815. await w.executeJavaScript(`{
  816. document.querySelectorAll('webview').forEach(el => el.remove())
  817. }`);
  818. });
  819. after(closeAllWindows);
  820. describe('src attribute', () => {
  821. it('specifies the page to load', async () => {
  822. const message = await loadWebViewAndWaitForMessage(w, {
  823. src: `file://${fixtures}/pages/a.html`
  824. });
  825. expect(message).to.equal('a');
  826. });
  827. it('navigates to new page when changed', async () => {
  828. await loadWebView(w, {
  829. src: `file://${fixtures}/pages/a.html`
  830. });
  831. const { message } = await w.executeJavaScript(`new Promise(resolve => {
  832. webview.addEventListener('console-message', e => resolve({message: e.message}))
  833. webview.src = ${JSON.stringify(`file://${fixtures}/pages/b.html`)}
  834. })`);
  835. expect(message).to.equal('b');
  836. });
  837. it('resolves relative URLs', async () => {
  838. const message = await loadWebViewAndWaitForMessage(w, {
  839. src: './e.html'
  840. });
  841. expect(message).to.equal('Window script is loaded before preload script');
  842. });
  843. it('ignores empty values', async () => {
  844. loadWebView(w, {});
  845. for (const emptyValue of ['""', 'null', 'undefined']) {
  846. const src = await w.executeJavaScript(`webview.src = ${emptyValue}, webview.src`);
  847. expect(src).to.equal('');
  848. }
  849. });
  850. it('does not wait until loadURL is resolved', async () => {
  851. await loadWebView(w, { src: 'about:blank' });
  852. const delay = await w.executeJavaScript(`new Promise(resolve => {
  853. const before = Date.now();
  854. webview.src = 'file://${fixtures}/pages/blank.html';
  855. const now = Date.now();
  856. resolve(now - before);
  857. })`);
  858. // Setting src is essentially sending a sync IPC message, which should
  859. // not exceed more than a few ms.
  860. //
  861. // This is for testing #18638.
  862. expect(delay).to.be.below(100);
  863. });
  864. });
  865. describe('nodeintegration attribute', () => {
  866. it('inserts no node symbols when not set', async () => {
  867. const message = await loadWebViewAndWaitForMessage(w, {
  868. src: `file://${fixtures}/pages/c.html`
  869. });
  870. const types = JSON.parse(message);
  871. expect(types).to.include({
  872. require: 'undefined',
  873. module: 'undefined',
  874. process: 'undefined',
  875. global: 'undefined'
  876. });
  877. });
  878. it('inserts node symbols when set', async () => {
  879. const message = await loadWebViewAndWaitForMessage(w, {
  880. nodeintegration: 'on',
  881. webpreferences: 'contextIsolation=no',
  882. src: `file://${fixtures}/pages/d.html`
  883. });
  884. const types = JSON.parse(message);
  885. expect(types).to.include({
  886. require: 'function',
  887. module: 'object',
  888. process: 'object'
  889. });
  890. });
  891. it('loads node symbols after POST navigation when set', async function () {
  892. const message = await loadWebViewAndWaitForMessage(w, {
  893. nodeintegration: 'on',
  894. webpreferences: 'contextIsolation=no',
  895. src: `file://${fixtures}/pages/post.html`
  896. });
  897. const types = JSON.parse(message);
  898. expect(types).to.include({
  899. require: 'function',
  900. module: 'object',
  901. process: 'object'
  902. });
  903. });
  904. it('disables node integration on child windows when it is disabled on the webview', async () => {
  905. const src = url.format({
  906. pathname: `${fixtures}/pages/webview-opener-no-node-integration.html`,
  907. protocol: 'file',
  908. query: {
  909. p: `${fixtures}/pages/window-opener-node.html`
  910. },
  911. slashes: true
  912. });
  913. const message = await loadWebViewAndWaitForMessage(w, {
  914. allowpopups: 'on',
  915. webpreferences: 'contextIsolation=no',
  916. src
  917. });
  918. expect(JSON.parse(message).isProcessGlobalUndefined).to.be.true();
  919. });
  920. ifit(!process.env.ELECTRON_SKIP_NATIVE_MODULE_TESTS)('loads native modules when navigation happens', async function () {
  921. await loadWebView(w, {
  922. nodeintegration: 'on',
  923. webpreferences: 'contextIsolation=no',
  924. src: `file://${fixtures}/pages/native-module.html`
  925. });
  926. const message = await w.executeJavaScript(`new Promise(resolve => {
  927. webview.addEventListener('console-message', e => resolve(e.message))
  928. webview.reload();
  929. })`);
  930. expect(message).to.equal('function');
  931. });
  932. });
  933. describe('preload attribute', () => {
  934. useRemoteContext({ webPreferences: { webviewTag: true } });
  935. it('loads the script before other scripts in window', async () => {
  936. const message = await loadWebViewAndWaitForMessage(w, {
  937. preload: `${fixtures}/module/preload.js`,
  938. src: `file://${fixtures}/pages/e.html`
  939. });
  940. expect(message).to.be.a('string');
  941. expect(message).to.be.not.equal('Window script is loaded before preload script');
  942. });
  943. it('preload script can still use "process" and "Buffer" when nodeintegration is off', async () => {
  944. const message = await loadWebViewAndWaitForMessage(w, {
  945. preload: `${fixtures}/module/preload-node-off.js`,
  946. src: `file://${fixtures}/api/blank.html`
  947. });
  948. const types = JSON.parse(message);
  949. expect(types).to.include({
  950. process: 'object',
  951. Buffer: 'function'
  952. });
  953. });
  954. it('runs in the correct scope when sandboxed', async () => {
  955. const message = await loadWebViewAndWaitForMessage(w, {
  956. preload: `${fixtures}/module/preload-context.js`,
  957. src: `file://${fixtures}/api/blank.html`,
  958. webpreferences: 'sandbox=yes'
  959. });
  960. const types = JSON.parse(message);
  961. expect(types).to.include({
  962. require: 'function', // arguments passed to it should be available
  963. electron: 'undefined', // objects from the scope it is called from should not be available
  964. window: 'object', // the window object should be available
  965. localVar: 'undefined' // but local variables should not be exposed to the window
  966. });
  967. });
  968. it('preload script can require modules that still use "process" and "Buffer" when nodeintegration is off', async () => {
  969. const message = await loadWebViewAndWaitForMessage(w, {
  970. preload: `${fixtures}/module/preload-node-off-wrapper.js`,
  971. webpreferences: 'sandbox=no',
  972. src: `file://${fixtures}/api/blank.html`
  973. });
  974. const types = JSON.parse(message);
  975. expect(types).to.include({
  976. process: 'object',
  977. Buffer: 'function'
  978. });
  979. });
  980. it('receives ipc message in preload script', async () => {
  981. await loadWebView(w, {
  982. preload: `${fixtures}/module/preload-ipc.js`,
  983. src: `file://${fixtures}/pages/e.html`
  984. });
  985. const message = 'boom!';
  986. const { channel, args } = await w.executeJavaScript(`new Promise(resolve => {
  987. webview.send('ping', ${JSON.stringify(message)})
  988. webview.addEventListener('ipc-message', ({channel, args}) => resolve({channel, args}))
  989. })`);
  990. expect(channel).to.equal('pong');
  991. expect(args).to.deep.equal([message]);
  992. });
  993. itremote('<webview>.sendToFrame()', async (fixtures: string) => {
  994. const w = new WebView();
  995. w.setAttribute('nodeintegration', 'on');
  996. w.setAttribute('webpreferences', 'contextIsolation=no');
  997. w.setAttribute('preload', `file://${fixtures}/module/preload-ipc.js`);
  998. w.setAttribute('src', `file://${fixtures}/pages/ipc-message.html`);
  999. document.body.appendChild(w);
  1000. const { frameId } = await new Promise<any>(resolve => w.addEventListener('ipc-message', resolve, { once: true }));
  1001. const message = 'boom!';
  1002. w.sendToFrame(frameId, 'ping', message);
  1003. const { channel, args } = await new Promise<any>(resolve => w.addEventListener('ipc-message', resolve, { once: true }));
  1004. expect(channel).to.equal('pong');
  1005. expect(args).to.deep.equal([message]);
  1006. }, [fixtures]);
  1007. it('works without script tag in page', async () => {
  1008. const message = await loadWebViewAndWaitForMessage(w, {
  1009. preload: `${fixtures}/module/preload.js`,
  1010. webpreferences: 'sandbox=no',
  1011. src: `file://${fixtures}/pages/base-page.html`
  1012. });
  1013. const types = JSON.parse(message);
  1014. expect(types).to.include({
  1015. require: 'function',
  1016. module: 'object',
  1017. process: 'object',
  1018. Buffer: 'function'
  1019. });
  1020. });
  1021. it('resolves relative URLs', async () => {
  1022. const message = await loadWebViewAndWaitForMessage(w, {
  1023. preload: '../module/preload.js',
  1024. webpreferences: 'sandbox=no',
  1025. src: `file://${fixtures}/pages/e.html`
  1026. });
  1027. const types = JSON.parse(message);
  1028. expect(types).to.include({
  1029. require: 'function',
  1030. module: 'object',
  1031. process: 'object',
  1032. Buffer: 'function'
  1033. });
  1034. });
  1035. itremote('ignores empty values', async () => {
  1036. const webview = new WebView();
  1037. for (const emptyValue of ['', null, undefined]) {
  1038. webview.preload = emptyValue;
  1039. expect(webview.preload).to.equal('');
  1040. }
  1041. });
  1042. });
  1043. describe('httpreferrer attribute', () => {
  1044. it('sets the referrer url', async () => {
  1045. const referrer = 'http://github.com/';
  1046. const received = await new Promise<string | undefined>((resolve, reject) => {
  1047. const server = http.createServer((req, res) => {
  1048. try {
  1049. resolve(req.headers.referer);
  1050. } catch (e) {
  1051. reject(e);
  1052. } finally {
  1053. res.end();
  1054. server.close();
  1055. }
  1056. });
  1057. listen(server).then(({ url }) => {
  1058. loadWebView(w, {
  1059. httpreferrer: referrer,
  1060. src: url
  1061. });
  1062. });
  1063. });
  1064. expect(received).to.equal(referrer);
  1065. });
  1066. });
  1067. describe('useragent attribute', () => {
  1068. it('sets the user agent', async () => {
  1069. const referrer = 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko';
  1070. const message = await loadWebViewAndWaitForMessage(w, {
  1071. src: `file://${fixtures}/pages/useragent.html`,
  1072. useragent: referrer
  1073. });
  1074. expect(message).to.equal(referrer);
  1075. });
  1076. });
  1077. describe('disablewebsecurity attribute', () => {
  1078. it('does not disable web security when not set', async () => {
  1079. await loadWebView(w, { src: 'about:blank' });
  1080. const result = await w.executeJavaScript(`webview.executeJavaScript(\`fetch(${JSON.stringify(blankPageUrl)}).then(() => 'ok', () => 'failed')\`)`);
  1081. expect(result).to.equal('failed');
  1082. });
  1083. it('disables web security when set', async () => {
  1084. await loadWebView(w, { src: 'about:blank', disablewebsecurity: '' });
  1085. const result = await w.executeJavaScript(`webview.executeJavaScript(\`fetch(${JSON.stringify(blankPageUrl)}).then(() => 'ok', () => 'failed')\`)`);
  1086. expect(result).to.equal('ok');
  1087. });
  1088. it('does not break node integration', async () => {
  1089. const message = await loadWebViewAndWaitForMessage(w, {
  1090. disablewebsecurity: '',
  1091. nodeintegration: 'on',
  1092. webpreferences: 'contextIsolation=no',
  1093. src: `file://${fixtures}/pages/d.html`
  1094. });
  1095. const types = JSON.parse(message);
  1096. expect(types).to.include({
  1097. require: 'function',
  1098. module: 'object',
  1099. process: 'object'
  1100. });
  1101. });
  1102. it('does not break preload script', async () => {
  1103. const message = await loadWebViewAndWaitForMessage(w, {
  1104. disablewebsecurity: '',
  1105. preload: `${fixtures}/module/preload.js`,
  1106. webpreferences: 'sandbox=no',
  1107. src: `file://${fixtures}/pages/e.html`
  1108. });
  1109. const types = JSON.parse(message);
  1110. expect(types).to.include({
  1111. require: 'function',
  1112. module: 'object',
  1113. process: 'object',
  1114. Buffer: 'function'
  1115. });
  1116. });
  1117. });
  1118. describe('partition attribute', () => {
  1119. it('inserts no node symbols when not set', async () => {
  1120. const message = await loadWebViewAndWaitForMessage(w, {
  1121. partition: 'test1',
  1122. src: `file://${fixtures}/pages/c.html`
  1123. });
  1124. const types = JSON.parse(message);
  1125. expect(types).to.include({
  1126. require: 'undefined',
  1127. module: 'undefined',
  1128. process: 'undefined',
  1129. global: 'undefined'
  1130. });
  1131. });
  1132. it('inserts node symbols when set', async () => {
  1133. const message = await loadWebViewAndWaitForMessage(w, {
  1134. nodeintegration: 'on',
  1135. partition: 'test2',
  1136. webpreferences: 'contextIsolation=no',
  1137. src: `file://${fixtures}/pages/d.html`
  1138. });
  1139. const types = JSON.parse(message);
  1140. expect(types).to.include({
  1141. require: 'function',
  1142. module: 'object',
  1143. process: 'object'
  1144. });
  1145. });
  1146. it('isolates storage for different id', async () => {
  1147. await w.executeJavaScript('localStorage.setItem(\'test\', \'one\')');
  1148. const message = await loadWebViewAndWaitForMessage(w, {
  1149. partition: 'test3',
  1150. src: `file://${fixtures}/pages/partition/one.html`
  1151. });
  1152. const parsedMessage = JSON.parse(message);
  1153. expect(parsedMessage).to.include({
  1154. numberOfEntries: 0,
  1155. testValue: null
  1156. });
  1157. });
  1158. it('uses current session storage when no id is provided', async () => {
  1159. await w.executeJavaScript('localStorage.setItem(\'test\', \'two\')');
  1160. const testValue = 'two';
  1161. const message = await loadWebViewAndWaitForMessage(w, {
  1162. src: `file://${fixtures}/pages/partition/one.html`
  1163. });
  1164. const parsedMessage = JSON.parse(message);
  1165. expect(parsedMessage).to.include({
  1166. testValue
  1167. });
  1168. });
  1169. });
  1170. describe('allowpopups attribute', () => {
  1171. const generateSpecs = (description: string, webpreferences = '') => {
  1172. describe(description, () => {
  1173. it('can not open new window when not set', async () => {
  1174. const message = await loadWebViewAndWaitForMessage(w, {
  1175. webpreferences,
  1176. src: `file://${fixtures}/pages/window-open-hide.html`
  1177. });
  1178. expect(message).to.equal('null');
  1179. });
  1180. it('can open new window when set', async () => {
  1181. const message = await loadWebViewAndWaitForMessage(w, {
  1182. webpreferences,
  1183. allowpopups: 'on',
  1184. src: `file://${fixtures}/pages/window-open-hide.html`
  1185. });
  1186. expect(message).to.equal('window');
  1187. });
  1188. });
  1189. };
  1190. generateSpecs('without sandbox');
  1191. generateSpecs('with sandbox', 'sandbox=yes');
  1192. });
  1193. describe('webpreferences attribute', () => {
  1194. it('can enable nodeintegration', async () => {
  1195. const message = await loadWebViewAndWaitForMessage(w, {
  1196. src: `file://${fixtures}/pages/d.html`,
  1197. webpreferences: 'nodeIntegration,contextIsolation=no'
  1198. });
  1199. const types = JSON.parse(message);
  1200. expect(types).to.include({
  1201. require: 'function',
  1202. module: 'object',
  1203. process: 'object'
  1204. });
  1205. });
  1206. it('can disable web security and enable nodeintegration', async () => {
  1207. await loadWebView(w, { src: 'about:blank', webpreferences: 'webSecurity=no, nodeIntegration=yes, contextIsolation=no' });
  1208. const result = await w.executeJavaScript(`webview.executeJavaScript(\`fetch(${JSON.stringify(blankPageUrl)}).then(() => 'ok', () => 'failed')\`)`);
  1209. expect(result).to.equal('ok');
  1210. const type = await w.executeJavaScript('webview.executeJavaScript("typeof require")');
  1211. expect(type).to.equal('function');
  1212. });
  1213. });
  1214. });
  1215. describe('events', () => {
  1216. useRemoteContext({ webPreferences: { webviewTag: true } });
  1217. let w: WebContents;
  1218. before(async () => {
  1219. const window = new BrowserWindow({
  1220. show: false,
  1221. webPreferences: {
  1222. webviewTag: true,
  1223. nodeIntegration: true,
  1224. contextIsolation: false
  1225. }
  1226. });
  1227. await window.loadURL(`file://${fixtures}/pages/blank.html`);
  1228. w = window.webContents;
  1229. });
  1230. afterEach(async () => {
  1231. await w.executeJavaScript(`{
  1232. document.querySelectorAll('webview').forEach(el => el.remove())
  1233. }`);
  1234. });
  1235. after(closeAllWindows);
  1236. describe('ipc-message event', () => {
  1237. it('emits when guest sends an ipc message to browser', async () => {
  1238. const { frameId, channel, args } = await loadWebViewAndWaitForEvent(w, {
  1239. src: `file://${fixtures}/pages/ipc-message.html`,
  1240. nodeintegration: 'on',
  1241. webpreferences: 'contextIsolation=no'
  1242. }, 'ipc-message');
  1243. expect(frameId).to.be.an('array').that.has.lengthOf(2);
  1244. expect(channel).to.equal('channel');
  1245. expect(args).to.deep.equal(['arg1', 'arg2']);
  1246. });
  1247. });
  1248. describe('page-title-updated event', () => {
  1249. it('emits when title is set', async () => {
  1250. const { title, explicitSet } = await loadWebViewAndWaitForEvent(w, {
  1251. src: `file://${fixtures}/pages/a.html`
  1252. }, 'page-title-updated');
  1253. expect(title).to.equal('test');
  1254. expect(explicitSet).to.be.true();
  1255. });
  1256. });
  1257. describe('page-favicon-updated event', () => {
  1258. it('emits when favicon urls are received', async () => {
  1259. const { favicons } = await loadWebViewAndWaitForEvent(w, {
  1260. src: `file://${fixtures}/pages/a.html`
  1261. }, 'page-favicon-updated');
  1262. expect(favicons).to.be.an('array').of.length(2);
  1263. if (process.platform === 'win32') {
  1264. expect(favicons[0]).to.match(/^file:\/\/\/[A-Z]:\/favicon.png$/i);
  1265. } else {
  1266. expect(favicons[0]).to.equal('file:///favicon.png');
  1267. }
  1268. });
  1269. });
  1270. describe('did-redirect-navigation event', () => {
  1271. it('is emitted on redirects', async () => {
  1272. const server = http.createServer((req, res) => {
  1273. if (req.url === '/302') {
  1274. res.setHeader('Location', '/200');
  1275. res.statusCode = 302;
  1276. res.end();
  1277. } else {
  1278. res.end();
  1279. }
  1280. });
  1281. const { url } = await listen(server);
  1282. defer(() => { server.close(); });
  1283. const event = await loadWebViewAndWaitForEvent(w, {
  1284. src: `${url}/302`
  1285. }, 'did-redirect-navigation');
  1286. expect(event.url).to.equal(`${url}/200`);
  1287. expect(event.isInPlace).to.be.false();
  1288. expect(event.isMainFrame).to.be.true();
  1289. expect(event.frameProcessId).to.be.a('number');
  1290. expect(event.frameRoutingId).to.be.a('number');
  1291. });
  1292. });
  1293. describe('will-navigate event', () => {
  1294. it('emits when a url that leads to outside of the page is loaded', async () => {
  1295. const { url } = await loadWebViewAndWaitForEvent(w, {
  1296. src: `file://${fixtures}/pages/webview-will-navigate.html`
  1297. }, 'will-navigate');
  1298. expect(url).to.equal('http://host/');
  1299. });
  1300. });
  1301. describe('will-frame-navigate event', () => {
  1302. it('emits when a link that leads to outside of the page is loaded', async () => {
  1303. const { url, isMainFrame } = await loadWebViewAndWaitForEvent(w, {
  1304. src: `file://${fixtures}/pages/webview-will-navigate.html`
  1305. }, 'will-frame-navigate');
  1306. expect(url).to.equal('http://host/');
  1307. expect(isMainFrame).to.be.true();
  1308. });
  1309. it('emits when a link within an iframe, which leads to outside of the page, is loaded', async () => {
  1310. await loadWebView(w, {
  1311. src: `file://${fixtures}/pages/webview-will-navigate-in-frame.html`,
  1312. nodeIntegration: ''
  1313. });
  1314. const { url, frameProcessId, frameRoutingId } = await w.executeJavaScript(`
  1315. new Promise((resolve, reject) => {
  1316. let hasFrameNavigatedOnce = false;
  1317. const webview = document.getElementById('webview');
  1318. webview.addEventListener('will-frame-navigate', ({url, isMainFrame, frameProcessId, frameRoutingId}) => {
  1319. if (isMainFrame) return;
  1320. if (hasFrameNavigatedOnce) resolve({
  1321. url,
  1322. isMainFrame,
  1323. frameProcessId,
  1324. frameRoutingId,
  1325. });
  1326. // First navigation is the initial iframe load within the <webview>
  1327. hasFrameNavigatedOnce = true;
  1328. });
  1329. webview.executeJavaScript('loadSubframe()');
  1330. });
  1331. `);
  1332. expect(url).to.equal('http://host/');
  1333. expect(frameProcessId).to.be.a('number');
  1334. expect(frameRoutingId).to.be.a('number');
  1335. });
  1336. });
  1337. describe('did-navigate event', () => {
  1338. it('emits when a url that leads to outside of the page is clicked', async () => {
  1339. const pageUrl = url.pathToFileURL(path.join(fixtures, 'pages', 'webview-will-navigate.html')).toString();
  1340. const event = await loadWebViewAndWaitForEvent(w, { src: pageUrl }, 'did-navigate');
  1341. expect(event.url).to.equal(pageUrl);
  1342. });
  1343. });
  1344. describe('did-navigate-in-page event', () => {
  1345. it('emits when an anchor link is clicked', async () => {
  1346. const pageUrl = url.pathToFileURL(path.join(fixtures, 'pages', 'webview-did-navigate-in-page.html')).toString();
  1347. const event = await loadWebViewAndWaitForEvent(w, { src: pageUrl }, 'did-navigate-in-page');
  1348. expect(event.url).to.equal(`${pageUrl}#test_content`);
  1349. });
  1350. it('emits when window.history.replaceState is called', async () => {
  1351. const { url } = await loadWebViewAndWaitForEvent(w, {
  1352. src: `file://${fixtures}/pages/webview-did-navigate-in-page-with-history.html`
  1353. }, 'did-navigate-in-page');
  1354. expect(url).to.equal('http://host/');
  1355. });
  1356. it('emits when window.location.hash is changed', async () => {
  1357. const pageUrl = url.pathToFileURL(path.join(fixtures, 'pages', 'webview-did-navigate-in-page-with-hash.html')).toString();
  1358. const event = await loadWebViewAndWaitForEvent(w, { src: pageUrl }, 'did-navigate-in-page');
  1359. expect(event.url).to.equal(`${pageUrl}#test`);
  1360. });
  1361. });
  1362. describe('close event', () => {
  1363. it('should fire when interior page calls window.close', async () => {
  1364. await loadWebViewAndWaitForEvent(w, { src: `file://${fixtures}/pages/close.html` }, 'close');
  1365. });
  1366. });
  1367. describe('devtools-opened event', () => {
  1368. it('should fire when webview.openDevTools() is called', async () => {
  1369. await loadWebViewAndWaitForEvent(w, {
  1370. src: `file://${fixtures}/pages/base-page.html`
  1371. }, 'dom-ready');
  1372. await w.executeJavaScript(`new Promise((resolve) => {
  1373. webview.openDevTools()
  1374. webview.addEventListener('devtools-opened', () => resolve(), {once: true})
  1375. })`);
  1376. await w.executeJavaScript('webview.closeDevTools()');
  1377. });
  1378. });
  1379. describe('devtools-closed event', () => {
  1380. itremote('should fire when webview.closeDevTools() is called', async (fixtures: string) => {
  1381. const webview = new WebView();
  1382. webview.src = `file://${fixtures}/pages/base-page.html`;
  1383. document.body.appendChild(webview);
  1384. await new Promise(resolve => webview.addEventListener('dom-ready', resolve, { once: true }));
  1385. webview.openDevTools();
  1386. await new Promise(resolve => webview.addEventListener('devtools-opened', resolve, { once: true }));
  1387. webview.closeDevTools();
  1388. await new Promise(resolve => webview.addEventListener('devtools-closed', resolve, { once: true }));
  1389. }, [fixtures]);
  1390. });
  1391. describe('devtools-focused event', () => {
  1392. itremote('should fire when webview.openDevTools() is called', async (fixtures: string) => {
  1393. const webview = new WebView();
  1394. webview.src = `file://${fixtures}/pages/base-page.html`;
  1395. document.body.appendChild(webview);
  1396. const waitForDevToolsFocused = new Promise(resolve => webview.addEventListener('devtools-focused', resolve, { once: true }));
  1397. await new Promise(resolve => webview.addEventListener('dom-ready', resolve, { once: true }));
  1398. webview.openDevTools();
  1399. await waitForDevToolsFocused;
  1400. webview.closeDevTools();
  1401. }, [fixtures]);
  1402. });
  1403. describe('dom-ready event', () => {
  1404. it('emits when document is loaded', async () => {
  1405. const server = http.createServer(() => {});
  1406. const { port } = await listen(server);
  1407. await loadWebViewAndWaitForEvent(w, {
  1408. src: `file://${fixtures}/pages/dom-ready.html?port=${port}`
  1409. }, 'dom-ready');
  1410. });
  1411. itremote('throws a custom error when an API method is called before the event is emitted', () => {
  1412. const expectedErrorMessage =
  1413. 'The WebView must be attached to the DOM ' +
  1414. 'and the dom-ready event emitted before this method can be called.';
  1415. const webview = new WebView();
  1416. expect(() => { webview.stop(); }).to.throw(expectedErrorMessage);
  1417. });
  1418. });
  1419. describe('context-menu event', () => {
  1420. it('emits when right-clicked in page', async () => {
  1421. await loadWebView(w, { src: 'about:blank' });
  1422. const { params, url } = await w.executeJavaScript(`new Promise(resolve => {
  1423. webview.addEventListener('context-menu', (e) => resolve({...e, url: webview.getURL() }), {once: true})
  1424. // Simulate right-click to create context-menu event.
  1425. const opts = { x: 0, y: 0, button: 'right' };
  1426. webview.sendInputEvent({ ...opts, type: 'mouseDown' });
  1427. webview.sendInputEvent({ ...opts, type: 'mouseUp' });
  1428. })`);
  1429. expect(params.pageURL).to.equal(url);
  1430. expect(params.frame).to.be.undefined();
  1431. expect(params.x).to.be.a('number');
  1432. expect(params.y).to.be.a('number');
  1433. });
  1434. });
  1435. describe('found-in-page event', () => {
  1436. itremote('emits when a request is made', async (fixtures: string) => {
  1437. const webview = new WebView();
  1438. const didFinishLoad = new Promise(resolve => webview.addEventListener('did-finish-load', resolve, { once: true }));
  1439. webview.src = `file://${fixtures}/pages/content.html`;
  1440. document.body.appendChild(webview);
  1441. // TODO(deepak1556): With https://codereview.chromium.org/2836973002
  1442. // focus of the webContents is required when triggering the api.
  1443. // Remove this workaround after determining the cause for
  1444. // incorrect focus.
  1445. webview.focus();
  1446. await didFinishLoad;
  1447. const activeMatchOrdinal = [];
  1448. for (;;) {
  1449. const foundInPage = new Promise<any>(resolve => webview.addEventListener('found-in-page', resolve, { once: true }));
  1450. const requestId = webview.findInPage('virtual');
  1451. const event = await foundInPage;
  1452. expect(event.result.requestId).to.equal(requestId);
  1453. expect(event.result.matches).to.equal(3);
  1454. activeMatchOrdinal.push(event.result.activeMatchOrdinal);
  1455. if (event.result.activeMatchOrdinal === event.result.matches) {
  1456. break;
  1457. }
  1458. }
  1459. expect(activeMatchOrdinal).to.deep.equal([1, 2, 3]);
  1460. webview.stopFindInPage('clearSelection');
  1461. }, [fixtures]);
  1462. });
  1463. describe('will-attach-webview event', () => {
  1464. itremote('does not emit when src is not changed', async () => {
  1465. const webview = new WebView();
  1466. document.body.appendChild(webview);
  1467. await setTimeout();
  1468. const expectedErrorMessage = 'The WebView must be attached to the DOM and the dom-ready event emitted before this method can be called.';
  1469. expect(() => { webview.stop(); }).to.throw(expectedErrorMessage);
  1470. });
  1471. it('supports changing the web preferences', async () => {
  1472. w.once('will-attach-webview', (event, webPreferences, params) => {
  1473. params.src = `file://${path.join(fixtures, 'pages', 'c.html')}`;
  1474. webPreferences.nodeIntegration = false;
  1475. });
  1476. const message = await loadWebViewAndWaitForMessage(w, {
  1477. nodeintegration: 'yes',
  1478. src: `file://${fixtures}/pages/a.html`
  1479. });
  1480. const types = JSON.parse(message);
  1481. expect(types).to.include({
  1482. require: 'undefined',
  1483. module: 'undefined',
  1484. process: 'undefined',
  1485. global: 'undefined'
  1486. });
  1487. });
  1488. it('handler modifying params.instanceId does not break <webview>', async () => {
  1489. w.once('will-attach-webview', (event, webPreferences, params) => {
  1490. params.instanceId = null as any;
  1491. });
  1492. await loadWebViewAndWaitForMessage(w, {
  1493. src: `file://${fixtures}/pages/a.html`
  1494. });
  1495. });
  1496. it('supports preventing a webview from being created', async () => {
  1497. w.once('will-attach-webview', event => event.preventDefault());
  1498. await loadWebViewAndWaitForEvent(w, {
  1499. src: `file://${fixtures}/pages/c.html`
  1500. }, 'destroyed');
  1501. });
  1502. it('supports removing the preload script', async () => {
  1503. w.once('will-attach-webview', (event, webPreferences, params) => {
  1504. params.src = url.pathToFileURL(path.join(fixtures, 'pages', 'webview-stripped-preload.html')).toString();
  1505. delete webPreferences.preload;
  1506. });
  1507. const message = await loadWebViewAndWaitForMessage(w, {
  1508. nodeintegration: 'yes',
  1509. preload: path.join(fixtures, 'module', 'preload-set-global.js'),
  1510. src: `file://${fixtures}/pages/a.html`
  1511. });
  1512. expect(message).to.equal('undefined');
  1513. });
  1514. });
  1515. describe('media-started-playing and media-paused events', () => {
  1516. it('emits when audio starts and stops playing', async function () {
  1517. if (!await w.executeJavaScript('document.createElement(\'audio\').canPlayType(\'audio/wav\')')) {
  1518. return this.skip();
  1519. }
  1520. await loadWebView(w, { src: blankPageUrl });
  1521. // With the new autoplay policy, audio elements must be unmuted
  1522. // see https://goo.gl/xX8pDD.
  1523. await w.executeJavaScript(`new Promise(resolve => {
  1524. webview.executeJavaScript(\`
  1525. const audio = document.createElement("audio")
  1526. audio.src = "../assets/tone.wav"
  1527. document.body.appendChild(audio);
  1528. audio.play()
  1529. \`, true)
  1530. webview.addEventListener('media-started-playing', () => resolve(), {once: true})
  1531. })`);
  1532. await w.executeJavaScript(`new Promise(resolve => {
  1533. webview.executeJavaScript(\`
  1534. document.querySelector("audio").pause()
  1535. \`, true)
  1536. webview.addEventListener('media-paused', () => resolve(), {once: true})
  1537. })`);
  1538. });
  1539. });
  1540. });
  1541. describe('methods', () => {
  1542. let w: WebContents;
  1543. before(async () => {
  1544. const window = new BrowserWindow({
  1545. show: false,
  1546. webPreferences: {
  1547. webviewTag: true,
  1548. nodeIntegration: true,
  1549. contextIsolation: false
  1550. }
  1551. });
  1552. await window.loadURL(`file://${fixtures}/pages/blank.html`);
  1553. w = window.webContents;
  1554. });
  1555. afterEach(async () => {
  1556. await w.executeJavaScript(`{
  1557. document.querySelectorAll('webview').forEach(el => el.remove())
  1558. }`);
  1559. });
  1560. after(closeAllWindows);
  1561. describe('<webview>.reload()', () => {
  1562. it('should emit beforeunload handler', async () => {
  1563. await loadWebView(w, {
  1564. nodeintegration: 'on',
  1565. webpreferences: 'contextIsolation=no',
  1566. src: `file://${fixtures}/pages/beforeunload-false.html`
  1567. });
  1568. // Event handler has to be added before reload.
  1569. const channel = await w.executeJavaScript(`new Promise(resolve => {
  1570. webview.addEventListener('ipc-message', e => resolve(e.channel))
  1571. webview.reload();
  1572. })`);
  1573. expect(channel).to.equal('onbeforeunload');
  1574. });
  1575. });
  1576. describe('<webview>.goForward()', () => {
  1577. useRemoteContext({ webPreferences: { webviewTag: true } });
  1578. itremote('should work after a replaced history entry', async (fixtures: string) => {
  1579. function waitForEvent (target: EventTarget, event: string) {
  1580. return new Promise<any>(resolve => target.addEventListener(event, resolve, { once: true }));
  1581. }
  1582. function waitForEvents (target: EventTarget, ...events: string[]) {
  1583. return Promise.all(events.map(event => waitForEvent(webview, event)));
  1584. }
  1585. const webview = new WebView();
  1586. webview.setAttribute('nodeintegration', 'on');
  1587. webview.setAttribute('webpreferences', 'contextIsolation=no');
  1588. webview.src = `file://${fixtures}/pages/history-replace.html`;
  1589. document.body.appendChild(webview);
  1590. {
  1591. const [e] = await waitForEvents(webview, 'ipc-message', 'did-stop-loading');
  1592. expect(e.channel).to.equal('history');
  1593. expect(e.args[0]).to.equal(1);
  1594. expect(webview.canGoBack()).to.be.false();
  1595. expect(webview.canGoForward()).to.be.false();
  1596. }
  1597. webview.src = `file://${fixtures}/pages/base-page.html`;
  1598. await new Promise<void>(resolve => webview.addEventListener('did-stop-loading', resolve, { once: true }));
  1599. expect(webview.canGoBack()).to.be.true();
  1600. expect(webview.canGoForward()).to.be.false();
  1601. webview.goBack();
  1602. {
  1603. const [e] = await waitForEvents(webview, 'ipc-message', 'did-stop-loading');
  1604. expect(e.channel).to.equal('history');
  1605. expect(e.args[0]).to.equal(2);
  1606. expect(webview.canGoBack()).to.be.false();
  1607. expect(webview.canGoForward()).to.be.true();
  1608. }
  1609. webview.goForward();
  1610. await new Promise<void>(resolve => webview.addEventListener('did-stop-loading', resolve, { once: true }));
  1611. expect(webview.canGoBack()).to.be.true();
  1612. expect(webview.canGoForward()).to.be.false();
  1613. }, [fixtures]);
  1614. });
  1615. describe('<webview>.clearHistory()', () => {
  1616. it('should clear the navigation history', async () => {
  1617. await loadWebView(w, {
  1618. nodeintegration: 'on',
  1619. webpreferences: 'contextIsolation=no',
  1620. src: blankPageUrl
  1621. });
  1622. // Navigation must be triggered by a user gesture to make canGoBack() return true
  1623. await w.executeJavaScript('webview.executeJavaScript(`history.pushState(null, "", "foo.html")`, true)');
  1624. expect(await w.executeJavaScript('webview.canGoBack()')).to.be.true();
  1625. await w.executeJavaScript('webview.clearHistory()');
  1626. expect(await w.executeJavaScript('webview.canGoBack()')).to.be.false();
  1627. });
  1628. });
  1629. describe('executeJavaScript', () => {
  1630. it('can return the result of the executed script', async () => {
  1631. await loadWebView(w, {
  1632. src: 'about:blank'
  1633. });
  1634. const jsScript = "'4'+2";
  1635. const expectedResult = '42';
  1636. const result = await w.executeJavaScript(`webview.executeJavaScript(${JSON.stringify(jsScript)})`);
  1637. expect(result).to.equal(expectedResult);
  1638. });
  1639. });
  1640. it('supports inserting CSS', async () => {
  1641. await loadWebView(w, { src: `file://${fixtures}/pages/base-page.html` });
  1642. await w.executeJavaScript('webview.insertCSS(\'body { background-repeat: round; }\')');
  1643. const result = await w.executeJavaScript('webview.executeJavaScript(\'window.getComputedStyle(document.body).getPropertyValue("background-repeat")\')');
  1644. expect(result).to.equal('round');
  1645. });
  1646. it('supports removing inserted CSS', async () => {
  1647. await loadWebView(w, { src: `file://${fixtures}/pages/base-page.html` });
  1648. const key = await w.executeJavaScript('webview.insertCSS(\'body { background-repeat: round; }\')');
  1649. await w.executeJavaScript(`webview.removeInsertedCSS(${JSON.stringify(key)})`);
  1650. const result = await w.executeJavaScript('webview.executeJavaScript(\'window.getComputedStyle(document.body).getPropertyValue("background-repeat")\')');
  1651. expect(result).to.equal('repeat');
  1652. });
  1653. describe('sendInputEvent', () => {
  1654. it('can send keyboard event', async () => {
  1655. await loadWebViewAndWaitForEvent(w, {
  1656. nodeintegration: 'on',
  1657. webpreferences: 'contextIsolation=no',
  1658. src: `file://${fixtures}/pages/onkeyup.html`
  1659. }, 'dom-ready');
  1660. const waitForIpcMessage = w.executeJavaScript('new Promise(resolve => webview.addEventListener("ipc-message", e => resolve({...e})), {once: true})');
  1661. w.executeJavaScript(`webview.sendInputEvent({
  1662. type: 'keyup',
  1663. keyCode: 'c',
  1664. modifiers: ['shift']
  1665. })`);
  1666. const { channel, args } = await waitForIpcMessage;
  1667. expect(channel).to.equal('keyup');
  1668. expect(args).to.deep.equal(['C', 'KeyC', 67, true, false]);
  1669. });
  1670. it('can send mouse event', async () => {
  1671. await loadWebViewAndWaitForEvent(w, {
  1672. nodeintegration: 'on',
  1673. webpreferences: 'contextIsolation=no',
  1674. src: `file://${fixtures}/pages/onmouseup.html`
  1675. }, 'dom-ready');
  1676. const waitForIpcMessage = w.executeJavaScript('new Promise(resolve => webview.addEventListener("ipc-message", e => resolve({...e})), {once: true})');
  1677. w.executeJavaScript(`webview.sendInputEvent({
  1678. type: 'mouseup',
  1679. modifiers: ['ctrl'],
  1680. x: 10,
  1681. y: 20
  1682. })`);
  1683. const { channel, args } = await waitForIpcMessage;
  1684. expect(channel).to.equal('mouseup');
  1685. expect(args).to.deep.equal([10, 20, false, true]);
  1686. });
  1687. });
  1688. describe('<webview>.getWebContentsId', () => {
  1689. it('can return the WebContents ID', async () => {
  1690. await loadWebView(w, { src: 'about:blank' });
  1691. expect(await w.executeJavaScript('webview.getWebContentsId()')).to.be.a('number');
  1692. });
  1693. });
  1694. ifdescribe(features.isPrintingEnabled())('<webview>.printToPDF()', () => {
  1695. it('rejects on incorrectly typed parameters', async () => {
  1696. const badTypes = {
  1697. landscape: [],
  1698. displayHeaderFooter: '123',
  1699. printBackground: 2,
  1700. scale: 'not-a-number',
  1701. pageSize: 'IAmAPageSize',
  1702. margins: 'terrible',
  1703. pageRanges: { oops: 'im-not-the-right-key' },
  1704. headerTemplate: [1, 2, 3],
  1705. footerTemplate: [4, 5, 6],
  1706. preferCSSPageSize: 'no'
  1707. };
  1708. // These will hard crash in Chromium unless we type-check
  1709. for (const [key, value] of Object.entries(badTypes)) {
  1710. const param = { [key]: value };
  1711. const src = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E';
  1712. await loadWebView(w, { src });
  1713. await expect(w.executeJavaScript(`webview.printToPDF(${JSON.stringify(param)})`)).to.eventually.be.rejected();
  1714. }
  1715. });
  1716. it('can print to PDF', async () => {
  1717. const src = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E';
  1718. await loadWebView(w, { src });
  1719. const data = await w.executeJavaScript('webview.printToPDF({})');
  1720. expect(data).to.be.an.instanceof(Uint8Array).that.is.not.empty();
  1721. });
  1722. });
  1723. describe('DOM events', () => {
  1724. for (const [description, sandbox] of [
  1725. ['without sandbox', false] as const,
  1726. ['with sandbox', true] as const
  1727. ]) {
  1728. describe(description, () => {
  1729. it('emits focus event', async () => {
  1730. await loadWebViewAndWaitForEvent(w, {
  1731. src: `file://${fixtures}/pages/a.html`,
  1732. webpreferences: `sandbox=${sandbox ? 'yes' : 'no'}`
  1733. }, 'dom-ready');
  1734. // If this test fails, check if webview.focus() still works.
  1735. await w.executeJavaScript(`new Promise(resolve => {
  1736. webview.addEventListener('focus', () => resolve(), {once: true});
  1737. webview.focus();
  1738. })`);
  1739. });
  1740. });
  1741. }
  1742. });
  1743. // TODO(miniak): figure out why this is failing on windows
  1744. ifdescribe(process.platform !== 'win32')('<webview>.capturePage()', () => {
  1745. it('returns a Promise with a NativeImage', async () => {
  1746. const src = 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E';
  1747. await loadWebViewAndWaitForEvent(w, { src }, 'did-stop-loading');
  1748. // Retry a few times due to flake.
  1749. for (let i = 0; i < 5; i++) {
  1750. try {
  1751. const image = await w.executeJavaScript('webview.capturePage()');
  1752. const imgBuffer = image.toPNG();
  1753. // Check the 25th byte in the PNG.
  1754. // Values can be 0,2,3,4, or 6. We want 6, which is RGB + Alpha
  1755. expect(imgBuffer[25]).to.equal(6);
  1756. return;
  1757. } catch (e) {
  1758. /* drop the error */
  1759. }
  1760. }
  1761. expect(false).to.be.true('could not successfully capture the page');
  1762. });
  1763. });
  1764. // FIXME(zcbenz): Disabled because of moving to OOPIF webview.
  1765. xdescribe('setDevToolsWebContents() API', () => {
  1766. /*
  1767. it('sets webContents of webview as devtools', async () => {
  1768. const webview2 = new WebView();
  1769. loadWebView(webview2);
  1770. // Setup an event handler for further usage.
  1771. const waitForDomReady = waitForEvent(webview2, 'dom-ready');
  1772. loadWebView(webview, { src: 'about:blank' });
  1773. await waitForEvent(webview, 'dom-ready');
  1774. webview.getWebContents().setDevToolsWebContents(webview2.getWebContents());
  1775. webview.getWebContents().openDevTools();
  1776. await waitForDomReady;
  1777. // Its WebContents should be a DevTools.
  1778. const devtools = webview2.getWebContents();
  1779. expect(devtools.getURL().startsWith('devtools://devtools')).to.be.true();
  1780. const name = await devtools.executeJavaScript('InspectorFrontendHost.constructor.name');
  1781. document.body.removeChild(webview2);
  1782. expect(name).to.be.equal('InspectorFrontendHostImpl');
  1783. });
  1784. */
  1785. });
  1786. });
  1787. describe('basic auth', () => {
  1788. let w: WebContents;
  1789. before(async () => {
  1790. const window = new BrowserWindow({
  1791. show: false,
  1792. webPreferences: {
  1793. webviewTag: true,
  1794. nodeIntegration: true,
  1795. contextIsolation: false
  1796. }
  1797. });
  1798. await window.loadURL(`file://${fixtures}/pages/blank.html`);
  1799. w = window.webContents;
  1800. });
  1801. afterEach(async () => {
  1802. await w.executeJavaScript(`{
  1803. document.querySelectorAll('webview').forEach(el => el.remove())
  1804. }`);
  1805. });
  1806. after(closeAllWindows);
  1807. it('should authenticate with correct credentials', async () => {
  1808. const message = 'Authenticated';
  1809. const server = http.createServer((req, res) => {
  1810. const credentials = auth(req)!;
  1811. if (credentials.name === 'test' && credentials.pass === 'test') {
  1812. res.end(message);
  1813. } else {
  1814. res.end('failed');
  1815. }
  1816. });
  1817. defer(() => {
  1818. server.close();
  1819. });
  1820. const { port } = await listen(server);
  1821. const e = await loadWebViewAndWaitForEvent(w, {
  1822. nodeintegration: 'on',
  1823. webpreferences: 'contextIsolation=no',
  1824. src: `file://${fixtures}/pages/basic-auth.html?port=${port}`
  1825. }, 'ipc-message');
  1826. expect(e.channel).to.equal(message);
  1827. });
  1828. });
  1829. });