chromium-spec.ts 118 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019
  1. import { expect } from 'chai';
  2. import { BrowserWindow, WebContents, webFrameMain, session, ipcMain, app, protocol, webContents } from 'electron/main';
  3. import { emittedOnce } from './events-helpers';
  4. import { closeAllWindows } from './window-helpers';
  5. import * as https from 'https';
  6. import * as http from 'http';
  7. import * as path from 'path';
  8. import * as fs from 'fs';
  9. import * as url from 'url';
  10. import * as ChildProcess from 'child_process';
  11. import { EventEmitter } from 'events';
  12. import { promisify } from 'util';
  13. import { ifit, ifdescribe, defer, delay, itremote } from './spec-helpers';
  14. import { AddressInfo } from 'net';
  15. import { PipeTransport } from './pipe-transport';
  16. import * as ws from 'ws';
  17. const features = process._linkedBinding('electron_common_features');
  18. const fixturesPath = path.resolve(__dirname, 'fixtures');
  19. describe('reporting api', () => {
  20. // TODO(nornagon): this started failing a lot on CI. Figure out why and fix
  21. // it.
  22. it.skip('sends a report for a deprecation', async () => {
  23. const reports = new EventEmitter();
  24. // The Reporting API only works on https with valid certs. To dodge having
  25. // to set up a trusted certificate, hack the validator.
  26. session.defaultSession.setCertificateVerifyProc((req, cb) => {
  27. cb(0);
  28. });
  29. const certPath = path.join(fixturesPath, 'certificates');
  30. const options = {
  31. key: fs.readFileSync(path.join(certPath, 'server.key')),
  32. cert: fs.readFileSync(path.join(certPath, 'server.pem')),
  33. ca: [
  34. fs.readFileSync(path.join(certPath, 'rootCA.pem')),
  35. fs.readFileSync(path.join(certPath, 'intermediateCA.pem'))
  36. ],
  37. requestCert: true,
  38. rejectUnauthorized: false
  39. };
  40. const server = https.createServer(options, (req, res) => {
  41. if (req.url === '/report') {
  42. let data = '';
  43. req.on('data', (d) => { data += d.toString('utf-8'); });
  44. req.on('end', () => {
  45. reports.emit('report', JSON.parse(data));
  46. });
  47. }
  48. res.setHeader('Report-To', JSON.stringify({
  49. group: 'default',
  50. max_age: 120,
  51. endpoints: [{ url: `https://localhost:${(server.address() as any).port}/report` }]
  52. }));
  53. res.setHeader('Content-Type', 'text/html');
  54. // using the deprecated `webkitRequestAnimationFrame` will trigger a
  55. // "deprecation" report.
  56. res.end('<script>webkitRequestAnimationFrame(() => {})</script>');
  57. });
  58. await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
  59. const bw = new BrowserWindow({
  60. show: false
  61. });
  62. try {
  63. const reportGenerated = emittedOnce(reports, 'report');
  64. const url = `https://localhost:${(server.address() as any).port}/a`;
  65. await bw.loadURL(url);
  66. const [report] = await reportGenerated;
  67. expect(report).to.be.an('array');
  68. expect(report[0].type).to.equal('deprecation');
  69. expect(report[0].url).to.equal(url);
  70. expect(report[0].body.id).to.equal('PrefixedRequestAnimationFrame');
  71. } finally {
  72. bw.destroy();
  73. server.close();
  74. }
  75. });
  76. });
  77. describe('window.postMessage', () => {
  78. afterEach(async () => {
  79. await closeAllWindows();
  80. });
  81. it('sets the source and origin correctly', async () => {
  82. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  83. w.loadURL(`file://${fixturesPath}/pages/window-open-postMessage-driver.html`);
  84. const [, message] = await emittedOnce(ipcMain, 'complete');
  85. expect(message.data).to.equal('testing');
  86. expect(message.origin).to.equal('file://');
  87. expect(message.sourceEqualsOpener).to.equal(true);
  88. expect(message.eventOrigin).to.equal('file://');
  89. });
  90. });
  91. describe('focus handling', () => {
  92. let webviewContents: WebContents = null as unknown as WebContents;
  93. let w: BrowserWindow = null as unknown as BrowserWindow;
  94. beforeEach(async () => {
  95. w = new BrowserWindow({
  96. show: true,
  97. webPreferences: {
  98. nodeIntegration: true,
  99. webviewTag: true,
  100. contextIsolation: false
  101. }
  102. });
  103. const webviewReady = emittedOnce(w.webContents, 'did-attach-webview');
  104. await w.loadFile(path.join(fixturesPath, 'pages', 'tab-focus-loop-elements.html'));
  105. const [, wvContents] = await webviewReady;
  106. webviewContents = wvContents;
  107. await emittedOnce(webviewContents, 'did-finish-load');
  108. w.focus();
  109. });
  110. afterEach(() => {
  111. webviewContents = null as unknown as WebContents;
  112. w.destroy();
  113. w = null as unknown as BrowserWindow;
  114. });
  115. const expectFocusChange = async () => {
  116. const [, focusedElementId] = await emittedOnce(ipcMain, 'focus-changed');
  117. return focusedElementId;
  118. };
  119. describe('a TAB press', () => {
  120. const tabPressEvent: any = {
  121. type: 'keyDown',
  122. keyCode: 'Tab'
  123. };
  124. it('moves focus to the next focusable item', async () => {
  125. let focusChange = expectFocusChange();
  126. w.webContents.sendInputEvent(tabPressEvent);
  127. let focusedElementId = await focusChange;
  128. expect(focusedElementId).to.equal('BUTTON-element-1', `should start focused in element-1, it's instead in ${focusedElementId}`);
  129. focusChange = expectFocusChange();
  130. w.webContents.sendInputEvent(tabPressEvent);
  131. focusedElementId = await focusChange;
  132. expect(focusedElementId).to.equal('BUTTON-element-2', `focus should've moved to element-2, it's instead in ${focusedElementId}`);
  133. focusChange = expectFocusChange();
  134. w.webContents.sendInputEvent(tabPressEvent);
  135. focusedElementId = await focusChange;
  136. expect(focusedElementId).to.equal('BUTTON-wv-element-1', `focus should've moved to the webview's element-1, it's instead in ${focusedElementId}`);
  137. focusChange = expectFocusChange();
  138. webviewContents.sendInputEvent(tabPressEvent);
  139. focusedElementId = await focusChange;
  140. expect(focusedElementId).to.equal('BUTTON-wv-element-2', `focus should've moved to the webview's element-2, it's instead in ${focusedElementId}`);
  141. focusChange = expectFocusChange();
  142. webviewContents.sendInputEvent(tabPressEvent);
  143. focusedElementId = await focusChange;
  144. expect(focusedElementId).to.equal('BUTTON-element-3', `focus should've moved to element-3, it's instead in ${focusedElementId}`);
  145. focusChange = expectFocusChange();
  146. w.webContents.sendInputEvent(tabPressEvent);
  147. focusedElementId = await focusChange;
  148. expect(focusedElementId).to.equal('BUTTON-element-1', `focus should've looped back to element-1, it's instead in ${focusedElementId}`);
  149. });
  150. });
  151. describe('a SHIFT + TAB press', () => {
  152. const shiftTabPressEvent: any = {
  153. type: 'keyDown',
  154. modifiers: ['Shift'],
  155. keyCode: 'Tab'
  156. };
  157. it('moves focus to the previous focusable item', async () => {
  158. let focusChange = expectFocusChange();
  159. w.webContents.sendInputEvent(shiftTabPressEvent);
  160. let focusedElementId = await focusChange;
  161. expect(focusedElementId).to.equal('BUTTON-element-3', `should start focused in element-3, it's instead in ${focusedElementId}`);
  162. focusChange = expectFocusChange();
  163. w.webContents.sendInputEvent(shiftTabPressEvent);
  164. focusedElementId = await focusChange;
  165. expect(focusedElementId).to.equal('BUTTON-wv-element-2', `focus should've moved to the webview's element-2, it's instead in ${focusedElementId}`);
  166. focusChange = expectFocusChange();
  167. webviewContents.sendInputEvent(shiftTabPressEvent);
  168. focusedElementId = await focusChange;
  169. expect(focusedElementId).to.equal('BUTTON-wv-element-1', `focus should've moved to the webview's element-1, it's instead in ${focusedElementId}`);
  170. focusChange = expectFocusChange();
  171. webviewContents.sendInputEvent(shiftTabPressEvent);
  172. focusedElementId = await focusChange;
  173. expect(focusedElementId).to.equal('BUTTON-element-2', `focus should've moved to element-2, it's instead in ${focusedElementId}`);
  174. focusChange = expectFocusChange();
  175. w.webContents.sendInputEvent(shiftTabPressEvent);
  176. focusedElementId = await focusChange;
  177. expect(focusedElementId).to.equal('BUTTON-element-1', `focus should've moved to element-1, it's instead in ${focusedElementId}`);
  178. focusChange = expectFocusChange();
  179. w.webContents.sendInputEvent(shiftTabPressEvent);
  180. focusedElementId = await focusChange;
  181. expect(focusedElementId).to.equal('BUTTON-element-3', `focus should've looped back to element-3, it's instead in ${focusedElementId}`);
  182. });
  183. });
  184. });
  185. describe('web security', () => {
  186. afterEach(closeAllWindows);
  187. let server: http.Server;
  188. let serverUrl: string;
  189. before(async () => {
  190. server = http.createServer((req, res) => {
  191. res.setHeader('Content-Type', 'text/html');
  192. res.end('<body>');
  193. });
  194. await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
  195. serverUrl = `http://localhost:${(server.address() as any).port}`;
  196. });
  197. after(() => {
  198. server.close();
  199. });
  200. it('engages CORB when web security is not disabled', async () => {
  201. const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: true, nodeIntegration: true, contextIsolation: false } });
  202. const p = emittedOnce(ipcMain, 'success');
  203. await w.loadURL(`data:text/html,<script>
  204. const s = document.createElement('script')
  205. s.src = "${serverUrl}"
  206. // The script will load successfully but its body will be emptied out
  207. // by CORB, so we don't expect a syntax error.
  208. s.onload = () => { require('electron').ipcRenderer.send('success') }
  209. document.documentElement.appendChild(s)
  210. </script>`);
  211. await p;
  212. });
  213. it('bypasses CORB when web security is disabled', async () => {
  214. const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: false, nodeIntegration: true, contextIsolation: false } });
  215. const p = emittedOnce(ipcMain, 'success');
  216. await w.loadURL(`data:text/html,
  217. <script>
  218. window.onerror = (e) => { require('electron').ipcRenderer.send('success', e) }
  219. </script>
  220. <script src="${serverUrl}"></script>`);
  221. await p;
  222. });
  223. it('engages CORS when web security is not disabled', async () => {
  224. const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: true, nodeIntegration: true, contextIsolation: false } });
  225. const p = emittedOnce(ipcMain, 'response');
  226. await w.loadURL(`data:text/html,<script>
  227. (async function() {
  228. try {
  229. await fetch('${serverUrl}');
  230. require('electron').ipcRenderer.send('response', 'passed');
  231. } catch {
  232. require('electron').ipcRenderer.send('response', 'failed');
  233. }
  234. })();
  235. </script>`);
  236. const [, response] = await p;
  237. expect(response).to.equal('failed');
  238. });
  239. it('bypasses CORS when web security is disabled', async () => {
  240. const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: false, nodeIntegration: true, contextIsolation: false } });
  241. const p = emittedOnce(ipcMain, 'response');
  242. await w.loadURL(`data:text/html,<script>
  243. (async function() {
  244. try {
  245. await fetch('${serverUrl}');
  246. require('electron').ipcRenderer.send('response', 'passed');
  247. } catch {
  248. require('electron').ipcRenderer.send('response', 'failed');
  249. }
  250. })();
  251. </script>`);
  252. const [, response] = await p;
  253. expect(response).to.equal('passed');
  254. });
  255. describe('accessing file://', () => {
  256. async function loadFile (w: BrowserWindow) {
  257. const thisFile = url.format({
  258. pathname: __filename.replace(/\\/g, '/'),
  259. protocol: 'file',
  260. slashes: true
  261. });
  262. await w.loadURL(`data:text/html,<script>
  263. function loadFile() {
  264. return new Promise((resolve) => {
  265. fetch('${thisFile}').then(
  266. () => resolve('loaded'),
  267. () => resolve('failed')
  268. )
  269. });
  270. }
  271. </script>`);
  272. return await w.webContents.executeJavaScript('loadFile()');
  273. }
  274. it('is forbidden when web security is enabled', async () => {
  275. const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: true } });
  276. const result = await loadFile(w);
  277. expect(result).to.equal('failed');
  278. });
  279. it('is allowed when web security is disabled', async () => {
  280. const w = new BrowserWindow({ show: false, webPreferences: { webSecurity: false } });
  281. const result = await loadFile(w);
  282. expect(result).to.equal('loaded');
  283. });
  284. });
  285. describe('wasm-eval csp', () => {
  286. async function loadWasm (csp: string) {
  287. const w = new BrowserWindow({
  288. show: false,
  289. webPreferences: {
  290. sandbox: true,
  291. enableBlinkFeatures: 'WebAssemblyCSP'
  292. }
  293. });
  294. await w.loadURL(`data:text/html,<head>
  295. <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' ${csp}">
  296. </head>
  297. <script>
  298. function loadWasm() {
  299. const wasmBin = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0])
  300. return new Promise((resolve) => {
  301. WebAssembly.instantiate(wasmBin).then(() => {
  302. resolve('loaded')
  303. }).catch((error) => {
  304. resolve(error.message)
  305. })
  306. });
  307. }
  308. </script>`);
  309. return await w.webContents.executeJavaScript('loadWasm()');
  310. }
  311. it('wasm codegen is disallowed by default', async () => {
  312. const r = await loadWasm('');
  313. expect(r).to.equal('WebAssembly.instantiate(): Refused to compile or instantiate WebAssembly module because \'unsafe-eval\' is not an allowed source of script in the following Content Security Policy directive: "script-src \'self\' \'unsafe-inline\'"');
  314. });
  315. it('wasm codegen is allowed with "wasm-unsafe-eval" csp', async () => {
  316. const r = await loadWasm("'wasm-unsafe-eval'");
  317. expect(r).to.equal('loaded');
  318. });
  319. });
  320. describe('csp', () => {
  321. for (const sandbox of [true, false]) {
  322. describe(`when sandbox: ${sandbox}`, () => {
  323. for (const contextIsolation of [true, false]) {
  324. describe(`when contextIsolation: ${contextIsolation}`, () => {
  325. it('prevents eval from running in an inline script', async () => {
  326. const w = new BrowserWindow({
  327. show: false,
  328. webPreferences: { sandbox, contextIsolation }
  329. });
  330. w.loadURL(`data:text/html,<head>
  331. <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'">
  332. </head>
  333. <script>
  334. try {
  335. // We use console.log here because it is easier than making a
  336. // preload script, and the behavior under test changes when
  337. // contextIsolation: false
  338. console.log(eval('true'))
  339. } catch (e) {
  340. console.log(e.message)
  341. }
  342. </script>`);
  343. const [,, message] = await emittedOnce(w.webContents, 'console-message');
  344. expect(message).to.match(/Refused to evaluate a string/);
  345. });
  346. it('does not prevent eval from running in an inline script when there is no csp', async () => {
  347. const w = new BrowserWindow({
  348. show: false,
  349. webPreferences: { sandbox, contextIsolation }
  350. });
  351. w.loadURL(`data:text/html,
  352. <script>
  353. try {
  354. // We use console.log here because it is easier than making a
  355. // preload script, and the behavior under test changes when
  356. // contextIsolation: false
  357. console.log(eval('true'))
  358. } catch (e) {
  359. console.log(e.message)
  360. }
  361. </script>`);
  362. const [,, message] = await emittedOnce(w.webContents, 'console-message');
  363. expect(message).to.equal('true');
  364. });
  365. it('prevents eval from running in executeJavaScript', async () => {
  366. const w = new BrowserWindow({
  367. show: false,
  368. webPreferences: { sandbox, contextIsolation }
  369. });
  370. w.loadURL('data:text/html,<head><meta http-equiv="Content-Security-Policy" content="default-src \'self\'; script-src \'self\' \'unsafe-inline\'"></meta></head>');
  371. await expect(w.webContents.executeJavaScript('eval("true")')).to.be.rejected();
  372. });
  373. it('does not prevent eval from running in executeJavaScript when there is no csp', async () => {
  374. const w = new BrowserWindow({
  375. show: false,
  376. webPreferences: { sandbox, contextIsolation }
  377. });
  378. w.loadURL('data:text/html,');
  379. expect(await w.webContents.executeJavaScript('eval("true")')).to.be.true();
  380. });
  381. });
  382. }
  383. });
  384. }
  385. });
  386. it('does not crash when multiple WebContent are created with web security disabled', () => {
  387. const options = { show: false, webPreferences: { webSecurity: false } };
  388. const w1 = new BrowserWindow(options);
  389. w1.loadURL(serverUrl);
  390. const w2 = new BrowserWindow(options);
  391. w2.loadURL(serverUrl);
  392. });
  393. });
  394. describe('command line switches', () => {
  395. let appProcess: ChildProcess.ChildProcessWithoutNullStreams | undefined;
  396. afterEach(() => {
  397. if (appProcess && !appProcess.killed) {
  398. appProcess.kill();
  399. appProcess = undefined;
  400. }
  401. });
  402. describe('--lang switch', () => {
  403. const currentLocale = app.getLocale();
  404. const currentSystemLocale = app.getSystemLocale();
  405. const currentPreferredLanguages = JSON.stringify(app.getPreferredSystemLanguages());
  406. const testLocale = async (locale: string, result: string, printEnv: boolean = false) => {
  407. const appPath = path.join(fixturesPath, 'api', 'locale-check');
  408. const args = [appPath, `--set-lang=${locale}`];
  409. if (printEnv) {
  410. args.push('--print-env');
  411. }
  412. appProcess = ChildProcess.spawn(process.execPath, args);
  413. let output = '';
  414. appProcess.stdout.on('data', (data) => { output += data; });
  415. let stderr = '';
  416. appProcess.stderr.on('data', (data) => { stderr += data; });
  417. const [code, signal] = await emittedOnce(appProcess, 'exit');
  418. if (code !== 0) {
  419. throw new Error(`Process exited with code "${code}" signal "${signal}" output "${output}" stderr "${stderr}"`);
  420. }
  421. output = output.replace(/(\r\n|\n|\r)/gm, '');
  422. expect(output).to.equal(result);
  423. };
  424. it('should set the locale', async () => testLocale('fr', `fr|${currentSystemLocale}|${currentPreferredLanguages}`));
  425. it('should set the locale with country code', async () => testLocale('zh-CN', `zh-CN|${currentSystemLocale}|${currentPreferredLanguages}`));
  426. it('should not set an invalid locale', async () => testLocale('asdfkl', `${currentLocale}|${currentSystemLocale}|${currentPreferredLanguages}`));
  427. const lcAll = String(process.env.LC_ALL);
  428. ifit(process.platform === 'linux')('current process has a valid LC_ALL env', async () => {
  429. // The LC_ALL env should not be set to DOM locale string.
  430. expect(lcAll).to.not.equal(app.getLocale());
  431. });
  432. ifit(process.platform === 'linux')('should not change LC_ALL', async () => testLocale('fr', lcAll, true));
  433. ifit(process.platform === 'linux')('should not change LC_ALL when setting invalid locale', async () => testLocale('asdfkl', lcAll, true));
  434. ifit(process.platform === 'linux')('should not change LC_ALL when --lang is not set', async () => testLocale('', lcAll, true));
  435. });
  436. describe('--remote-debugging-pipe switch', () => {
  437. it('should expose CDP via pipe', async () => {
  438. const electronPath = process.execPath;
  439. appProcess = ChildProcess.spawn(electronPath, ['--remote-debugging-pipe'], {
  440. stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'pipe']
  441. }) as ChildProcess.ChildProcessWithoutNullStreams;
  442. const stdio = appProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
  443. const pipe = new PipeTransport(stdio[3], stdio[4]);
  444. const versionPromise = new Promise(resolve => { pipe.onmessage = resolve; });
  445. pipe.send({ id: 1, method: 'Browser.getVersion', params: {} });
  446. const message = (await versionPromise) as any;
  447. expect(message.id).to.equal(1);
  448. expect(message.result.product).to.contain('Chrome');
  449. expect(message.result.userAgent).to.contain('Electron');
  450. });
  451. it('should override --remote-debugging-port switch', async () => {
  452. const electronPath = process.execPath;
  453. appProcess = ChildProcess.spawn(electronPath, ['--remote-debugging-pipe', '--remote-debugging-port=0'], {
  454. stdio: ['inherit', 'inherit', 'pipe', 'pipe', 'pipe']
  455. }) as ChildProcess.ChildProcessWithoutNullStreams;
  456. let stderr = '';
  457. appProcess.stderr.on('data', (data: string) => { stderr += data; });
  458. const stdio = appProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
  459. const pipe = new PipeTransport(stdio[3], stdio[4]);
  460. const versionPromise = new Promise(resolve => { pipe.onmessage = resolve; });
  461. pipe.send({ id: 1, method: 'Browser.getVersion', params: {} });
  462. const message = (await versionPromise) as any;
  463. expect(message.id).to.equal(1);
  464. expect(stderr).to.not.include('DevTools listening on');
  465. });
  466. it('should shut down Electron upon Browser.close CDP command', async () => {
  467. const electronPath = process.execPath;
  468. appProcess = ChildProcess.spawn(electronPath, ['--remote-debugging-pipe'], {
  469. stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'pipe']
  470. }) as ChildProcess.ChildProcessWithoutNullStreams;
  471. const stdio = appProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
  472. const pipe = new PipeTransport(stdio[3], stdio[4]);
  473. pipe.send({ id: 1, method: 'Browser.close', params: {} });
  474. await new Promise(resolve => { appProcess!.on('exit', resolve); });
  475. });
  476. });
  477. describe('--remote-debugging-port switch', () => {
  478. it('should display the discovery page', (done) => {
  479. const electronPath = process.execPath;
  480. let output = '';
  481. appProcess = ChildProcess.spawn(electronPath, ['--remote-debugging-port=']);
  482. appProcess.stdout.on('data', (data) => {
  483. console.log(data);
  484. });
  485. appProcess.stderr.on('data', (data) => {
  486. console.log(data);
  487. output += data;
  488. const m = /DevTools listening on ws:\/\/127.0.0.1:(\d+)\//.exec(output);
  489. if (m) {
  490. appProcess!.stderr.removeAllListeners('data');
  491. const port = m[1];
  492. http.get(`http://127.0.0.1:${port}`, (res) => {
  493. try {
  494. expect(res.statusCode).to.eql(200);
  495. expect(parseInt(res.headers['content-length']!)).to.be.greaterThan(0);
  496. done();
  497. } catch (e) {
  498. done(e);
  499. } finally {
  500. res.destroy();
  501. }
  502. });
  503. }
  504. });
  505. });
  506. });
  507. });
  508. describe('chromium features', () => {
  509. afterEach(closeAllWindows);
  510. describe('accessing key names also used as Node.js module names', () => {
  511. it('does not crash', (done) => {
  512. const w = new BrowserWindow({ show: false });
  513. w.webContents.once('did-finish-load', () => { done(); });
  514. w.webContents.once('crashed', () => done(new Error('WebContents crashed.')));
  515. w.loadFile(path.join(fixturesPath, 'pages', 'external-string.html'));
  516. });
  517. });
  518. describe('first party sets', () => {
  519. const fps = [
  520. 'https://fps-member1.glitch.me',
  521. 'https://fps-member2.glitch.me',
  522. 'https://fps-member3.glitch.me'
  523. ];
  524. it('loads first party sets', async () => {
  525. const appPath = path.join(fixturesPath, 'api', 'first-party-sets', 'base');
  526. const fpsProcess = ChildProcess.spawn(process.execPath, [appPath]);
  527. let output = '';
  528. fpsProcess.stdout.on('data', data => { output += data; });
  529. await emittedOnce(fpsProcess, 'exit');
  530. expect(output).to.include(fps.join(','));
  531. });
  532. it('loads sets from the command line', async () => {
  533. const appPath = path.join(fixturesPath, 'api', 'first-party-sets', 'command-line');
  534. const args = [appPath, `--use-first-party-set=${fps}`];
  535. const fpsProcess = ChildProcess.spawn(process.execPath, args);
  536. let output = '';
  537. fpsProcess.stdout.on('data', data => { output += data; });
  538. await emittedOnce(fpsProcess, 'exit');
  539. expect(output).to.include(fps.join(','));
  540. });
  541. });
  542. describe('loading jquery', () => {
  543. it('does not crash', (done) => {
  544. const w = new BrowserWindow({ show: false });
  545. w.webContents.once('did-finish-load', () => { done(); });
  546. w.webContents.once('crashed', () => done(new Error('WebContents crashed.')));
  547. w.loadFile(path.join(__dirname, 'fixtures', 'pages', 'jquery.html'));
  548. });
  549. });
  550. describe('navigator.languages', () => {
  551. it('should return the system locale only', async () => {
  552. const appLocale = app.getLocale();
  553. const w = new BrowserWindow({ show: false });
  554. await w.loadURL('about:blank');
  555. const languages = await w.webContents.executeJavaScript('navigator.languages');
  556. expect(languages.length).to.be.greaterThan(0);
  557. expect(languages).to.contain(appLocale);
  558. });
  559. });
  560. describe('navigator.serviceWorker', () => {
  561. it('should register for file scheme', (done) => {
  562. const w = new BrowserWindow({
  563. show: false,
  564. webPreferences: {
  565. nodeIntegration: true,
  566. partition: 'sw-file-scheme-spec',
  567. contextIsolation: false
  568. }
  569. });
  570. w.webContents.on('ipc-message', (event, channel, message) => {
  571. if (channel === 'reload') {
  572. w.webContents.reload();
  573. } else if (channel === 'error') {
  574. done(message);
  575. } else if (channel === 'response') {
  576. expect(message).to.equal('Hello from serviceWorker!');
  577. session.fromPartition('sw-file-scheme-spec').clearStorageData({
  578. storages: ['serviceworkers']
  579. }).then(() => done());
  580. }
  581. });
  582. w.webContents.on('crashed', () => done(new Error('WebContents crashed.')));
  583. w.loadFile(path.join(fixturesPath, 'pages', 'service-worker', 'index.html'));
  584. });
  585. it('should register for intercepted file scheme', (done) => {
  586. const customSession = session.fromPartition('intercept-file');
  587. customSession.protocol.interceptBufferProtocol('file', (request, callback) => {
  588. let file = url.parse(request.url).pathname!;
  589. if (file[0] === '/' && process.platform === 'win32') file = file.slice(1);
  590. const content = fs.readFileSync(path.normalize(file));
  591. const ext = path.extname(file);
  592. let type = 'text/html';
  593. if (ext === '.js') type = 'application/javascript';
  594. callback({ data: content, mimeType: type } as any);
  595. });
  596. const w = new BrowserWindow({
  597. show: false,
  598. webPreferences: {
  599. nodeIntegration: true,
  600. session: customSession,
  601. contextIsolation: false
  602. }
  603. });
  604. w.webContents.on('ipc-message', (event, channel, message) => {
  605. if (channel === 'reload') {
  606. w.webContents.reload();
  607. } else if (channel === 'error') {
  608. done(`unexpected error : ${message}`);
  609. } else if (channel === 'response') {
  610. expect(message).to.equal('Hello from serviceWorker!');
  611. customSession.clearStorageData({
  612. storages: ['serviceworkers']
  613. }).then(() => {
  614. customSession.protocol.uninterceptProtocol('file');
  615. done();
  616. });
  617. }
  618. });
  619. w.webContents.on('crashed', () => done(new Error('WebContents crashed.')));
  620. w.loadFile(path.join(fixturesPath, 'pages', 'service-worker', 'index.html'));
  621. });
  622. it('should register for custom scheme', (done) => {
  623. const customSession = session.fromPartition('custom-scheme');
  624. customSession.protocol.registerFileProtocol(serviceWorkerScheme, (request, callback) => {
  625. let file = url.parse(request.url).pathname!;
  626. if (file[0] === '/' && process.platform === 'win32') file = file.slice(1);
  627. callback({ path: path.normalize(file) } as any);
  628. });
  629. const w = new BrowserWindow({
  630. show: false,
  631. webPreferences: {
  632. nodeIntegration: true,
  633. session: customSession,
  634. contextIsolation: false
  635. }
  636. });
  637. w.webContents.on('ipc-message', (event, channel, message) => {
  638. if (channel === 'reload') {
  639. w.webContents.reload();
  640. } else if (channel === 'error') {
  641. done(`unexpected error : ${message}`);
  642. } else if (channel === 'response') {
  643. expect(message).to.equal('Hello from serviceWorker!');
  644. customSession.clearStorageData({
  645. storages: ['serviceworkers']
  646. }).then(() => {
  647. customSession.protocol.uninterceptProtocol(serviceWorkerScheme);
  648. done();
  649. });
  650. }
  651. });
  652. w.webContents.on('crashed', () => done(new Error('WebContents crashed.')));
  653. w.loadFile(path.join(fixturesPath, 'pages', 'service-worker', 'custom-scheme-index.html'));
  654. });
  655. it('should not allow nodeIntegrationInWorker', async () => {
  656. const w = new BrowserWindow({
  657. show: false,
  658. webPreferences: {
  659. nodeIntegration: true,
  660. nodeIntegrationInWorker: true,
  661. partition: 'sw-file-scheme-worker-spec',
  662. contextIsolation: false
  663. }
  664. });
  665. await w.loadURL(`file://${fixturesPath}/pages/service-worker/empty.html`);
  666. const data = await w.webContents.executeJavaScript(`
  667. navigator.serviceWorker.register('worker-no-node.js', {
  668. scope: './'
  669. }).then(() => navigator.serviceWorker.ready)
  670. new Promise((resolve) => {
  671. navigator.serviceWorker.onmessage = event => resolve(event.data);
  672. });
  673. `);
  674. expect(data).to.equal('undefined undefined undefined undefined');
  675. });
  676. });
  677. ifdescribe(features.isFakeLocationProviderEnabled())('navigator.geolocation', () => {
  678. it('returns error when permission is denied', async () => {
  679. const w = new BrowserWindow({
  680. show: false,
  681. webPreferences: {
  682. nodeIntegration: true,
  683. partition: 'geolocation-spec',
  684. contextIsolation: false
  685. }
  686. });
  687. const message = emittedOnce(w.webContents, 'ipc-message');
  688. w.webContents.session.setPermissionRequestHandler((wc, permission, callback) => {
  689. if (permission === 'geolocation') {
  690. callback(false);
  691. } else {
  692. callback(true);
  693. }
  694. });
  695. w.loadFile(path.join(fixturesPath, 'pages', 'geolocation', 'index.html'));
  696. const [, channel] = await message;
  697. expect(channel).to.equal('success', 'unexpected response from geolocation api');
  698. });
  699. it('returns position when permission is granted', async () => {
  700. const w = new BrowserWindow({ show: false });
  701. await w.loadURL(`file://${fixturesPath}/pages/blank.html`);
  702. const position = await w.webContents.executeJavaScript(`new Promise((resolve, reject) =>
  703. navigator.geolocation.getCurrentPosition(
  704. x => resolve({coords: x.coords, timestamp: x.timestamp}),
  705. reject))`);
  706. expect(position).to.have.property('coords');
  707. expect(position).to.have.property('timestamp');
  708. });
  709. });
  710. describe('web workers', () => {
  711. let appProcess: ChildProcess.ChildProcessWithoutNullStreams | undefined;
  712. afterEach(() => {
  713. if (appProcess && !appProcess.killed) {
  714. appProcess.kill();
  715. appProcess = undefined;
  716. }
  717. });
  718. it('Worker with nodeIntegrationInWorker has access to self.module.paths', async () => {
  719. const appPath = path.join(__dirname, 'fixtures', 'apps', 'self-module-paths');
  720. appProcess = ChildProcess.spawn(process.execPath, [appPath]);
  721. const [code] = await emittedOnce(appProcess, 'exit');
  722. expect(code).to.equal(0);
  723. });
  724. it('Worker can work', async () => {
  725. const w = new BrowserWindow({ show: false });
  726. await w.loadURL(`file://${fixturesPath}/pages/blank.html`);
  727. const data = await w.webContents.executeJavaScript(`
  728. const worker = new Worker('../workers/worker.js');
  729. const message = 'ping';
  730. const eventPromise = new Promise((resolve) => { worker.onmessage = resolve; });
  731. worker.postMessage(message);
  732. eventPromise.then(t => t.data)
  733. `);
  734. expect(data).to.equal('ping');
  735. });
  736. it('Worker has no node integration by default', async () => {
  737. const w = new BrowserWindow({ show: false });
  738. await w.loadURL(`file://${fixturesPath}/pages/blank.html`);
  739. const data = await w.webContents.executeJavaScript(`
  740. const worker = new Worker('../workers/worker_node.js');
  741. new Promise((resolve) => { worker.onmessage = e => resolve(e.data); })
  742. `);
  743. expect(data).to.equal('undefined undefined undefined undefined');
  744. });
  745. it('Worker has node integration with nodeIntegrationInWorker', async () => {
  746. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, nodeIntegrationInWorker: true, contextIsolation: false } });
  747. w.loadURL(`file://${fixturesPath}/pages/worker.html`);
  748. const [, data] = await emittedOnce(ipcMain, 'worker-result');
  749. expect(data).to.equal('object function object function');
  750. });
  751. describe('SharedWorker', () => {
  752. it('can work', async () => {
  753. const w = new BrowserWindow({ show: false });
  754. await w.loadURL(`file://${fixturesPath}/pages/blank.html`);
  755. const data = await w.webContents.executeJavaScript(`
  756. const worker = new SharedWorker('../workers/shared_worker.js');
  757. const message = 'ping';
  758. const eventPromise = new Promise((resolve) => { worker.port.onmessage = e => resolve(e.data); });
  759. worker.port.postMessage(message);
  760. eventPromise
  761. `);
  762. expect(data).to.equal('ping');
  763. });
  764. it('has no node integration by default', async () => {
  765. const w = new BrowserWindow({ show: false });
  766. await w.loadURL(`file://${fixturesPath}/pages/blank.html`);
  767. const data = await w.webContents.executeJavaScript(`
  768. const worker = new SharedWorker('../workers/shared_worker_node.js');
  769. new Promise((resolve) => { worker.port.onmessage = e => resolve(e.data); })
  770. `);
  771. expect(data).to.equal('undefined undefined undefined undefined');
  772. });
  773. it('does not have node integration with nodeIntegrationInWorker', async () => {
  774. const w = new BrowserWindow({
  775. show: false,
  776. webPreferences: {
  777. nodeIntegration: true,
  778. nodeIntegrationInWorker: true,
  779. contextIsolation: false
  780. }
  781. });
  782. await w.loadURL(`file://${fixturesPath}/pages/blank.html`);
  783. const data = await w.webContents.executeJavaScript(`
  784. const worker = new SharedWorker('../workers/shared_worker_node.js');
  785. new Promise((resolve) => { worker.port.onmessage = e => resolve(e.data); })
  786. `);
  787. expect(data).to.equal('undefined undefined undefined undefined');
  788. });
  789. });
  790. });
  791. describe('form submit', () => {
  792. let server: http.Server;
  793. let serverUrl: string;
  794. before(async () => {
  795. server = http.createServer((req, res) => {
  796. let body = '';
  797. req.on('data', (chunk) => {
  798. body += chunk;
  799. });
  800. res.setHeader('Content-Type', 'application/json');
  801. req.on('end', () => {
  802. res.end(`body:${body}`);
  803. });
  804. });
  805. await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
  806. serverUrl = `http://localhost:${(server.address() as any).port}`;
  807. });
  808. after(async () => {
  809. server.close();
  810. await closeAllWindows();
  811. });
  812. [true, false].forEach((isSandboxEnabled) =>
  813. describe(`sandbox=${isSandboxEnabled}`, () => {
  814. it('posts data in the same window', async () => {
  815. const w = new BrowserWindow({
  816. show: false,
  817. webPreferences: {
  818. sandbox: isSandboxEnabled
  819. }
  820. });
  821. await w.loadFile(path.join(fixturesPath, 'pages', 'form-with-data.html'));
  822. const loadPromise = emittedOnce(w.webContents, 'did-finish-load');
  823. w.webContents.executeJavaScript(`
  824. const form = document.querySelector('form')
  825. form.action = '${serverUrl}';
  826. form.submit();
  827. `);
  828. await loadPromise;
  829. const res = await w.webContents.executeJavaScript('document.body.innerText');
  830. expect(res).to.equal('body:greeting=hello');
  831. });
  832. it('posts data to a new window with target=_blank', async () => {
  833. const w = new BrowserWindow({
  834. show: false,
  835. webPreferences: {
  836. sandbox: isSandboxEnabled
  837. }
  838. });
  839. await w.loadFile(path.join(fixturesPath, 'pages', 'form-with-data.html'));
  840. const windowCreatedPromise = emittedOnce(app, 'browser-window-created');
  841. w.webContents.executeJavaScript(`
  842. const form = document.querySelector('form')
  843. form.action = '${serverUrl}';
  844. form.target = '_blank';
  845. form.submit();
  846. `);
  847. const [, newWin] = await windowCreatedPromise;
  848. const res = await newWin.webContents.executeJavaScript('document.body.innerText');
  849. expect(res).to.equal('body:greeting=hello');
  850. });
  851. })
  852. );
  853. });
  854. describe('window.open', () => {
  855. for (const show of [true, false]) {
  856. it(`shows the child regardless of parent visibility when parent {show=${show}}`, async () => {
  857. const w = new BrowserWindow({ show });
  858. // toggle visibility
  859. if (show) {
  860. w.hide();
  861. } else {
  862. w.show();
  863. }
  864. defer(() => { w.close(); });
  865. const promise = emittedOnce(app, 'browser-window-created');
  866. w.loadFile(path.join(fixturesPath, 'pages', 'window-open.html'));
  867. const [, newWindow] = await promise;
  868. expect(newWindow.isVisible()).to.equal(true);
  869. });
  870. }
  871. // FIXME(zcbenz): This test is making the spec runner hang on exit on Windows.
  872. ifit(process.platform !== 'win32')('disables node integration when it is disabled on the parent window', async () => {
  873. const windowUrl = url.pathToFileURL(path.join(fixturesPath, 'pages', 'window-opener-no-node-integration.html'));
  874. windowUrl.searchParams.set('p', `${fixturesPath}/pages/window-opener-node.html`);
  875. const w = new BrowserWindow({ show: false });
  876. w.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
  877. const { eventData } = await w.webContents.executeJavaScript(`(async () => {
  878. const message = new Promise(resolve => window.addEventListener('message', resolve, {once: true}));
  879. const b = window.open(${JSON.stringify(windowUrl)}, '', 'show=false')
  880. const e = await message
  881. b.close();
  882. return {
  883. eventData: e.data
  884. }
  885. })()`);
  886. expect(eventData.isProcessGlobalUndefined).to.be.true();
  887. });
  888. it('disables node integration when it is disabled on the parent window for chrome devtools URLs', async () => {
  889. // NB. webSecurity is disabled because native window.open() is not
  890. // allowed to load devtools:// URLs.
  891. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webSecurity: false } });
  892. w.loadURL('about:blank');
  893. w.webContents.executeJavaScript(`
  894. { b = window.open('devtools://devtools/bundled/inspector.html', '', 'nodeIntegration=no,show=no'); null }
  895. `);
  896. const [, contents] = await emittedOnce(app, 'web-contents-created');
  897. const typeofProcessGlobal = await contents.executeJavaScript('typeof process');
  898. expect(typeofProcessGlobal).to.equal('undefined');
  899. });
  900. it('can disable node integration when it is enabled on the parent window', async () => {
  901. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } });
  902. w.loadURL('about:blank');
  903. w.webContents.executeJavaScript(`
  904. { b = window.open('about:blank', '', 'nodeIntegration=no,show=no'); null }
  905. `);
  906. const [, contents] = await emittedOnce(app, 'web-contents-created');
  907. const typeofProcessGlobal = await contents.executeJavaScript('typeof process');
  908. expect(typeofProcessGlobal).to.equal('undefined');
  909. });
  910. // TODO(jkleinsc) fix this flaky test on WOA
  911. ifit(process.platform !== 'win32' || process.arch !== 'arm64')('disables JavaScript when it is disabled on the parent window', async () => {
  912. const w = new BrowserWindow({ show: true, webPreferences: { nodeIntegration: true } });
  913. w.webContents.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
  914. const windowUrl = require('url').format({
  915. pathname: `${fixturesPath}/pages/window-no-javascript.html`,
  916. protocol: 'file',
  917. slashes: true
  918. });
  919. w.webContents.executeJavaScript(`
  920. { b = window.open(${JSON.stringify(windowUrl)}, '', 'javascript=no,show=no'); null }
  921. `);
  922. const [, contents] = await emittedOnce(app, 'web-contents-created');
  923. await emittedOnce(contents, 'did-finish-load');
  924. // Click link on page
  925. contents.sendInputEvent({ type: 'mouseDown', clickCount: 1, x: 1, y: 1 });
  926. contents.sendInputEvent({ type: 'mouseUp', clickCount: 1, x: 1, y: 1 });
  927. const [, window] = await emittedOnce(app, 'browser-window-created');
  928. const preferences = window.webContents.getLastWebPreferences();
  929. expect(preferences.javascript).to.be.false();
  930. });
  931. it('defines a window.location getter', async () => {
  932. let targetURL: string;
  933. if (process.platform === 'win32') {
  934. targetURL = `file:///${fixturesPath.replace(/\\/g, '/')}/pages/base-page.html`;
  935. } else {
  936. targetURL = `file://${fixturesPath}/pages/base-page.html`;
  937. }
  938. const w = new BrowserWindow({ show: false });
  939. w.webContents.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
  940. w.webContents.executeJavaScript(`{ b = window.open(${JSON.stringify(targetURL)}); null }`);
  941. const [, window] = await emittedOnce(app, 'browser-window-created');
  942. await emittedOnce(window.webContents, 'did-finish-load');
  943. expect(await w.webContents.executeJavaScript('b.location.href')).to.equal(targetURL);
  944. });
  945. it('defines a window.location setter', async () => {
  946. const w = new BrowserWindow({ show: false });
  947. w.webContents.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
  948. w.webContents.executeJavaScript('{ b = window.open("about:blank"); null }');
  949. const [, { webContents }] = await emittedOnce(app, 'browser-window-created');
  950. await emittedOnce(webContents, 'did-finish-load');
  951. // When it loads, redirect
  952. w.webContents.executeJavaScript(`{ b.location = ${JSON.stringify(`file://${fixturesPath}/pages/base-page.html`)}; null }`);
  953. await emittedOnce(webContents, 'did-finish-load');
  954. });
  955. it('defines a window.location.href setter', async () => {
  956. const w = new BrowserWindow({ show: false });
  957. w.webContents.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
  958. w.webContents.executeJavaScript('{ b = window.open("about:blank"); null }');
  959. const [, { webContents }] = await emittedOnce(app, 'browser-window-created');
  960. await emittedOnce(webContents, 'did-finish-load');
  961. // When it loads, redirect
  962. w.webContents.executeJavaScript(`{ b.location.href = ${JSON.stringify(`file://${fixturesPath}/pages/base-page.html`)}; null }`);
  963. await emittedOnce(webContents, 'did-finish-load');
  964. });
  965. it('open a blank page when no URL is specified', async () => {
  966. const w = new BrowserWindow({ show: false });
  967. w.loadURL('about:blank');
  968. w.webContents.executeJavaScript('{ b = window.open(); null }');
  969. const [, { webContents }] = await emittedOnce(app, 'browser-window-created');
  970. await emittedOnce(webContents, 'did-finish-load');
  971. expect(await w.webContents.executeJavaScript('b.location.href')).to.equal('about:blank');
  972. });
  973. it('open a blank page when an empty URL is specified', async () => {
  974. const w = new BrowserWindow({ show: false });
  975. w.loadURL('about:blank');
  976. w.webContents.executeJavaScript('{ b = window.open(\'\'); null }');
  977. const [, { webContents }] = await emittedOnce(app, 'browser-window-created');
  978. await emittedOnce(webContents, 'did-finish-load');
  979. expect(await w.webContents.executeJavaScript('b.location.href')).to.equal('about:blank');
  980. });
  981. it('does not throw an exception when the frameName is a built-in object property', async () => {
  982. const w = new BrowserWindow({ show: false });
  983. w.loadURL('about:blank');
  984. w.webContents.executeJavaScript('{ b = window.open(\'\', \'__proto__\'); null }');
  985. const frameName = await new Promise((resolve) => {
  986. w.webContents.setWindowOpenHandler(details => {
  987. setImmediate(() => resolve(details.frameName));
  988. return { action: 'allow' };
  989. });
  990. });
  991. expect(frameName).to.equal('__proto__');
  992. });
  993. // TODO(nornagon): I'm not sure this ... ever was correct?
  994. it.skip('inherit options of parent window', async () => {
  995. const w = new BrowserWindow({ show: false, width: 123, height: 456 });
  996. w.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
  997. const url = `file://${fixturesPath}/pages/window-open-size.html`;
  998. const { width, height, eventData } = await w.webContents.executeJavaScript(`(async () => {
  999. const message = new Promise(resolve => window.addEventListener('message', resolve, {once: true}));
  1000. const b = window.open(${JSON.stringify(url)}, '', 'show=false')
  1001. const e = await message
  1002. b.close();
  1003. const width = outerWidth;
  1004. const height = outerHeight;
  1005. return {
  1006. width,
  1007. height,
  1008. eventData: e.data
  1009. }
  1010. })()`);
  1011. expect(eventData).to.equal(`size: ${width} ${height}`);
  1012. expect(eventData).to.equal('size: 123 456');
  1013. });
  1014. it('does not override child options', async () => {
  1015. const w = new BrowserWindow({ show: false });
  1016. w.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
  1017. const windowUrl = `file://${fixturesPath}/pages/window-open-size.html`;
  1018. const { eventData } = await w.webContents.executeJavaScript(`(async () => {
  1019. const message = new Promise(resolve => window.addEventListener('message', resolve, {once: true}));
  1020. const b = window.open(${JSON.stringify(windowUrl)}, '', 'show=no,width=350,height=450')
  1021. const e = await message
  1022. b.close();
  1023. return { eventData: e.data }
  1024. })()`);
  1025. expect(eventData).to.equal('size: 350 450');
  1026. });
  1027. it('disables the <webview> tag when it is disabled on the parent window', async () => {
  1028. const windowUrl = url.pathToFileURL(path.join(fixturesPath, 'pages', 'window-opener-no-webview-tag.html'));
  1029. windowUrl.searchParams.set('p', `${fixturesPath}/pages/window-opener-webview.html`);
  1030. const w = new BrowserWindow({ show: false });
  1031. w.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
  1032. const { eventData } = await w.webContents.executeJavaScript(`(async () => {
  1033. const message = new Promise(resolve => window.addEventListener('message', resolve, {once: true}));
  1034. const b = window.open(${JSON.stringify(windowUrl)}, '', 'webviewTag=no,contextIsolation=no,nodeIntegration=yes,show=no')
  1035. const e = await message
  1036. b.close();
  1037. return { eventData: e.data }
  1038. })()`);
  1039. expect(eventData.isWebViewGlobalUndefined).to.be.true();
  1040. });
  1041. it('throws an exception when the arguments cannot be converted to strings', async () => {
  1042. const w = new BrowserWindow({ show: false });
  1043. w.loadURL('about:blank');
  1044. await expect(
  1045. w.webContents.executeJavaScript('window.open(\'\', { toString: null })')
  1046. ).to.eventually.be.rejected();
  1047. await expect(
  1048. w.webContents.executeJavaScript('window.open(\'\', \'\', { toString: 3 })')
  1049. ).to.eventually.be.rejected();
  1050. });
  1051. it('does not throw an exception when the features include webPreferences', async () => {
  1052. const w = new BrowserWindow({ show: false });
  1053. w.loadURL('about:blank');
  1054. await expect(
  1055. w.webContents.executeJavaScript('window.open(\'\', \'\', \'show=no,webPreferences=\'); null')
  1056. ).to.eventually.be.fulfilled();
  1057. });
  1058. });
  1059. describe('window.opener', () => {
  1060. it('is null for main window', async () => {
  1061. const w = new BrowserWindow({
  1062. show: false,
  1063. webPreferences: {
  1064. nodeIntegration: true,
  1065. contextIsolation: false
  1066. }
  1067. });
  1068. w.loadFile(path.join(fixturesPath, 'pages', 'window-opener.html'));
  1069. const [, channel, opener] = await emittedOnce(w.webContents, 'ipc-message');
  1070. expect(channel).to.equal('opener');
  1071. expect(opener).to.equal(null);
  1072. });
  1073. it('is not null for window opened by window.open', async () => {
  1074. const w = new BrowserWindow({
  1075. show: false,
  1076. webPreferences: {
  1077. nodeIntegration: true,
  1078. contextIsolation: false
  1079. }
  1080. });
  1081. w.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
  1082. const windowUrl = `file://${fixturesPath}/pages/window-opener.html`;
  1083. const eventData = await w.webContents.executeJavaScript(`
  1084. const b = window.open(${JSON.stringify(windowUrl)}, '', 'show=no');
  1085. new Promise(resolve => window.addEventListener('message', resolve, {once: true})).then(e => e.data);
  1086. `);
  1087. expect(eventData).to.equal('object');
  1088. });
  1089. });
  1090. describe('window.opener.postMessage', () => {
  1091. it('sets source and origin correctly', async () => {
  1092. const w = new BrowserWindow({ show: false });
  1093. w.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
  1094. const windowUrl = `file://${fixturesPath}/pages/window-opener-postMessage.html`;
  1095. const { sourceIsChild, origin } = await w.webContents.executeJavaScript(`
  1096. const b = window.open(${JSON.stringify(windowUrl)}, '', 'show=no');
  1097. new Promise(resolve => window.addEventListener('message', resolve, {once: true})).then(e => ({
  1098. sourceIsChild: e.source === b,
  1099. origin: e.origin
  1100. }));
  1101. `);
  1102. expect(sourceIsChild).to.be.true();
  1103. expect(origin).to.equal('file://');
  1104. });
  1105. it('supports windows opened from a <webview>', async () => {
  1106. const w = new BrowserWindow({ show: false, webPreferences: { webviewTag: true } });
  1107. w.loadURL('about:blank');
  1108. const childWindowUrl = url.pathToFileURL(path.join(fixturesPath, 'pages', 'webview-opener-postMessage.html'));
  1109. childWindowUrl.searchParams.set('p', `${fixturesPath}/pages/window-opener-postMessage.html`);
  1110. const message = await w.webContents.executeJavaScript(`
  1111. const webview = new WebView();
  1112. webview.allowpopups = true;
  1113. webview.setAttribute('webpreferences', 'contextIsolation=no');
  1114. webview.src = ${JSON.stringify(childWindowUrl)}
  1115. const consoleMessage = new Promise(resolve => webview.addEventListener('console-message', resolve, {once: true}));
  1116. document.body.appendChild(webview);
  1117. consoleMessage.then(e => e.message)
  1118. `);
  1119. expect(message).to.equal('message');
  1120. });
  1121. describe('targetOrigin argument', () => {
  1122. let serverURL: string;
  1123. let server: any;
  1124. beforeEach((done) => {
  1125. server = http.createServer((req, res) => {
  1126. res.writeHead(200);
  1127. const filePath = path.join(fixturesPath, 'pages', 'window-opener-targetOrigin.html');
  1128. res.end(fs.readFileSync(filePath, 'utf8'));
  1129. });
  1130. server.listen(0, '127.0.0.1', () => {
  1131. serverURL = `http://127.0.0.1:${server.address().port}`;
  1132. done();
  1133. });
  1134. });
  1135. afterEach(() => {
  1136. server.close();
  1137. });
  1138. it('delivers messages that match the origin', async () => {
  1139. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  1140. w.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
  1141. const data = await w.webContents.executeJavaScript(`
  1142. window.open(${JSON.stringify(serverURL)}, '', 'show=no,contextIsolation=no,nodeIntegration=yes');
  1143. new Promise(resolve => window.addEventListener('message', resolve, {once: true})).then(e => e.data)
  1144. `);
  1145. expect(data).to.equal('deliver');
  1146. });
  1147. });
  1148. });
  1149. describe('navigator.mediaDevices', () => {
  1150. afterEach(closeAllWindows);
  1151. afterEach(() => {
  1152. session.defaultSession.setPermissionCheckHandler(null);
  1153. session.defaultSession.setPermissionRequestHandler(null);
  1154. });
  1155. it('can return labels of enumerated devices', async () => {
  1156. const w = new BrowserWindow({ show: false });
  1157. w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
  1158. const labels = await w.webContents.executeJavaScript('navigator.mediaDevices.enumerateDevices().then(ds => ds.map(d => d.label))');
  1159. expect(labels.some((l: any) => l)).to.be.true();
  1160. });
  1161. it('does not return labels of enumerated devices when permission denied', async () => {
  1162. session.defaultSession.setPermissionCheckHandler(() => false);
  1163. const w = new BrowserWindow({ show: false });
  1164. w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
  1165. const labels = await w.webContents.executeJavaScript('navigator.mediaDevices.enumerateDevices().then(ds => ds.map(d => d.label))');
  1166. expect(labels.some((l: any) => l)).to.be.false();
  1167. });
  1168. it('returns the same device ids across reloads', async () => {
  1169. const ses = session.fromPartition('persist:media-device-id');
  1170. const w = new BrowserWindow({
  1171. show: false,
  1172. webPreferences: {
  1173. nodeIntegration: true,
  1174. session: ses,
  1175. contextIsolation: false
  1176. }
  1177. });
  1178. w.loadFile(path.join(fixturesPath, 'pages', 'media-id-reset.html'));
  1179. const [, firstDeviceIds] = await emittedOnce(ipcMain, 'deviceIds');
  1180. const [, secondDeviceIds] = await emittedOnce(ipcMain, 'deviceIds', () => w.webContents.reload());
  1181. expect(firstDeviceIds).to.deep.equal(secondDeviceIds);
  1182. });
  1183. it('can return new device id when cookie storage is cleared', async () => {
  1184. const ses = session.fromPartition('persist:media-device-id');
  1185. const w = new BrowserWindow({
  1186. show: false,
  1187. webPreferences: {
  1188. nodeIntegration: true,
  1189. session: ses,
  1190. contextIsolation: false
  1191. }
  1192. });
  1193. w.loadFile(path.join(fixturesPath, 'pages', 'media-id-reset.html'));
  1194. const [, firstDeviceIds] = await emittedOnce(ipcMain, 'deviceIds');
  1195. await ses.clearStorageData({ storages: ['cookies'] });
  1196. const [, secondDeviceIds] = await emittedOnce(ipcMain, 'deviceIds', () => w.webContents.reload());
  1197. expect(firstDeviceIds).to.not.deep.equal(secondDeviceIds);
  1198. });
  1199. it('provides a securityOrigin to the request handler', async () => {
  1200. session.defaultSession.setPermissionRequestHandler(
  1201. (wc, permission, callback, details) => {
  1202. if (details.securityOrigin !== undefined) {
  1203. callback(true);
  1204. } else {
  1205. callback(false);
  1206. }
  1207. }
  1208. );
  1209. const w = new BrowserWindow({ show: false });
  1210. w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
  1211. const labels = await w.webContents.executeJavaScript(`navigator.mediaDevices.getUserMedia({
  1212. video: {
  1213. mandatory: {
  1214. chromeMediaSource: "desktop",
  1215. minWidth: 1280,
  1216. maxWidth: 1280,
  1217. minHeight: 720,
  1218. maxHeight: 720
  1219. }
  1220. }
  1221. }).then((stream) => stream.getVideoTracks())`);
  1222. expect(labels.some((l: any) => l)).to.be.true();
  1223. });
  1224. it('fails with "not supported" for getDisplayMedia', async () => {
  1225. const w = new BrowserWindow({ show: false });
  1226. w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
  1227. const { ok, err } = await w.webContents.executeJavaScript('navigator.mediaDevices.getDisplayMedia({video: true}).then(s => ({ok: true}), e => ({ok: false, err: e.message}))');
  1228. expect(ok).to.be.false();
  1229. expect(err).to.equal('Not supported');
  1230. });
  1231. });
  1232. describe('window.opener access', () => {
  1233. const scheme = 'app';
  1234. const fileUrl = `file://${fixturesPath}/pages/window-opener-location.html`;
  1235. const httpUrl1 = `${scheme}://origin1`;
  1236. const httpUrl2 = `${scheme}://origin2`;
  1237. const fileBlank = `file://${fixturesPath}/pages/blank.html`;
  1238. const httpBlank = `${scheme}://origin1/blank`;
  1239. const table = [
  1240. { parent: fileBlank, child: httpUrl1, nodeIntegration: false, openerAccessible: false },
  1241. { parent: fileBlank, child: httpUrl1, nodeIntegration: true, openerAccessible: false },
  1242. // {parent: httpBlank, child: fileUrl, nodeIntegration: false, openerAccessible: false}, // can't window.open()
  1243. // {parent: httpBlank, child: fileUrl, nodeIntegration: true, openerAccessible: false}, // can't window.open()
  1244. // NB. this is different from Chrome's behavior, which isolates file: urls from each other
  1245. { parent: fileBlank, child: fileUrl, nodeIntegration: false, openerAccessible: true },
  1246. { parent: fileBlank, child: fileUrl, nodeIntegration: true, openerAccessible: true },
  1247. { parent: httpBlank, child: httpUrl1, nodeIntegration: false, openerAccessible: true },
  1248. { parent: httpBlank, child: httpUrl1, nodeIntegration: true, openerAccessible: true },
  1249. { parent: httpBlank, child: httpUrl2, nodeIntegration: false, openerAccessible: false },
  1250. { parent: httpBlank, child: httpUrl2, nodeIntegration: true, openerAccessible: false }
  1251. ];
  1252. const s = (url: string) => url.startsWith('file') ? 'file://...' : url;
  1253. before(() => {
  1254. protocol.registerFileProtocol(scheme, (request, callback) => {
  1255. if (request.url.includes('blank')) {
  1256. callback(`${fixturesPath}/pages/blank.html`);
  1257. } else {
  1258. callback(`${fixturesPath}/pages/window-opener-location.html`);
  1259. }
  1260. });
  1261. });
  1262. after(() => {
  1263. protocol.unregisterProtocol(scheme);
  1264. });
  1265. afterEach(closeAllWindows);
  1266. describe('when opened from main window', () => {
  1267. for (const { parent, child, nodeIntegration, openerAccessible } of table) {
  1268. for (const sandboxPopup of [false, true]) {
  1269. const description = `when parent=${s(parent)} opens child=${s(child)} with nodeIntegration=${nodeIntegration} sandboxPopup=${sandboxPopup}, child should ${openerAccessible ? '' : 'not '}be able to access opener`;
  1270. it(description, async () => {
  1271. const w = new BrowserWindow({ show: true, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  1272. w.webContents.setWindowOpenHandler(() => ({
  1273. action: 'allow',
  1274. overrideBrowserWindowOptions: {
  1275. webPreferences: {
  1276. sandbox: sandboxPopup
  1277. }
  1278. }
  1279. }));
  1280. await w.loadURL(parent);
  1281. const childOpenerLocation = await w.webContents.executeJavaScript(`new Promise(resolve => {
  1282. window.addEventListener('message', function f(e) {
  1283. resolve(e.data)
  1284. })
  1285. window.open(${JSON.stringify(child)}, "", "show=no,nodeIntegration=${nodeIntegration ? 'yes' : 'no'}")
  1286. })`);
  1287. if (openerAccessible) {
  1288. expect(childOpenerLocation).to.be.a('string');
  1289. } else {
  1290. expect(childOpenerLocation).to.be.null();
  1291. }
  1292. });
  1293. }
  1294. }
  1295. });
  1296. describe('when opened from <webview>', () => {
  1297. for (const { parent, child, nodeIntegration, openerAccessible } of table) {
  1298. const description = `when parent=${s(parent)} opens child=${s(child)} with nodeIntegration=${nodeIntegration}, child should ${openerAccessible ? '' : 'not '}be able to access opener`;
  1299. it(description, async () => {
  1300. // This test involves three contexts:
  1301. // 1. The root BrowserWindow in which the test is run,
  1302. // 2. A <webview> belonging to the root window,
  1303. // 3. A window opened by calling window.open() from within the <webview>.
  1304. // We are testing whether context (3) can access context (2) under various conditions.
  1305. // This is context (1), the base window for the test.
  1306. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webviewTag: true, contextIsolation: false } });
  1307. await w.loadURL('about:blank');
  1308. const parentCode = `new Promise((resolve) => {
  1309. // This is context (3), a child window of the WebView.
  1310. const child = window.open(${JSON.stringify(child)}, "", "show=no,contextIsolation=no,nodeIntegration=yes")
  1311. window.addEventListener("message", e => {
  1312. resolve(e.data)
  1313. })
  1314. })`;
  1315. const childOpenerLocation = await w.webContents.executeJavaScript(`new Promise((resolve, reject) => {
  1316. // This is context (2), a WebView which will call window.open()
  1317. const webview = new WebView()
  1318. webview.setAttribute('nodeintegration', '${nodeIntegration ? 'on' : 'off'}')
  1319. webview.setAttribute('webpreferences', 'contextIsolation=no')
  1320. webview.setAttribute('allowpopups', 'on')
  1321. webview.src = ${JSON.stringify(parent + '?p=' + encodeURIComponent(child))}
  1322. webview.addEventListener('dom-ready', async () => {
  1323. webview.executeJavaScript(${JSON.stringify(parentCode)}).then(resolve, reject)
  1324. })
  1325. document.body.appendChild(webview)
  1326. })`);
  1327. if (openerAccessible) {
  1328. expect(childOpenerLocation).to.be.a('string');
  1329. } else {
  1330. expect(childOpenerLocation).to.be.null();
  1331. }
  1332. });
  1333. }
  1334. });
  1335. });
  1336. describe('storage', () => {
  1337. describe('custom non standard schemes', () => {
  1338. const protocolName = 'storage';
  1339. let contents: WebContents;
  1340. before(() => {
  1341. protocol.registerFileProtocol(protocolName, (request, callback) => {
  1342. const parsedUrl = url.parse(request.url);
  1343. let filename;
  1344. switch (parsedUrl.pathname) {
  1345. case '/localStorage' : filename = 'local_storage.html'; break;
  1346. case '/sessionStorage' : filename = 'session_storage.html'; break;
  1347. case '/WebSQL' : filename = 'web_sql.html'; break;
  1348. case '/indexedDB' : filename = 'indexed_db.html'; break;
  1349. case '/cookie' : filename = 'cookie.html'; break;
  1350. default : filename = '';
  1351. }
  1352. callback({ path: `${fixturesPath}/pages/storage/${filename}` });
  1353. });
  1354. });
  1355. after(() => {
  1356. protocol.unregisterProtocol(protocolName);
  1357. });
  1358. beforeEach(() => {
  1359. contents = (webContents as any).create({
  1360. nodeIntegration: true,
  1361. contextIsolation: false
  1362. });
  1363. });
  1364. afterEach(() => {
  1365. (contents as any).destroy();
  1366. contents = null as any;
  1367. });
  1368. it('cannot access localStorage', async () => {
  1369. const response = emittedOnce(ipcMain, 'local-storage-response');
  1370. contents.loadURL(protocolName + '://host/localStorage');
  1371. const [, error] = await response;
  1372. expect(error).to.equal('Failed to read the \'localStorage\' property from \'Window\': Access is denied for this document.');
  1373. });
  1374. it('cannot access sessionStorage', async () => {
  1375. const response = emittedOnce(ipcMain, 'session-storage-response');
  1376. contents.loadURL(`${protocolName}://host/sessionStorage`);
  1377. const [, error] = await response;
  1378. expect(error).to.equal('Failed to read the \'sessionStorage\' property from \'Window\': Access is denied for this document.');
  1379. });
  1380. it('cannot access WebSQL database', async () => {
  1381. const response = emittedOnce(ipcMain, 'web-sql-response');
  1382. contents.loadURL(`${protocolName}://host/WebSQL`);
  1383. const [, error] = await response;
  1384. expect(error).to.equal('Failed to execute \'openDatabase\' on \'Window\': Access to the WebDatabase API is denied in this context.');
  1385. });
  1386. it('cannot access indexedDB', async () => {
  1387. const response = emittedOnce(ipcMain, 'indexed-db-response');
  1388. contents.loadURL(`${protocolName}://host/indexedDB`);
  1389. const [, error] = await response;
  1390. expect(error).to.equal('Failed to execute \'open\' on \'IDBFactory\': access to the Indexed Database API is denied in this context.');
  1391. });
  1392. it('cannot access cookie', async () => {
  1393. const response = emittedOnce(ipcMain, 'cookie-response');
  1394. contents.loadURL(`${protocolName}://host/cookie`);
  1395. const [, error] = await response;
  1396. expect(error).to.equal('Failed to set the \'cookie\' property on \'Document\': Access is denied for this document.');
  1397. });
  1398. });
  1399. describe('can be accessed', () => {
  1400. let server: http.Server;
  1401. let serverUrl: string;
  1402. let serverCrossSiteUrl: string;
  1403. before((done) => {
  1404. server = http.createServer((req, res) => {
  1405. const respond = () => {
  1406. if (req.url === '/redirect-cross-site') {
  1407. res.setHeader('Location', `${serverCrossSiteUrl}/redirected`);
  1408. res.statusCode = 302;
  1409. res.end();
  1410. } else if (req.url === '/redirected') {
  1411. res.end('<html><script>window.localStorage</script></html>');
  1412. } else {
  1413. res.end();
  1414. }
  1415. };
  1416. setTimeout(respond, 0);
  1417. });
  1418. server.listen(0, '127.0.0.1', () => {
  1419. serverUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
  1420. serverCrossSiteUrl = `http://localhost:${(server.address() as AddressInfo).port}`;
  1421. done();
  1422. });
  1423. });
  1424. after(() => {
  1425. server.close();
  1426. server = null as any;
  1427. });
  1428. afterEach(closeAllWindows);
  1429. const testLocalStorageAfterXSiteRedirect = (testTitle: string, extraPreferences = {}) => {
  1430. it(testTitle, async () => {
  1431. const w = new BrowserWindow({
  1432. show: false,
  1433. ...extraPreferences
  1434. });
  1435. let redirected = false;
  1436. w.webContents.on('crashed', () => {
  1437. expect.fail('renderer crashed / was killed');
  1438. });
  1439. w.webContents.on('did-redirect-navigation', (event, url) => {
  1440. expect(url).to.equal(`${serverCrossSiteUrl}/redirected`);
  1441. redirected = true;
  1442. });
  1443. await w.loadURL(`${serverUrl}/redirect-cross-site`);
  1444. expect(redirected).to.be.true('didnt redirect');
  1445. });
  1446. };
  1447. testLocalStorageAfterXSiteRedirect('after a cross-site redirect');
  1448. testLocalStorageAfterXSiteRedirect('after a cross-site redirect in sandbox mode', { sandbox: true });
  1449. });
  1450. describe('enableWebSQL webpreference', () => {
  1451. const origin = `${standardScheme}://fake-host`;
  1452. const filePath = path.join(fixturesPath, 'pages', 'storage', 'web_sql.html');
  1453. const sqlPartition = 'web-sql-preference-test';
  1454. const sqlSession = session.fromPartition(sqlPartition);
  1455. const securityError = 'An attempt was made to break through the security policy of the user agent.';
  1456. let contents: WebContents, w: BrowserWindow;
  1457. before(() => {
  1458. sqlSession.protocol.registerFileProtocol(standardScheme, (request, callback) => {
  1459. callback({ path: filePath });
  1460. });
  1461. });
  1462. after(() => {
  1463. sqlSession.protocol.unregisterProtocol(standardScheme);
  1464. });
  1465. afterEach(async () => {
  1466. if (contents) {
  1467. (contents as any).destroy();
  1468. contents = null as any;
  1469. }
  1470. await closeAllWindows();
  1471. (w as any) = null;
  1472. });
  1473. it('default value allows websql', async () => {
  1474. contents = (webContents as any).create({
  1475. session: sqlSession,
  1476. nodeIntegration: true,
  1477. contextIsolation: false
  1478. });
  1479. contents.loadURL(origin);
  1480. const [, error] = await emittedOnce(ipcMain, 'web-sql-response');
  1481. expect(error).to.be.null();
  1482. });
  1483. it('when set to false can disallow websql', async () => {
  1484. contents = (webContents as any).create({
  1485. session: sqlSession,
  1486. nodeIntegration: true,
  1487. enableWebSQL: false,
  1488. contextIsolation: false
  1489. });
  1490. contents.loadURL(origin);
  1491. const [, error] = await emittedOnce(ipcMain, 'web-sql-response');
  1492. expect(error).to.equal(securityError);
  1493. });
  1494. it('when set to false does not disable indexedDB', async () => {
  1495. contents = (webContents as any).create({
  1496. session: sqlSession,
  1497. nodeIntegration: true,
  1498. enableWebSQL: false,
  1499. contextIsolation: false
  1500. });
  1501. contents.loadURL(origin);
  1502. const [, error] = await emittedOnce(ipcMain, 'web-sql-response');
  1503. expect(error).to.equal(securityError);
  1504. const dbName = 'random';
  1505. const result = await contents.executeJavaScript(`
  1506. new Promise((resolve, reject) => {
  1507. try {
  1508. let req = window.indexedDB.open('${dbName}');
  1509. req.onsuccess = (event) => {
  1510. let db = req.result;
  1511. resolve(db.name);
  1512. }
  1513. req.onerror = (event) => { resolve(event.target.code); }
  1514. } catch (e) {
  1515. resolve(e.message);
  1516. }
  1517. });
  1518. `);
  1519. expect(result).to.equal(dbName);
  1520. });
  1521. it('child webContents can override when the embedder has allowed websql', async () => {
  1522. w = new BrowserWindow({
  1523. show: false,
  1524. webPreferences: {
  1525. nodeIntegration: true,
  1526. webviewTag: true,
  1527. session: sqlSession,
  1528. contextIsolation: false
  1529. }
  1530. });
  1531. w.webContents.loadURL(origin);
  1532. const [, error] = await emittedOnce(ipcMain, 'web-sql-response');
  1533. expect(error).to.be.null();
  1534. const webviewResult = emittedOnce(ipcMain, 'web-sql-response');
  1535. await w.webContents.executeJavaScript(`
  1536. new Promise((resolve, reject) => {
  1537. const webview = new WebView();
  1538. webview.setAttribute('src', '${origin}');
  1539. webview.setAttribute('webpreferences', 'enableWebSQL=0,contextIsolation=no');
  1540. webview.setAttribute('partition', '${sqlPartition}');
  1541. webview.setAttribute('nodeIntegration', 'on');
  1542. document.body.appendChild(webview);
  1543. webview.addEventListener('dom-ready', () => resolve());
  1544. });
  1545. `);
  1546. const [, childError] = await webviewResult;
  1547. expect(childError).to.equal(securityError);
  1548. });
  1549. it('child webContents cannot override when the embedder has disallowed websql', async () => {
  1550. w = new BrowserWindow({
  1551. show: false,
  1552. webPreferences: {
  1553. nodeIntegration: true,
  1554. enableWebSQL: false,
  1555. webviewTag: true,
  1556. session: sqlSession,
  1557. contextIsolation: false
  1558. }
  1559. });
  1560. w.webContents.loadURL('data:text/html,<html></html>');
  1561. const webviewResult = emittedOnce(ipcMain, 'web-sql-response');
  1562. await w.webContents.executeJavaScript(`
  1563. new Promise((resolve, reject) => {
  1564. const webview = new WebView();
  1565. webview.setAttribute('src', '${origin}');
  1566. webview.setAttribute('webpreferences', 'enableWebSQL=1,contextIsolation=no');
  1567. webview.setAttribute('partition', '${sqlPartition}');
  1568. webview.setAttribute('nodeIntegration', 'on');
  1569. document.body.appendChild(webview);
  1570. webview.addEventListener('dom-ready', () => resolve());
  1571. });
  1572. `);
  1573. const [, childError] = await webviewResult;
  1574. expect(childError).to.equal(securityError);
  1575. });
  1576. it('child webContents can use websql when the embedder has allowed websql', async () => {
  1577. w = new BrowserWindow({
  1578. show: false,
  1579. webPreferences: {
  1580. nodeIntegration: true,
  1581. webviewTag: true,
  1582. session: sqlSession,
  1583. contextIsolation: false
  1584. }
  1585. });
  1586. w.webContents.loadURL(origin);
  1587. const [, error] = await emittedOnce(ipcMain, 'web-sql-response');
  1588. expect(error).to.be.null();
  1589. const webviewResult = emittedOnce(ipcMain, 'web-sql-response');
  1590. await w.webContents.executeJavaScript(`
  1591. new Promise((resolve, reject) => {
  1592. const webview = new WebView();
  1593. webview.setAttribute('src', '${origin}');
  1594. webview.setAttribute('webpreferences', 'enableWebSQL=1,contextIsolation=no');
  1595. webview.setAttribute('partition', '${sqlPartition}');
  1596. webview.setAttribute('nodeIntegration', 'on');
  1597. document.body.appendChild(webview);
  1598. webview.addEventListener('dom-ready', () => resolve());
  1599. });
  1600. `);
  1601. const [, childError] = await webviewResult;
  1602. expect(childError).to.be.null();
  1603. });
  1604. });
  1605. describe('DOM storage quota increase', () => {
  1606. ['localStorage', 'sessionStorage'].forEach((storageName) => {
  1607. it(`allows saving at least 40MiB in ${storageName}`, async () => {
  1608. const w = new BrowserWindow({ show: false });
  1609. w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
  1610. // Although JavaScript strings use UTF-16, the underlying
  1611. // storage provider may encode strings differently, muddling the
  1612. // translation between character and byte counts. However,
  1613. // a string of 40 * 2^20 characters will require at least 40MiB
  1614. // and presumably no more than 80MiB, a size guaranteed to
  1615. // to exceed the original 10MiB quota yet stay within the
  1616. // new 100MiB quota.
  1617. // Note that both the key name and value affect the total size.
  1618. const testKeyName = '_electronDOMStorageQuotaIncreasedTest';
  1619. const length = 40 * Math.pow(2, 20) - testKeyName.length;
  1620. await w.webContents.executeJavaScript(`
  1621. ${storageName}.setItem(${JSON.stringify(testKeyName)}, 'X'.repeat(${length}));
  1622. `);
  1623. // Wait at least one turn of the event loop to help avoid false positives
  1624. // Although not entirely necessary, the previous version of this test case
  1625. // failed to detect a real problem (perhaps related to DOM storage data caching)
  1626. // wherein calling `getItem` immediately after `setItem` would appear to work
  1627. // but then later (e.g. next tick) it would not.
  1628. await delay(1);
  1629. try {
  1630. const storedLength = await w.webContents.executeJavaScript(`${storageName}.getItem(${JSON.stringify(testKeyName)}).length`);
  1631. expect(storedLength).to.equal(length);
  1632. } finally {
  1633. await w.webContents.executeJavaScript(`${storageName}.removeItem(${JSON.stringify(testKeyName)});`);
  1634. }
  1635. });
  1636. it(`throws when attempting to use more than 128MiB in ${storageName}`, async () => {
  1637. const w = new BrowserWindow({ show: false });
  1638. w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
  1639. await expect((async () => {
  1640. const testKeyName = '_electronDOMStorageQuotaStillEnforcedTest';
  1641. const length = 128 * Math.pow(2, 20) - testKeyName.length;
  1642. try {
  1643. await w.webContents.executeJavaScript(`
  1644. ${storageName}.setItem(${JSON.stringify(testKeyName)}, 'X'.repeat(${length}));
  1645. `);
  1646. } finally {
  1647. await w.webContents.executeJavaScript(`${storageName}.removeItem(${JSON.stringify(testKeyName)});`);
  1648. }
  1649. })()).to.eventually.be.rejected();
  1650. });
  1651. });
  1652. });
  1653. describe('persistent storage', () => {
  1654. it('can be requested', async () => {
  1655. const w = new BrowserWindow({ show: false });
  1656. w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
  1657. const grantedBytes = await w.webContents.executeJavaScript(`new Promise(resolve => {
  1658. navigator.webkitPersistentStorage.requestQuota(1024 * 1024, resolve);
  1659. })`);
  1660. expect(grantedBytes).to.equal(1048576);
  1661. });
  1662. });
  1663. });
  1664. ifdescribe(features.isPDFViewerEnabled())('PDF Viewer', () => {
  1665. const pdfSource = url.format({
  1666. pathname: path.join(__dirname, 'fixtures', 'cat.pdf').replace(/\\/g, '/'),
  1667. protocol: 'file',
  1668. slashes: true
  1669. });
  1670. it('successfully loads a PDF file', async () => {
  1671. const w = new BrowserWindow({ show: false });
  1672. w.loadURL(pdfSource);
  1673. await emittedOnce(w.webContents, 'did-finish-load');
  1674. });
  1675. it('opens when loading a pdf resource as top level navigation', async () => {
  1676. const w = new BrowserWindow({ show: false });
  1677. w.loadURL(pdfSource);
  1678. const [, contents] = await emittedOnce(app, 'web-contents-created');
  1679. await emittedOnce(contents, 'did-navigate');
  1680. expect(contents.getURL()).to.equal('chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/index.html');
  1681. });
  1682. it('opens when loading a pdf resource in a iframe', async () => {
  1683. const w = new BrowserWindow({ show: false });
  1684. w.loadFile(path.join(__dirname, 'fixtures', 'pages', 'pdf-in-iframe.html'));
  1685. const [, contents] = await emittedOnce(app, 'web-contents-created');
  1686. await emittedOnce(contents, 'did-navigate');
  1687. expect(contents.getURL()).to.equal('chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/index.html');
  1688. });
  1689. });
  1690. describe('window.history', () => {
  1691. describe('window.history.pushState', () => {
  1692. it('should push state after calling history.pushState() from the same url', async () => {
  1693. const w = new BrowserWindow({ show: false });
  1694. await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
  1695. // History should have current page by now.
  1696. expect((w.webContents as any).length()).to.equal(1);
  1697. const waitCommit = emittedOnce(w.webContents, 'navigation-entry-committed');
  1698. w.webContents.executeJavaScript('window.history.pushState({}, "")');
  1699. await waitCommit;
  1700. // Initial page + pushed state.
  1701. expect((w.webContents as any).length()).to.equal(2);
  1702. });
  1703. });
  1704. describe('window.history.back', () => {
  1705. it('should not allow sandboxed iframe to modify main frame state', async () => {
  1706. const w = new BrowserWindow({ show: false });
  1707. w.loadURL('data:text/html,<iframe sandbox="allow-scripts"></iframe>');
  1708. await Promise.all([
  1709. emittedOnce(w.webContents, 'navigation-entry-committed'),
  1710. emittedOnce(w.webContents, 'did-frame-navigate'),
  1711. emittedOnce(w.webContents, 'did-navigate')
  1712. ]);
  1713. w.webContents.executeJavaScript('window.history.pushState(1, "")');
  1714. await Promise.all([
  1715. emittedOnce(w.webContents, 'navigation-entry-committed'),
  1716. emittedOnce(w.webContents, 'did-navigate-in-page')
  1717. ]);
  1718. (w.webContents as any).once('navigation-entry-committed', () => {
  1719. expect.fail('Unexpected navigation-entry-committed');
  1720. });
  1721. w.webContents.once('did-navigate-in-page', () => {
  1722. expect.fail('Unexpected did-navigate-in-page');
  1723. });
  1724. await w.webContents.mainFrame.frames[0].executeJavaScript('window.history.back()');
  1725. expect(await w.webContents.executeJavaScript('window.history.state')).to.equal(1);
  1726. expect((w.webContents as any).getActiveIndex()).to.equal(1);
  1727. });
  1728. });
  1729. });
  1730. describe('chrome://media-internals', () => {
  1731. it('loads the page successfully', async () => {
  1732. const w = new BrowserWindow({ show: false });
  1733. w.loadURL('chrome://media-internals');
  1734. const pageExists = await w.webContents.executeJavaScript(
  1735. "window.hasOwnProperty('chrome') && window.chrome.hasOwnProperty('send')"
  1736. );
  1737. expect(pageExists).to.be.true();
  1738. });
  1739. });
  1740. describe('chrome://webrtc-internals', () => {
  1741. it('loads the page successfully', async () => {
  1742. const w = new BrowserWindow({ show: false });
  1743. w.loadURL('chrome://webrtc-internals');
  1744. const pageExists = await w.webContents.executeJavaScript(
  1745. "window.hasOwnProperty('chrome') && window.chrome.hasOwnProperty('send')"
  1746. );
  1747. expect(pageExists).to.be.true();
  1748. });
  1749. });
  1750. describe('document.hasFocus', () => {
  1751. it('has correct value when multiple windows are opened', async () => {
  1752. const w1 = new BrowserWindow({ show: true });
  1753. const w2 = new BrowserWindow({ show: true });
  1754. const w3 = new BrowserWindow({ show: false });
  1755. await w1.loadFile(path.join(__dirname, 'fixtures', 'blank.html'));
  1756. await w2.loadFile(path.join(__dirname, 'fixtures', 'blank.html'));
  1757. await w3.loadFile(path.join(__dirname, 'fixtures', 'blank.html'));
  1758. expect(webContents.getFocusedWebContents().id).to.equal(w2.webContents.id);
  1759. let focus = false;
  1760. focus = await w1.webContents.executeJavaScript(
  1761. 'document.hasFocus()'
  1762. );
  1763. expect(focus).to.be.false();
  1764. focus = await w2.webContents.executeJavaScript(
  1765. 'document.hasFocus()'
  1766. );
  1767. expect(focus).to.be.true();
  1768. focus = await w3.webContents.executeJavaScript(
  1769. 'document.hasFocus()'
  1770. );
  1771. expect(focus).to.be.false();
  1772. });
  1773. });
  1774. describe('navigator.userAgentData', () => {
  1775. // These tests are done on an http server because navigator.userAgentData
  1776. // requires a secure context.
  1777. let server: http.Server;
  1778. let serverUrl: string;
  1779. before(async () => {
  1780. server = http.createServer((req, res) => {
  1781. res.setHeader('Content-Type', 'text/html');
  1782. res.end('');
  1783. });
  1784. await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
  1785. serverUrl = `http://localhost:${(server.address() as any).port}`;
  1786. });
  1787. after(() => {
  1788. server.close();
  1789. });
  1790. describe('is not empty', () => {
  1791. it('by default', async () => {
  1792. const w = new BrowserWindow({ show: false });
  1793. await w.loadURL(serverUrl);
  1794. const platform = await w.webContents.executeJavaScript('navigator.userAgentData.platform');
  1795. expect(platform).not.to.be.empty();
  1796. });
  1797. it('when there is a session-wide UA override', async () => {
  1798. const ses = session.fromPartition(`${Math.random()}`);
  1799. ses.setUserAgent('foobar');
  1800. const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
  1801. await w.loadURL(serverUrl);
  1802. const platform = await w.webContents.executeJavaScript('navigator.userAgentData.platform');
  1803. expect(platform).not.to.be.empty();
  1804. });
  1805. it('when there is a WebContents-specific UA override', async () => {
  1806. const w = new BrowserWindow({ show: false });
  1807. w.webContents.setUserAgent('foo');
  1808. await w.loadURL(serverUrl);
  1809. const platform = await w.webContents.executeJavaScript('navigator.userAgentData.platform');
  1810. expect(platform).not.to.be.empty();
  1811. });
  1812. it('when there is a WebContents-specific UA override at load time', async () => {
  1813. const w = new BrowserWindow({ show: false });
  1814. await w.loadURL(serverUrl, {
  1815. userAgent: 'foo'
  1816. });
  1817. const platform = await w.webContents.executeJavaScript('navigator.userAgentData.platform');
  1818. expect(platform).not.to.be.empty();
  1819. });
  1820. });
  1821. describe('brand list', () => {
  1822. it('contains chromium', async () => {
  1823. const w = new BrowserWindow({ show: false });
  1824. await w.loadURL(serverUrl);
  1825. const brands = await w.webContents.executeJavaScript('navigator.userAgentData.brands');
  1826. expect(brands.map((b: any) => b.brand)).to.include('Chromium');
  1827. });
  1828. });
  1829. });
  1830. describe('Badging API', () => {
  1831. it('does not crash', async () => {
  1832. const w = new BrowserWindow({ show: false });
  1833. await w.loadURL(`file://${fixturesPath}/pages/blank.html`);
  1834. await w.webContents.executeJavaScript('navigator.setAppBadge(42)');
  1835. await w.webContents.executeJavaScript('navigator.setAppBadge()');
  1836. await w.webContents.executeJavaScript('navigator.clearAppBadge()');
  1837. });
  1838. });
  1839. describe('navigator.webkitGetUserMedia', () => {
  1840. it('calls its callbacks', async () => {
  1841. const w = new BrowserWindow({ show: false });
  1842. await w.loadURL(`file://${fixturesPath}/pages/blank.html`);
  1843. await w.webContents.executeJavaScript(`new Promise((resolve) => {
  1844. navigator.webkitGetUserMedia({
  1845. audio: true,
  1846. video: false
  1847. }, () => resolve(),
  1848. () => resolve());
  1849. })`);
  1850. });
  1851. });
  1852. describe('navigator.language', () => {
  1853. it('should not be empty', async () => {
  1854. const w = new BrowserWindow({ show: false });
  1855. await w.loadURL('about:blank');
  1856. expect(await w.webContents.executeJavaScript('navigator.language')).to.not.equal('');
  1857. });
  1858. });
  1859. describe('heap snapshot', () => {
  1860. it('does not crash', async () => {
  1861. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  1862. w.loadURL('about:blank');
  1863. await w.webContents.executeJavaScript('process._linkedBinding(\'electron_common_v8_util\').takeHeapSnapshot()');
  1864. });
  1865. });
  1866. ifdescribe(process.platform !== 'win32' && process.platform !== 'linux')('webgl', () => {
  1867. it('can be gotten as context in canvas', async () => {
  1868. const w = new BrowserWindow({ show: false });
  1869. w.loadURL('about:blank');
  1870. await w.loadURL(`file://${fixturesPath}/pages/blank.html`);
  1871. const canWebglContextBeCreated = await w.webContents.executeJavaScript(`
  1872. document.createElement('canvas').getContext('webgl') != null;
  1873. `);
  1874. expect(canWebglContextBeCreated).to.be.true();
  1875. });
  1876. });
  1877. describe('iframe', () => {
  1878. it('does not have node integration', async () => {
  1879. const w = new BrowserWindow({ show: false });
  1880. await w.loadURL(`file://${fixturesPath}/pages/blank.html`);
  1881. const result = await w.webContents.executeJavaScript(`
  1882. const iframe = document.createElement('iframe')
  1883. iframe.src = './set-global.html';
  1884. document.body.appendChild(iframe);
  1885. new Promise(resolve => iframe.onload = e => resolve(iframe.contentWindow.test))
  1886. `);
  1887. expect(result).to.equal('undefined undefined undefined');
  1888. });
  1889. });
  1890. describe('websockets', () => {
  1891. it('has user agent', async () => {
  1892. const server = http.createServer();
  1893. await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
  1894. const port = (server.address() as AddressInfo).port;
  1895. const wss = new ws.Server({ server: server });
  1896. const finished = new Promise<string | undefined>((resolve, reject) => {
  1897. wss.on('error', reject);
  1898. wss.on('connection', (ws, upgradeReq) => {
  1899. resolve(upgradeReq.headers['user-agent']);
  1900. });
  1901. });
  1902. const w = new BrowserWindow({ show: false });
  1903. w.loadURL('about:blank');
  1904. w.webContents.executeJavaScript(`
  1905. new WebSocket('ws://127.0.0.1:${port}');
  1906. `);
  1907. expect(await finished).to.include('Electron');
  1908. });
  1909. });
  1910. describe('fetch', () => {
  1911. it('does not crash', async () => {
  1912. const server = http.createServer((req, res) => {
  1913. res.end('test');
  1914. });
  1915. defer(() => server.close());
  1916. await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
  1917. const port = (server.address() as AddressInfo).port;
  1918. const w = new BrowserWindow({ show: false });
  1919. w.loadURL(`file://${fixturesPath}/pages/blank.html`);
  1920. const x = await w.webContents.executeJavaScript(`
  1921. fetch('http://127.0.0.1:${port}').then((res) => res.body.getReader())
  1922. .then((reader) => {
  1923. return reader.read().then((r) => {
  1924. reader.cancel();
  1925. return r.value;
  1926. });
  1927. })
  1928. `);
  1929. expect(x).to.deep.equal(new Uint8Array([116, 101, 115, 116]));
  1930. });
  1931. });
  1932. describe('Promise', () => {
  1933. before(() => {
  1934. ipcMain.handle('ping', (e, arg) => arg);
  1935. });
  1936. after(() => {
  1937. ipcMain.removeHandler('ping');
  1938. });
  1939. itremote('resolves correctly in Node.js calls', async () => {
  1940. await new Promise<void>((resolve, reject) => {
  1941. class XElement extends HTMLElement {}
  1942. customElements.define('x-element', XElement);
  1943. setImmediate(() => {
  1944. let called = false;
  1945. Promise.resolve().then(() => {
  1946. if (called) resolve();
  1947. else reject(new Error('wrong sequence'));
  1948. });
  1949. document.createElement('x-element');
  1950. called = true;
  1951. });
  1952. });
  1953. });
  1954. itremote('resolves correctly in Electron calls', async () => {
  1955. await new Promise<void>((resolve, reject) => {
  1956. class YElement extends HTMLElement {}
  1957. customElements.define('y-element', YElement);
  1958. require('electron').ipcRenderer.invoke('ping').then(() => {
  1959. let called = false;
  1960. Promise.resolve().then(() => {
  1961. if (called) resolve();
  1962. else reject(new Error('wrong sequence'));
  1963. });
  1964. document.createElement('y-element');
  1965. called = true;
  1966. });
  1967. });
  1968. });
  1969. });
  1970. describe('synchronous prompts', () => {
  1971. describe('window.alert(message, title)', () => {
  1972. itremote('throws an exception when the arguments cannot be converted to strings', () => {
  1973. expect(() => {
  1974. window.alert({ toString: null });
  1975. }).to.throw('Cannot convert object to primitive value');
  1976. });
  1977. });
  1978. describe('window.confirm(message, title)', () => {
  1979. itremote('throws an exception when the arguments cannot be converted to strings', () => {
  1980. expect(() => {
  1981. (window.confirm as any)({ toString: null }, 'title');
  1982. }).to.throw('Cannot convert object to primitive value');
  1983. });
  1984. });
  1985. });
  1986. describe('window.history', () => {
  1987. describe('window.history.go(offset)', () => {
  1988. itremote('throws an exception when the argument cannot be converted to a string', () => {
  1989. expect(() => {
  1990. (window.history.go as any)({ toString: null });
  1991. }).to.throw('Cannot convert object to primitive value');
  1992. });
  1993. });
  1994. });
  1995. describe('console functions', () => {
  1996. itremote('should exist', () => {
  1997. expect(console.log, 'log').to.be.a('function');
  1998. expect(console.error, 'error').to.be.a('function');
  1999. expect(console.warn, 'warn').to.be.a('function');
  2000. expect(console.info, 'info').to.be.a('function');
  2001. expect(console.debug, 'debug').to.be.a('function');
  2002. expect(console.trace, 'trace').to.be.a('function');
  2003. expect(console.time, 'time').to.be.a('function');
  2004. expect(console.timeEnd, 'timeEnd').to.be.a('function');
  2005. });
  2006. });
  2007. ifdescribe(features.isTtsEnabled())('SpeechSynthesis', () => {
  2008. before(function () {
  2009. // TODO(nornagon): this is broken on CI, it triggers:
  2010. // [FATAL:speech_synthesis.mojom-shared.h(237)] The outgoing message will
  2011. // trigger VALIDATION_ERROR_UNEXPECTED_NULL_POINTER at the receiving side
  2012. // (null text in SpeechSynthesisUtterance struct).
  2013. this.skip();
  2014. });
  2015. itremote('should emit lifecycle events', async () => {
  2016. const sentence = `long sentence which will take at least a few seconds to
  2017. utter so that it's possible to pause and resume before the end`;
  2018. const utter = new SpeechSynthesisUtterance(sentence);
  2019. // Create a dummy utterance so that speech synthesis state
  2020. // is initialized for later calls.
  2021. speechSynthesis.speak(new SpeechSynthesisUtterance());
  2022. speechSynthesis.cancel();
  2023. speechSynthesis.speak(utter);
  2024. // paused state after speak()
  2025. expect(speechSynthesis.paused).to.be.false();
  2026. await new Promise((resolve) => { utter.onstart = resolve; });
  2027. // paused state after start event
  2028. expect(speechSynthesis.paused).to.be.false();
  2029. speechSynthesis.pause();
  2030. // paused state changes async, right before the pause event
  2031. expect(speechSynthesis.paused).to.be.false();
  2032. await new Promise((resolve) => { utter.onpause = resolve; });
  2033. expect(speechSynthesis.paused).to.be.true();
  2034. speechSynthesis.resume();
  2035. await new Promise((resolve) => { utter.onresume = resolve; });
  2036. // paused state after resume event
  2037. expect(speechSynthesis.paused).to.be.false();
  2038. await new Promise((resolve) => { utter.onend = resolve; });
  2039. });
  2040. });
  2041. });
  2042. describe('font fallback', () => {
  2043. async function getRenderedFonts (html: string) {
  2044. const w = new BrowserWindow({ show: false });
  2045. try {
  2046. await w.loadURL(`data:text/html,${html}`);
  2047. w.webContents.debugger.attach();
  2048. const sendCommand = (method: string, commandParams?: any) => w.webContents.debugger.sendCommand(method, commandParams);
  2049. const { nodeId } = (await sendCommand('DOM.getDocument')).root.children[0];
  2050. await sendCommand('CSS.enable');
  2051. const { fonts } = await sendCommand('CSS.getPlatformFontsForNode', { nodeId });
  2052. return fonts;
  2053. } finally {
  2054. w.close();
  2055. }
  2056. }
  2057. it('should use Helvetica for sans-serif on Mac, and Arial on Windows and Linux', async () => {
  2058. const html = '<body style="font-family: sans-serif">test</body>';
  2059. const fonts = await getRenderedFonts(html);
  2060. expect(fonts).to.be.an('array');
  2061. expect(fonts).to.have.length(1);
  2062. if (process.platform === 'win32') {
  2063. expect(fonts[0].familyName).to.equal('Arial');
  2064. } else if (process.platform === 'darwin') {
  2065. expect(fonts[0].familyName).to.equal('Helvetica');
  2066. } else if (process.platform === 'linux') {
  2067. expect(fonts[0].familyName).to.equal('DejaVu Sans');
  2068. } // I think this depends on the distro? We don't specify a default.
  2069. });
  2070. ifit(process.platform !== 'linux')('should fall back to Japanese font for sans-serif Japanese script', async function () {
  2071. const html = `
  2072. <html lang="ja-JP">
  2073. <head>
  2074. <meta charset="utf-8" />
  2075. </head>
  2076. <body style="font-family: sans-serif">test 智史</body>
  2077. </html>
  2078. `;
  2079. const fonts = await getRenderedFonts(html);
  2080. expect(fonts).to.be.an('array');
  2081. expect(fonts).to.have.length(1);
  2082. if (process.platform === 'win32') { expect(fonts[0].familyName).to.be.oneOf(['Meiryo', 'Yu Gothic']); } else if (process.platform === 'darwin') { expect(fonts[0].familyName).to.equal('Hiragino Kaku Gothic ProN'); }
  2083. });
  2084. });
  2085. describe('iframe using HTML fullscreen API while window is OS-fullscreened', () => {
  2086. const fullscreenChildHtml = promisify(fs.readFile)(
  2087. path.join(fixturesPath, 'pages', 'fullscreen-oopif.html')
  2088. );
  2089. let w: BrowserWindow, server: http.Server;
  2090. before(() => {
  2091. server = http.createServer(async (_req, res) => {
  2092. res.writeHead(200, { 'Content-Type': 'text/html' });
  2093. res.write(await fullscreenChildHtml);
  2094. res.end();
  2095. });
  2096. server.listen(8989, '127.0.0.1');
  2097. });
  2098. beforeEach(() => {
  2099. w = new BrowserWindow({
  2100. show: true,
  2101. fullscreen: true,
  2102. webPreferences: {
  2103. nodeIntegration: true,
  2104. nodeIntegrationInSubFrames: true,
  2105. contextIsolation: false
  2106. }
  2107. });
  2108. });
  2109. afterEach(async () => {
  2110. await closeAllWindows();
  2111. (w as any) = null;
  2112. server.close();
  2113. });
  2114. ifit(process.platform !== 'darwin')('can fullscreen from out-of-process iframes (non-macOS)', async () => {
  2115. const fullscreenChange = emittedOnce(ipcMain, 'fullscreenChange');
  2116. const html =
  2117. '<iframe style="width: 0" frameborder=0 src="http://localhost:8989" allowfullscreen></iframe>';
  2118. w.loadURL(`data:text/html,${html}`);
  2119. await fullscreenChange;
  2120. const fullscreenWidth = await w.webContents.executeJavaScript(
  2121. "document.querySelector('iframe').offsetWidth"
  2122. );
  2123. expect(fullscreenWidth > 0).to.be.true();
  2124. await w.webContents.executeJavaScript(
  2125. "document.querySelector('iframe').contentWindow.postMessage('exitFullscreen', '*')"
  2126. );
  2127. await delay(500);
  2128. const width = await w.webContents.executeJavaScript(
  2129. "document.querySelector('iframe').offsetWidth"
  2130. );
  2131. expect(width).to.equal(0);
  2132. });
  2133. ifit(process.platform === 'darwin')('can fullscreen from out-of-process iframes (macOS)', async () => {
  2134. await emittedOnce(w, 'enter-full-screen');
  2135. const fullscreenChange = emittedOnce(ipcMain, 'fullscreenChange');
  2136. const html =
  2137. '<iframe style="width: 0" frameborder=0 src="http://localhost:8989" allowfullscreen></iframe>';
  2138. w.loadURL(`data:text/html,${html}`);
  2139. await fullscreenChange;
  2140. const fullscreenWidth = await w.webContents.executeJavaScript(
  2141. "document.querySelector('iframe').offsetWidth"
  2142. );
  2143. expect(fullscreenWidth > 0).to.be.true();
  2144. await w.webContents.executeJavaScript(
  2145. "document.querySelector('iframe').contentWindow.postMessage('exitFullscreen', '*')"
  2146. );
  2147. await emittedOnce(w.webContents, 'leave-html-full-screen');
  2148. const width = await w.webContents.executeJavaScript(
  2149. "document.querySelector('iframe').offsetWidth"
  2150. );
  2151. expect(width).to.equal(0);
  2152. w.setFullScreen(false);
  2153. await emittedOnce(w, 'leave-full-screen');
  2154. });
  2155. // TODO(jkleinsc) fix this flaky test on WOA
  2156. ifit(process.platform !== 'win32' || process.arch !== 'arm64')('can fullscreen from in-process iframes', async () => {
  2157. if (process.platform === 'darwin') await emittedOnce(w, 'enter-full-screen');
  2158. const fullscreenChange = emittedOnce(ipcMain, 'fullscreenChange');
  2159. w.loadFile(path.join(fixturesPath, 'pages', 'fullscreen-ipif.html'));
  2160. await fullscreenChange;
  2161. const fullscreenWidth = await w.webContents.executeJavaScript(
  2162. "document.querySelector('iframe').offsetWidth"
  2163. );
  2164. expect(fullscreenWidth > 0).to.true();
  2165. await w.webContents.executeJavaScript('document.exitFullscreen()');
  2166. const width = await w.webContents.executeJavaScript(
  2167. "document.querySelector('iframe').offsetWidth"
  2168. );
  2169. expect(width).to.equal(0);
  2170. });
  2171. });
  2172. describe('navigator.serial', () => {
  2173. let w: BrowserWindow;
  2174. before(async () => {
  2175. w = new BrowserWindow({
  2176. show: false
  2177. });
  2178. await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
  2179. });
  2180. const getPorts: any = () => {
  2181. return w.webContents.executeJavaScript(`
  2182. navigator.serial.requestPort().then(port => port.toString()).catch(err => err.toString());
  2183. `, true);
  2184. };
  2185. after(closeAllWindows);
  2186. afterEach(() => {
  2187. session.defaultSession.setPermissionCheckHandler(null);
  2188. session.defaultSession.removeAllListeners('select-serial-port');
  2189. });
  2190. it('does not return a port if select-serial-port event is not defined', async () => {
  2191. w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
  2192. const port = await getPorts();
  2193. expect(port).to.equal('NotFoundError: No port selected by the user.');
  2194. });
  2195. it('does not return a port when permission denied', async () => {
  2196. w.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => {
  2197. callback(portList[0].portId);
  2198. });
  2199. session.defaultSession.setPermissionCheckHandler(() => false);
  2200. const port = await getPorts();
  2201. expect(port).to.equal('NotFoundError: No port selected by the user.');
  2202. });
  2203. it('does not crash when select-serial-port is called with an invalid port', async () => {
  2204. w.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => {
  2205. callback('i-do-not-exist');
  2206. });
  2207. const port = await getPorts();
  2208. expect(port).to.equal('NotFoundError: No port selected by the user.');
  2209. });
  2210. it('returns a port when select-serial-port event is defined', async () => {
  2211. let havePorts = false;
  2212. w.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => {
  2213. if (portList.length > 0) {
  2214. havePorts = true;
  2215. callback(portList[0].portId);
  2216. } else {
  2217. callback('');
  2218. }
  2219. });
  2220. const port = await getPorts();
  2221. if (havePorts) {
  2222. expect(port).to.equal('[object SerialPort]');
  2223. } else {
  2224. expect(port).to.equal('NotFoundError: No port selected by the user.');
  2225. }
  2226. });
  2227. it('navigator.serial.getPorts() returns values', async () => {
  2228. let havePorts = false;
  2229. w.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => {
  2230. if (portList.length > 0) {
  2231. havePorts = true;
  2232. callback(portList[0].portId);
  2233. } else {
  2234. callback('');
  2235. }
  2236. });
  2237. await getPorts();
  2238. if (havePorts) {
  2239. const grantedPorts = await w.webContents.executeJavaScript('navigator.serial.getPorts()');
  2240. expect(grantedPorts).to.not.be.empty();
  2241. }
  2242. });
  2243. it('supports port.forget()', async () => {
  2244. let forgottenPortFromEvent = {};
  2245. let havePorts = false;
  2246. w.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => {
  2247. if (portList.length > 0) {
  2248. havePorts = true;
  2249. callback(portList[0].portId);
  2250. } else {
  2251. callback('');
  2252. }
  2253. });
  2254. w.webContents.session.on('serial-port-revoked', (event, details) => {
  2255. forgottenPortFromEvent = details.port;
  2256. });
  2257. await getPorts();
  2258. if (havePorts) {
  2259. const grantedPorts = await w.webContents.executeJavaScript('navigator.serial.getPorts()');
  2260. if (grantedPorts.length > 0) {
  2261. const forgottenPort = await w.webContents.executeJavaScript(`
  2262. navigator.serial.getPorts().then(async(ports) => {
  2263. const portInfo = await ports[0].getInfo();
  2264. await ports[0].forget();
  2265. if (portInfo.usbVendorId && portInfo.usbProductId) {
  2266. return {
  2267. vendorId: '' + portInfo.usbVendorId,
  2268. productId: '' + portInfo.usbProductId
  2269. }
  2270. } else {
  2271. return {};
  2272. }
  2273. })
  2274. `);
  2275. const grantedPorts2 = await w.webContents.executeJavaScript('navigator.serial.getPorts()');
  2276. expect(grantedPorts2.length).to.be.lessThan(grantedPorts.length);
  2277. if (forgottenPort.vendorId && forgottenPort.productId) {
  2278. expect(forgottenPortFromEvent).to.include(forgottenPort);
  2279. }
  2280. }
  2281. }
  2282. });
  2283. });
  2284. describe('window.getScreenDetails', () => {
  2285. let w: BrowserWindow;
  2286. before(async () => {
  2287. w = new BrowserWindow({
  2288. show: false
  2289. });
  2290. await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
  2291. });
  2292. after(closeAllWindows);
  2293. afterEach(() => {
  2294. session.defaultSession.setPermissionRequestHandler(null);
  2295. });
  2296. const getScreenDetails: any = () => {
  2297. return w.webContents.executeJavaScript('window.getScreenDetails().then(data => data.screens).catch(err => err.message)', true);
  2298. };
  2299. it('returns screens when a PermissionRequestHandler is not defined', async () => {
  2300. const screens = await getScreenDetails();
  2301. expect(screens).to.not.equal('Read permission denied.');
  2302. });
  2303. it('returns an error when permission denied', async () => {
  2304. session.defaultSession.setPermissionRequestHandler((wc, permission, callback) => {
  2305. if (permission === 'window-placement') {
  2306. callback(false);
  2307. } else {
  2308. callback(true);
  2309. }
  2310. });
  2311. const screens = await getScreenDetails();
  2312. expect(screens).to.equal('Permission denied.');
  2313. });
  2314. it('returns screens when permission is granted', async () => {
  2315. session.defaultSession.setPermissionRequestHandler((wc, permission, callback) => {
  2316. if (permission === 'window-placement') {
  2317. callback(true);
  2318. } else {
  2319. callback(false);
  2320. }
  2321. });
  2322. const screens = await getScreenDetails();
  2323. expect(screens).to.not.equal('Permission denied.');
  2324. });
  2325. });
  2326. describe('navigator.clipboard.read', () => {
  2327. let w: BrowserWindow;
  2328. before(async () => {
  2329. w = new BrowserWindow();
  2330. await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
  2331. });
  2332. const readClipboard: any = () => {
  2333. return w.webContents.executeJavaScript(`
  2334. navigator.clipboard.read().then(clipboard => clipboard.toString()).catch(err => err.message);
  2335. `, true);
  2336. };
  2337. after(closeAllWindows);
  2338. afterEach(() => {
  2339. session.defaultSession.setPermissionRequestHandler(null);
  2340. });
  2341. it('returns clipboard contents when a PermissionRequestHandler is not defined', async () => {
  2342. const clipboard = await readClipboard();
  2343. expect(clipboard).to.not.equal('Read permission denied.');
  2344. });
  2345. it('returns an error when permission denied', async () => {
  2346. session.defaultSession.setPermissionRequestHandler((wc, permission, callback) => {
  2347. if (permission === 'clipboard-read') {
  2348. callback(false);
  2349. } else {
  2350. callback(true);
  2351. }
  2352. });
  2353. const clipboard = await readClipboard();
  2354. expect(clipboard).to.equal('Read permission denied.');
  2355. });
  2356. it('returns clipboard contents when permission is granted', async () => {
  2357. session.defaultSession.setPermissionRequestHandler((wc, permission, callback) => {
  2358. if (permission === 'clipboard-read') {
  2359. callback(true);
  2360. } else {
  2361. callback(false);
  2362. }
  2363. });
  2364. const clipboard = await readClipboard();
  2365. expect(clipboard).to.not.equal('Read permission denied.');
  2366. });
  2367. });
  2368. describe('navigator.clipboard.write', () => {
  2369. let w: BrowserWindow;
  2370. before(async () => {
  2371. w = new BrowserWindow();
  2372. await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
  2373. });
  2374. const writeClipboard: any = () => {
  2375. return w.webContents.executeJavaScript(`
  2376. navigator.clipboard.writeText('Hello World!').catch(err => err.message);
  2377. `, true);
  2378. };
  2379. after(closeAllWindows);
  2380. afterEach(() => {
  2381. session.defaultSession.setPermissionRequestHandler(null);
  2382. });
  2383. it('returns clipboard contents when a PermissionRequestHandler is not defined', async () => {
  2384. const clipboard = await writeClipboard();
  2385. expect(clipboard).to.not.equal('Write permission denied.');
  2386. });
  2387. it('returns an error when permission denied', async () => {
  2388. session.defaultSession.setPermissionRequestHandler((wc, permission, callback) => {
  2389. if (permission === 'clipboard-sanitized-write') {
  2390. callback(false);
  2391. } else {
  2392. callback(true);
  2393. }
  2394. });
  2395. const clipboard = await writeClipboard();
  2396. expect(clipboard).to.equal('Write permission denied.');
  2397. });
  2398. it('returns clipboard contents when permission is granted', async () => {
  2399. session.defaultSession.setPermissionRequestHandler((wc, permission, callback) => {
  2400. if (permission === 'clipboard-sanitized-write') {
  2401. callback(true);
  2402. } else {
  2403. callback(false);
  2404. }
  2405. });
  2406. const clipboard = await writeClipboard();
  2407. expect(clipboard).to.not.equal('Write permission denied.');
  2408. });
  2409. });
  2410. ifdescribe((process.platform !== 'linux' || app.isUnityRunning()))('navigator.setAppBadge/clearAppBadge', () => {
  2411. let w: BrowserWindow;
  2412. const expectedBadgeCount = 42;
  2413. const fireAppBadgeAction: any = (action: string, value: any) => {
  2414. return w.webContents.executeJavaScript(`
  2415. navigator.${action}AppBadge(${value}).then(() => 'success').catch(err => err.message)`);
  2416. };
  2417. // For some reason on macOS changing the badge count doesn't happen right away, so wait
  2418. // until it changes.
  2419. async function waitForBadgeCount (value: number) {
  2420. let badgeCount = app.getBadgeCount();
  2421. while (badgeCount !== value) {
  2422. await new Promise(resolve => setTimeout(resolve, 10));
  2423. badgeCount = app.getBadgeCount();
  2424. }
  2425. return badgeCount;
  2426. }
  2427. describe('in the renderer', () => {
  2428. before(async () => {
  2429. w = new BrowserWindow({
  2430. show: false
  2431. });
  2432. await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
  2433. });
  2434. after(() => {
  2435. app.badgeCount = 0;
  2436. closeAllWindows();
  2437. });
  2438. it('setAppBadge can set a numerical value', async () => {
  2439. const result = await fireAppBadgeAction('set', expectedBadgeCount);
  2440. expect(result).to.equal('success');
  2441. expect(waitForBadgeCount(expectedBadgeCount)).to.eventually.equal(expectedBadgeCount);
  2442. });
  2443. it('setAppBadge can set an empty(dot) value', async () => {
  2444. const result = await fireAppBadgeAction('set');
  2445. expect(result).to.equal('success');
  2446. expect(waitForBadgeCount(0)).to.eventually.equal(0);
  2447. });
  2448. it('clearAppBadge can clear a value', async () => {
  2449. let result = await fireAppBadgeAction('set', expectedBadgeCount);
  2450. expect(result).to.equal('success');
  2451. expect(waitForBadgeCount(expectedBadgeCount)).to.eventually.equal(expectedBadgeCount);
  2452. result = await fireAppBadgeAction('clear');
  2453. expect(result).to.equal('success');
  2454. expect(waitForBadgeCount(0)).to.eventually.equal(0);
  2455. });
  2456. });
  2457. describe('in a service worker', () => {
  2458. beforeEach(async () => {
  2459. w = new BrowserWindow({
  2460. show: false,
  2461. webPreferences: {
  2462. nodeIntegration: true,
  2463. partition: 'sw-file-scheme-spec',
  2464. contextIsolation: false
  2465. }
  2466. });
  2467. });
  2468. afterEach(() => {
  2469. app.badgeCount = 0;
  2470. closeAllWindows();
  2471. });
  2472. it('setAppBadge can be called in a ServiceWorker', (done) => {
  2473. w.webContents.on('ipc-message', (event, channel, message) => {
  2474. if (channel === 'reload') {
  2475. w.webContents.reload();
  2476. } else if (channel === 'error') {
  2477. done(message);
  2478. } else if (channel === 'response') {
  2479. expect(message).to.equal('SUCCESS setting app badge');
  2480. expect(waitForBadgeCount(expectedBadgeCount)).to.eventually.equal(expectedBadgeCount);
  2481. session.fromPartition('sw-file-scheme-spec').clearStorageData({
  2482. storages: ['serviceworkers']
  2483. }).then(() => done());
  2484. }
  2485. });
  2486. w.webContents.on('crashed', () => done(new Error('WebContents crashed.')));
  2487. w.loadFile(path.join(fixturesPath, 'pages', 'service-worker', 'badge-index.html'), { search: '?setBadge' });
  2488. });
  2489. it('clearAppBadge can be called in a ServiceWorker', (done) => {
  2490. w.webContents.on('ipc-message', (event, channel, message) => {
  2491. if (channel === 'reload') {
  2492. w.webContents.reload();
  2493. } else if (channel === 'setAppBadge') {
  2494. expect(message).to.equal('SUCCESS setting app badge');
  2495. expect(waitForBadgeCount(expectedBadgeCount)).to.eventually.equal(expectedBadgeCount);
  2496. } else if (channel === 'error') {
  2497. done(message);
  2498. } else if (channel === 'response') {
  2499. expect(message).to.equal('SUCCESS clearing app badge');
  2500. expect(waitForBadgeCount(expectedBadgeCount)).to.eventually.equal(expectedBadgeCount);
  2501. session.fromPartition('sw-file-scheme-spec').clearStorageData({
  2502. storages: ['serviceworkers']
  2503. }).then(() => done());
  2504. }
  2505. });
  2506. w.webContents.on('crashed', () => done(new Error('WebContents crashed.')));
  2507. w.loadFile(path.join(fixturesPath, 'pages', 'service-worker', 'badge-index.html'), { search: '?clearBadge' });
  2508. });
  2509. });
  2510. });
  2511. describe('navigator.bluetooth', () => {
  2512. let w: BrowserWindow;
  2513. before(async () => {
  2514. w = new BrowserWindow({
  2515. show: false,
  2516. webPreferences: {
  2517. enableBlinkFeatures: 'WebBluetooth'
  2518. }
  2519. });
  2520. await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
  2521. });
  2522. after(closeAllWindows);
  2523. it('can request bluetooth devices', async () => {
  2524. const bluetooth = await w.webContents.executeJavaScript(`
  2525. navigator.bluetooth.requestDevice({ acceptAllDevices: true}).then(device => "Found a device!").catch(err => err.message);`, true);
  2526. expect(bluetooth).to.be.oneOf(['Found a device!', 'Bluetooth adapter not available.', 'User cancelled the requestDevice() chooser.']);
  2527. });
  2528. });
  2529. describe('navigator.hid', () => {
  2530. let w: BrowserWindow;
  2531. let server: http.Server;
  2532. let serverUrl: string;
  2533. before(async () => {
  2534. w = new BrowserWindow({
  2535. show: false
  2536. });
  2537. await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
  2538. server = http.createServer((req, res) => {
  2539. res.setHeader('Content-Type', 'text/html');
  2540. res.end('<body>');
  2541. });
  2542. await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
  2543. serverUrl = `http://localhost:${(server.address() as any).port}`;
  2544. });
  2545. const requestDevices: any = () => {
  2546. return w.webContents.executeJavaScript(`
  2547. navigator.hid.requestDevice({filters: []}).then(device => device.toString()).catch(err => err.toString());
  2548. `, true);
  2549. };
  2550. after(() => {
  2551. server.close();
  2552. closeAllWindows();
  2553. });
  2554. afterEach(() => {
  2555. session.defaultSession.setPermissionCheckHandler(null);
  2556. session.defaultSession.setDevicePermissionHandler(null);
  2557. session.defaultSession.removeAllListeners('select-hid-device');
  2558. });
  2559. it('does not return a device if select-hid-device event is not defined', async () => {
  2560. w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
  2561. const device = await requestDevices();
  2562. expect(device).to.equal('');
  2563. });
  2564. it('does not return a device when permission denied', async () => {
  2565. let selectFired = false;
  2566. w.webContents.session.on('select-hid-device', (event, details, callback) => {
  2567. selectFired = true;
  2568. callback();
  2569. });
  2570. session.defaultSession.setPermissionCheckHandler(() => false);
  2571. const device = await requestDevices();
  2572. expect(selectFired).to.be.false();
  2573. expect(device).to.equal('');
  2574. });
  2575. it('returns a device when select-hid-device event is defined', async () => {
  2576. let haveDevices = false;
  2577. let selectFired = false;
  2578. w.webContents.session.on('select-hid-device', (event, details, callback) => {
  2579. expect(details.frame).to.have.ownProperty('frameTreeNodeId').that.is.a('number');
  2580. selectFired = true;
  2581. if (details.deviceList.length > 0) {
  2582. haveDevices = true;
  2583. callback(details.deviceList[0].deviceId);
  2584. } else {
  2585. callback();
  2586. }
  2587. });
  2588. const device = await requestDevices();
  2589. expect(selectFired).to.be.true();
  2590. if (haveDevices) {
  2591. expect(device).to.contain('[object HIDDevice]');
  2592. } else {
  2593. expect(device).to.equal('');
  2594. }
  2595. if (haveDevices) {
  2596. // Verify that navigation will clear device permissions
  2597. const grantedDevices = await w.webContents.executeJavaScript('navigator.hid.getDevices()');
  2598. expect(grantedDevices).to.not.be.empty();
  2599. w.loadURL(serverUrl);
  2600. const [,,,,, frameProcessId, frameRoutingId] = await emittedOnce(w.webContents, 'did-frame-navigate');
  2601. const frame = webFrameMain.fromId(frameProcessId, frameRoutingId);
  2602. expect(frame).to.not.be.empty();
  2603. if (frame) {
  2604. const grantedDevicesOnNewPage = await frame.executeJavaScript('navigator.hid.getDevices()');
  2605. expect(grantedDevicesOnNewPage).to.be.empty();
  2606. }
  2607. }
  2608. });
  2609. it('returns a device when DevicePermissionHandler is defined', async () => {
  2610. let haveDevices = false;
  2611. let selectFired = false;
  2612. let gotDevicePerms = false;
  2613. w.webContents.session.on('select-hid-device', (event, details, callback) => {
  2614. selectFired = true;
  2615. if (details.deviceList.length > 0) {
  2616. const foundDevice = details.deviceList.find((device) => {
  2617. if (device.name && device.name !== '' && device.serialNumber && device.serialNumber !== '') {
  2618. haveDevices = true;
  2619. return true;
  2620. }
  2621. });
  2622. if (foundDevice) {
  2623. callback(foundDevice.deviceId);
  2624. return;
  2625. }
  2626. }
  2627. callback();
  2628. });
  2629. session.defaultSession.setDevicePermissionHandler(() => {
  2630. gotDevicePerms = true;
  2631. return true;
  2632. });
  2633. await w.webContents.executeJavaScript('navigator.hid.getDevices();', true);
  2634. const device = await requestDevices();
  2635. expect(selectFired).to.be.true();
  2636. if (haveDevices) {
  2637. expect(device).to.contain('[object HIDDevice]');
  2638. expect(gotDevicePerms).to.be.true();
  2639. } else {
  2640. expect(device).to.equal('');
  2641. }
  2642. });
  2643. it('excludes a device when a exclusionFilter is specified', async () => {
  2644. const exclusionFilters = <any>[];
  2645. let haveDevices = false;
  2646. let checkForExcludedDevice = false;
  2647. w.webContents.session.on('select-hid-device', (event, details, callback) => {
  2648. if (details.deviceList.length > 0) {
  2649. details.deviceList.find((device) => {
  2650. if (device.name && device.name !== '' && device.serialNumber && device.serialNumber !== '') {
  2651. if (checkForExcludedDevice) {
  2652. const compareDevice = {
  2653. vendorId: device.vendorId,
  2654. productId: device.productId
  2655. };
  2656. expect(compareDevice).to.not.equal(exclusionFilters[0], 'excluded device should not be returned');
  2657. } else {
  2658. haveDevices = true;
  2659. exclusionFilters.push({
  2660. vendorId: device.vendorId,
  2661. productId: device.productId
  2662. });
  2663. return true;
  2664. }
  2665. }
  2666. });
  2667. }
  2668. callback();
  2669. });
  2670. await requestDevices();
  2671. if (haveDevices) {
  2672. // We have devices to exclude, so check if exclusionFilters work
  2673. checkForExcludedDevice = true;
  2674. await w.webContents.executeJavaScript(`
  2675. navigator.hid.requestDevice({filters: [], exclusionFilters: ${JSON.stringify(exclusionFilters)}}).then(device => device.toString()).catch(err => err.toString());
  2676. `, true);
  2677. }
  2678. });
  2679. it('supports device.forget()', async () => {
  2680. let deletedDeviceFromEvent;
  2681. let haveDevices = false;
  2682. w.webContents.session.on('select-hid-device', (event, details, callback) => {
  2683. if (details.deviceList.length > 0) {
  2684. haveDevices = true;
  2685. callback(details.deviceList[0].deviceId);
  2686. } else {
  2687. callback();
  2688. }
  2689. });
  2690. w.webContents.session.on('hid-device-revoked', (event, details) => {
  2691. deletedDeviceFromEvent = details.device;
  2692. });
  2693. await requestDevices();
  2694. if (haveDevices) {
  2695. const grantedDevices = await w.webContents.executeJavaScript('navigator.hid.getDevices()');
  2696. if (grantedDevices.length > 0) {
  2697. const deletedDevice = await w.webContents.executeJavaScript(`
  2698. navigator.hid.getDevices().then(devices => {
  2699. devices[0].forget();
  2700. return {
  2701. vendorId: devices[0].vendorId,
  2702. productId: devices[0].productId,
  2703. name: devices[0].productName
  2704. }
  2705. })
  2706. `);
  2707. const grantedDevices2 = await w.webContents.executeJavaScript('navigator.hid.getDevices()');
  2708. expect(grantedDevices2.length).to.be.lessThan(grantedDevices.length);
  2709. if (deletedDevice.name !== '' && deletedDevice.productId && deletedDevice.vendorId) {
  2710. expect(deletedDeviceFromEvent).to.include(deletedDevice);
  2711. }
  2712. }
  2713. }
  2714. });
  2715. });