chromium-spec.ts 128 KB

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