extensions-spec.ts 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850
  1. import { expect } from 'chai';
  2. import { app, session, BrowserWindow, ipcMain, WebContents, Extension } from 'electron';
  3. import { closeAllWindows, closeWindow } from './window-helpers';
  4. import * as http from 'http';
  5. import { AddressInfo } from 'net';
  6. import * as path from 'path';
  7. import * as fs from 'fs';
  8. import { ifdescribe } from './spec-helpers';
  9. import { emittedOnce, emittedNTimes } from './events-helpers';
  10. const fixtures = path.join(__dirname, 'fixtures');
  11. ifdescribe(process.electronBinding('features').isExtensionsEnabled())('chrome extensions', () => {
  12. // NB. extensions are only allowed on http://, https:// and ftp:// (!) urls by default.
  13. let server: http.Server;
  14. let url: string;
  15. before(async () => {
  16. server = http.createServer((req, res) => {
  17. if (req.url === '/cors') {
  18. res.setHeader('Access-Control-Allow-Origin', 'http://example.com');
  19. }
  20. res.end();
  21. });
  22. await new Promise(resolve => server.listen(0, '127.0.0.1', () => {
  23. url = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
  24. resolve();
  25. }));
  26. });
  27. after(() => {
  28. server.close();
  29. });
  30. afterEach(closeAllWindows);
  31. afterEach(() => {
  32. session.defaultSession.getAllExtensions().forEach((e: any) => {
  33. session.defaultSession.removeExtension(e.id);
  34. });
  35. });
  36. it('does not crash when using chrome.management', async () => {
  37. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  38. const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, sandbox: true } });
  39. w.loadURL('about:blank');
  40. await emittedOnce(w.webContents, 'dom-ready');
  41. await customSession.loadExtension(path.join(fixtures, 'extensions', 'persistent-background-page'));
  42. const args: any = await emittedOnce(app, 'web-contents-created');
  43. const wc: Electron.WebContents = args[1];
  44. await expect(wc.executeJavaScript(`
  45. (() => {
  46. return new Promise((resolve) => {
  47. chrome.management.getSelf((info) => {
  48. resolve(info);
  49. });
  50. })
  51. })();
  52. `)).to.eventually.have.property('id');
  53. });
  54. it('can open WebSQLDatabase in a background page', async () => {
  55. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  56. const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, sandbox: true } });
  57. w.loadURL('about:blank');
  58. await emittedOnce(w.webContents, 'dom-ready');
  59. await customSession.loadExtension(path.join(fixtures, 'extensions', 'persistent-background-page'));
  60. const args: any = await emittedOnce(app, 'web-contents-created');
  61. const wc: Electron.WebContents = args[1];
  62. await expect(wc.executeJavaScript('(()=>{try{openDatabase("t", "1.0", "test", 2e5);return true;}catch(e){throw e}})()')).to.not.be.rejected();
  63. });
  64. function fetch (contents: WebContents, url: string) {
  65. return contents.executeJavaScript(`fetch(${JSON.stringify(url)})`);
  66. }
  67. it('bypasses CORS in requests made from extensions', async () => {
  68. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  69. const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, sandbox: true } });
  70. const extension = await customSession.loadExtension(path.join(fixtures, 'extensions', 'ui-page'));
  71. w.loadURL(`${extension.url}bare-page.html`);
  72. await emittedOnce(w.webContents, 'dom-ready');
  73. await expect(fetch(w.webContents, `${url}/cors`)).to.not.be.rejectedWith(TypeError);
  74. });
  75. it('loads an extension', async () => {
  76. // NB. we have to use a persist: session (i.e. non-OTR) because the
  77. // extension registry is redirected to the main session. so installing an
  78. // extension in an in-memory session results in it being installed in the
  79. // default session.
  80. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  81. await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
  82. const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
  83. await w.loadURL(url);
  84. const bg = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor');
  85. expect(bg).to.equal('red');
  86. });
  87. it('serializes a loaded extension', async () => {
  88. const extensionPath = path.join(fixtures, 'extensions', 'red-bg');
  89. const manifest = JSON.parse(fs.readFileSync(path.join(extensionPath, 'manifest.json'), 'utf-8'));
  90. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  91. const extension = await customSession.loadExtension(extensionPath);
  92. expect(extension.id).to.be.a('string');
  93. expect(extension.name).to.be.a('string');
  94. expect(extension.path).to.be.a('string');
  95. expect(extension.version).to.be.a('string');
  96. expect(extension.url).to.be.a('string');
  97. expect(extension.manifest).to.deep.equal(manifest);
  98. });
  99. it('removes an extension', async () => {
  100. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  101. const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
  102. {
  103. const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
  104. await w.loadURL(url);
  105. const bg = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor');
  106. expect(bg).to.equal('red');
  107. }
  108. customSession.removeExtension(id);
  109. {
  110. const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
  111. await w.loadURL(url);
  112. const bg = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor');
  113. expect(bg).to.equal('');
  114. }
  115. });
  116. it('lists loaded extensions in getAllExtensions', async () => {
  117. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  118. const e = await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
  119. expect(customSession.getAllExtensions()).to.deep.equal([e]);
  120. customSession.removeExtension(e.id);
  121. expect(customSession.getAllExtensions()).to.deep.equal([]);
  122. });
  123. it('gets an extension by id', async () => {
  124. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  125. const e = await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
  126. expect(customSession.getExtension(e.id)).to.deep.equal(e);
  127. });
  128. it('confines an extension to the session it was loaded in', async () => {
  129. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  130. await customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'));
  131. const w = new BrowserWindow({ show: false }); // not in the session
  132. await w.loadURL(url);
  133. const bg = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor');
  134. expect(bg).to.equal('');
  135. });
  136. it('loading an extension in a temporary session throws an error', async () => {
  137. const customSession = session.fromPartition(require('uuid').v4());
  138. await expect(customSession.loadExtension(path.join(fixtures, 'extensions', 'red-bg'))).to.eventually.be.rejectedWith('Extensions cannot be loaded in a temporary session');
  139. });
  140. describe('chrome.i18n', () => {
  141. let w: BrowserWindow;
  142. let extension: Extension;
  143. const exec = async (name: string) => {
  144. const p = emittedOnce(ipcMain, 'success');
  145. await w.webContents.executeJavaScript(`exec('${name}')`);
  146. const [, result] = await p;
  147. return result;
  148. };
  149. beforeEach(async () => {
  150. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  151. extension = await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-i18n'));
  152. w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } });
  153. await w.loadURL(url);
  154. });
  155. it('getAcceptLanguages()', async () => {
  156. const result = await exec('getAcceptLanguages');
  157. expect(result).to.be.an('array').and.deep.equal(['en-US']);
  158. });
  159. it('getMessage()', async () => {
  160. const result = await exec('getMessage');
  161. expect(result.id).to.be.a('string').and.equal(extension.id);
  162. expect(result.name).to.be.a('string').and.equal('chrome-i18n');
  163. });
  164. });
  165. describe('chrome.runtime', () => {
  166. let content: any;
  167. before(async () => {
  168. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  169. customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-runtime'));
  170. const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
  171. try {
  172. await w.loadURL(url);
  173. content = JSON.parse(await w.webContents.executeJavaScript('document.documentElement.textContent'));
  174. expect(content).to.be.an('object');
  175. } finally {
  176. w.destroy();
  177. }
  178. });
  179. it('getManifest()', () => {
  180. expect(content.manifest).to.be.an('object').with.property('name', 'chrome-runtime');
  181. });
  182. it('id', () => {
  183. expect(content.id).to.be.a('string').with.lengthOf(32);
  184. });
  185. it('getURL()', () => {
  186. expect(content.url).to.be.a('string').and.match(/^chrome-extension:\/\/.*main.js$/);
  187. });
  188. });
  189. describe('chrome.storage', () => {
  190. it('stores and retrieves a key', async () => {
  191. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  192. await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-storage'));
  193. const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } });
  194. try {
  195. const p = emittedOnce(ipcMain, 'storage-success');
  196. await w.loadURL(url);
  197. const [, v] = await p;
  198. expect(v).to.equal('value');
  199. } finally {
  200. w.destroy();
  201. }
  202. });
  203. });
  204. describe('chrome.tabs', () => {
  205. it('executeScript', async () => {
  206. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  207. await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-api'));
  208. const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } });
  209. await w.loadURL(url);
  210. const message = { method: 'executeScript', args: ['1 + 2'] };
  211. w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
  212. const [,, responseString] = await emittedOnce(w.webContents, 'console-message');
  213. const response = JSON.parse(responseString);
  214. expect(response).to.equal(3);
  215. });
  216. it('connect', async () => {
  217. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  218. await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-api'));
  219. const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } });
  220. await w.loadURL(url);
  221. const portName = require('uuid').v4();
  222. const message = { method: 'connectTab', args: [portName] };
  223. w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
  224. const [,, responseString] = await emittedOnce(w.webContents, 'console-message');
  225. const response = responseString.split(',');
  226. expect(response[0]).to.equal(portName);
  227. expect(response[1]).to.equal('howdy');
  228. });
  229. it('sendMessage receives the response', async function () {
  230. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  231. await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-api'));
  232. const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } });
  233. await w.loadURL(url);
  234. const message = { method: 'sendMessage', args: ['Hello World!'] };
  235. w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
  236. const [,, responseString] = await emittedOnce(w.webContents, 'console-message');
  237. const response = JSON.parse(responseString);
  238. expect(response.message).to.equal('Hello World!');
  239. expect(response.tabId).to.equal(w.webContents.id);
  240. });
  241. });
  242. describe('background pages', () => {
  243. it('loads a lazy background page when sending a message', async () => {
  244. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  245. await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page'));
  246. const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } });
  247. try {
  248. w.loadURL(url);
  249. const [, resp] = await emittedOnce(ipcMain, 'bg-page-message-response');
  250. expect(resp.message).to.deep.equal({ some: 'message' });
  251. expect(resp.sender.id).to.be.a('string');
  252. expect(resp.sender.origin).to.equal(url);
  253. expect(resp.sender.url).to.equal(url + '/');
  254. } finally {
  255. w.destroy();
  256. }
  257. });
  258. it('can use extension.getBackgroundPage from a ui page', async () => {
  259. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  260. const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page'));
  261. const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
  262. await w.loadURL(`chrome-extension://${id}/page-get-background.html`);
  263. const receivedMessage = await w.webContents.executeJavaScript('window.completionPromise');
  264. expect(receivedMessage).to.deep.equal({ some: 'message' });
  265. });
  266. it('can use extension.getBackgroundPage from a ui page', async () => {
  267. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  268. const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page'));
  269. const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
  270. await w.loadURL(`chrome-extension://${id}/page-get-background.html`);
  271. const receivedMessage = await w.webContents.executeJavaScript('window.completionPromise');
  272. expect(receivedMessage).to.deep.equal({ some: 'message' });
  273. });
  274. it('can use runtime.getBackgroundPage from a ui page', async () => {
  275. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  276. const { id } = await customSession.loadExtension(path.join(fixtures, 'extensions', 'lazy-background-page'));
  277. const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
  278. await w.loadURL(`chrome-extension://${id}/page-runtime-get-background.html`);
  279. const receivedMessage = await w.webContents.executeJavaScript('window.completionPromise');
  280. expect(receivedMessage).to.deep.equal({ some: 'message' });
  281. });
  282. it('has session in background page', async () => {
  283. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  284. await customSession.loadExtension(path.join(fixtures, 'extensions', 'persistent-background-page'));
  285. const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
  286. const promise = emittedOnce(app, 'web-contents-created');
  287. await w.loadURL('about:blank');
  288. const [, bgPageContents] = await promise;
  289. expect(bgPageContents.session).to.not.equal(undefined);
  290. });
  291. it('can open devtools of background page', async () => {
  292. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  293. await customSession.loadExtension(path.join(fixtures, 'extensions', 'persistent-background-page'));
  294. const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
  295. const promise = emittedOnce(app, 'web-contents-created');
  296. await w.loadURL('about:blank');
  297. const [, bgPageContents] = await promise;
  298. expect(bgPageContents.getType()).to.equal('backgroundPage');
  299. bgPageContents.openDevTools();
  300. bgPageContents.closeDevTools();
  301. });
  302. });
  303. describe('devtools extensions', () => {
  304. let showPanelTimeoutId: any = null;
  305. afterEach(() => {
  306. if (showPanelTimeoutId) clearTimeout(showPanelTimeoutId);
  307. });
  308. const showLastDevToolsPanel = (w: BrowserWindow) => {
  309. w.webContents.once('devtools-opened', () => {
  310. const show = () => {
  311. if (w == null || w.isDestroyed()) return;
  312. const { devToolsWebContents } = w as unknown as { devToolsWebContents: WebContents | undefined };
  313. if (devToolsWebContents == null || devToolsWebContents.isDestroyed()) {
  314. return;
  315. }
  316. const showLastPanel = () => {
  317. // this is executed in the devtools context, where UI is a global
  318. const { UI } = (window as any);
  319. const lastPanelId = UI.inspectorView._tabbedPane._tabs.peekLast().id;
  320. UI.inspectorView.showPanel(lastPanelId);
  321. };
  322. devToolsWebContents.executeJavaScript(`(${showLastPanel})()`, false).then(() => {
  323. showPanelTimeoutId = setTimeout(show, 100);
  324. });
  325. };
  326. showPanelTimeoutId = setTimeout(show, 100);
  327. });
  328. };
  329. it('loads a devtools extension', async () => {
  330. const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
  331. customSession.loadExtension(path.join(fixtures, 'extensions', 'devtools-extension'));
  332. const winningMessage = emittedOnce(ipcMain, 'winning');
  333. const w = new BrowserWindow({ show: true, webPreferences: { session: customSession, nodeIntegration: true } });
  334. await w.loadURL(url);
  335. w.webContents.openDevTools();
  336. showLastDevToolsPanel(w);
  337. await winningMessage;
  338. });
  339. });
  340. describe('deprecation shims', () => {
  341. it('loads an extension through BrowserWindow.addExtension', async () => {
  342. BrowserWindow.addExtension(path.join(fixtures, 'extensions', 'red-bg'));
  343. const w = new BrowserWindow({ show: false });
  344. await w.loadURL(url);
  345. const bg = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor');
  346. expect(bg).to.equal('red');
  347. });
  348. it('loads an extension through BrowserWindow.addDevToolsExtension', async () => {
  349. BrowserWindow.addDevToolsExtension(path.join(fixtures, 'extensions', 'red-bg'));
  350. const w = new BrowserWindow({ show: false });
  351. await w.loadURL(url);
  352. const bg = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor');
  353. expect(bg).to.equal('red');
  354. });
  355. it('removes an extension through BrowserWindow.removeExtension', async () => {
  356. await (BrowserWindow.addExtension(path.join(fixtures, 'extensions', 'red-bg')) as any);
  357. BrowserWindow.removeExtension('red-bg');
  358. const w = new BrowserWindow({ show: false });
  359. await w.loadURL(url);
  360. const bg = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor');
  361. expect(bg).to.equal('');
  362. });
  363. });
  364. describe('chrome extension content scripts', () => {
  365. const fixtures = path.resolve(__dirname, 'fixtures');
  366. const extensionPath = path.resolve(fixtures, 'extensions');
  367. const addExtension = (name: string) => session.defaultSession.loadExtension(path.resolve(extensionPath, name));
  368. const removeAllExtensions = () => {
  369. Object.keys(session.defaultSession.getAllExtensions()).map(extName => {
  370. session.defaultSession.removeExtension(extName);
  371. });
  372. };
  373. let responseIdCounter = 0;
  374. const executeJavaScriptInFrame = (webContents: WebContents, frameRoutingId: number, code: string) => {
  375. return new Promise(resolve => {
  376. const responseId = responseIdCounter++;
  377. ipcMain.once(`executeJavaScriptInFrame_${responseId}`, (event, result) => {
  378. resolve(result);
  379. });
  380. webContents.send('executeJavaScriptInFrame', frameRoutingId, code, responseId);
  381. });
  382. };
  383. const generateTests = (sandboxEnabled: boolean, contextIsolationEnabled: boolean) => {
  384. describe(`with sandbox ${sandboxEnabled ? 'enabled' : 'disabled'} and context isolation ${contextIsolationEnabled ? 'enabled' : 'disabled'}`, () => {
  385. let w: BrowserWindow;
  386. describe('supports "run_at" option', () => {
  387. beforeEach(async () => {
  388. await closeWindow(w);
  389. w = new BrowserWindow({
  390. show: false,
  391. width: 400,
  392. height: 400,
  393. webPreferences: {
  394. contextIsolation: contextIsolationEnabled,
  395. sandbox: sandboxEnabled
  396. }
  397. });
  398. });
  399. afterEach(() => {
  400. removeAllExtensions();
  401. return closeWindow(w).then(() => { w = null as unknown as BrowserWindow; });
  402. });
  403. it('should run content script at document_start', async () => {
  404. await addExtension('content-script-document-start');
  405. w.webContents.once('dom-ready', async () => {
  406. const result = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor');
  407. expect(result).to.equal('red');
  408. });
  409. w.loadURL(url);
  410. });
  411. it('should run content script at document_idle', async () => {
  412. await addExtension('content-script-document-idle');
  413. w.loadURL(url);
  414. const result = await w.webContents.executeJavaScript('document.body.style.backgroundColor');
  415. expect(result).to.equal('red');
  416. });
  417. it('should run content script at document_end', async () => {
  418. await addExtension('content-script-document-end');
  419. w.webContents.once('did-finish-load', async () => {
  420. const result = await w.webContents.executeJavaScript('document.documentElement.style.backgroundColor');
  421. expect(result).to.equal('red');
  422. });
  423. w.loadURL(url);
  424. });
  425. });
  426. // TODO(nornagon): real extensions don't load on file: urls, so this
  427. // test needs to be updated to serve its content over http.
  428. describe.skip('supports "all_frames" option', () => {
  429. const contentScript = path.resolve(fixtures, 'extensions/content-script');
  430. // Computed style values
  431. const COLOR_RED = 'rgb(255, 0, 0)';
  432. const COLOR_BLUE = 'rgb(0, 0, 255)';
  433. const COLOR_TRANSPARENT = 'rgba(0, 0, 0, 0)';
  434. before(() => {
  435. BrowserWindow.addExtension(contentScript);
  436. });
  437. after(() => {
  438. BrowserWindow.removeExtension('content-script-test');
  439. });
  440. beforeEach(() => {
  441. w = new BrowserWindow({
  442. show: false,
  443. webPreferences: {
  444. // enable content script injection in subframes
  445. nodeIntegrationInSubFrames: true,
  446. preload: path.join(contentScript, 'all_frames-preload.js')
  447. }
  448. });
  449. });
  450. afterEach(() =>
  451. closeWindow(w).then(() => {
  452. w = null as unknown as BrowserWindow;
  453. })
  454. );
  455. it('applies matching rules in subframes', async () => {
  456. const detailsPromise = emittedNTimes(w.webContents, 'did-frame-finish-load', 2);
  457. w.loadFile(path.join(contentScript, 'frame-with-frame.html'));
  458. const frameEvents = await detailsPromise;
  459. await Promise.all(
  460. frameEvents.map(async frameEvent => {
  461. const [, isMainFrame, , frameRoutingId] = frameEvent;
  462. const result: any = await executeJavaScriptInFrame(
  463. w.webContents,
  464. frameRoutingId,
  465. `(() => {
  466. const a = document.getElementById('all_frames_enabled')
  467. const b = document.getElementById('all_frames_disabled')
  468. return {
  469. enabledColor: getComputedStyle(a).backgroundColor,
  470. disabledColor: getComputedStyle(b).backgroundColor
  471. }
  472. })()`
  473. );
  474. expect(result.enabledColor).to.equal(COLOR_RED);
  475. if (isMainFrame) {
  476. expect(result.disabledColor).to.equal(COLOR_BLUE);
  477. } else {
  478. expect(result.disabledColor).to.equal(COLOR_TRANSPARENT); // null color
  479. }
  480. })
  481. );
  482. });
  483. });
  484. });
  485. };
  486. generateTests(false, false);
  487. generateTests(false, true);
  488. generateTests(true, false);
  489. generateTests(true, true);
  490. });
  491. describe('extension ui pages', () => {
  492. afterEach(() => {
  493. session.defaultSession.getAllExtensions().forEach(e => {
  494. session.defaultSession.removeExtension(e.id);
  495. });
  496. });
  497. it('loads a ui page of an extension', async () => {
  498. const { id } = await session.defaultSession.loadExtension(path.join(fixtures, 'extensions', 'ui-page'));
  499. const w = new BrowserWindow({ show: false });
  500. await w.loadURL(`chrome-extension://${id}/bare-page.html`);
  501. const textContent = await w.webContents.executeJavaScript('document.body.textContent');
  502. expect(textContent).to.equal('ui page loaded ok\n');
  503. });
  504. it('can load resources', async () => {
  505. const { id } = await session.defaultSession.loadExtension(path.join(fixtures, 'extensions', 'ui-page'));
  506. const w = new BrowserWindow({ show: false });
  507. await w.loadURL(`chrome-extension://${id}/page-script-load.html`);
  508. const textContent = await w.webContents.executeJavaScript('document.body.textContent');
  509. expect(textContent).to.equal('script loaded ok\n');
  510. });
  511. });
  512. });
  513. ifdescribe(!process.electronBinding('features').isExtensionsEnabled())('chrome extensions', () => {
  514. const fixtures = path.resolve(__dirname, 'fixtures');
  515. let w: BrowserWindow;
  516. before(() => {
  517. BrowserWindow.addExtension(path.join(fixtures, 'extensions/chrome-api'));
  518. });
  519. after(() => {
  520. BrowserWindow.removeExtension('chrome-api');
  521. });
  522. beforeEach(() => {
  523. w = new BrowserWindow({ show: false });
  524. });
  525. afterEach(() => closeWindow(w).then(() => { w = null as unknown as BrowserWindow; }));
  526. it('chrome.runtime.connect parses arguments properly', async function () {
  527. await w.loadURL('about:blank');
  528. const promise = emittedOnce(w.webContents, 'console-message');
  529. const message = { method: 'connect' };
  530. w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
  531. const [,, responseString] = await promise;
  532. const response = JSON.parse(responseString);
  533. expect(response).to.be.true();
  534. });
  535. it('runtime.getManifest returns extension manifest', async () => {
  536. const actualManifest = (() => {
  537. const data = fs.readFileSync(path.join(fixtures, 'extensions/chrome-api/manifest.json'), 'utf-8');
  538. return JSON.parse(data);
  539. })();
  540. await w.loadURL('about:blank');
  541. const promise = emittedOnce(w.webContents, 'console-message');
  542. const message = { method: 'getManifest' };
  543. w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
  544. const [,, manifestString] = await promise;
  545. const manifest = JSON.parse(manifestString);
  546. expect(manifest.name).to.equal(actualManifest.name);
  547. expect(manifest.content_scripts).to.have.lengthOf(actualManifest.content_scripts.length);
  548. });
  549. it('chrome.tabs.sendMessage receives the response', async function () {
  550. await w.loadURL('about:blank');
  551. const promise = emittedOnce(w.webContents, 'console-message');
  552. const message = { method: 'sendMessage', args: ['Hello World!'] };
  553. w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
  554. const [,, responseString] = await promise;
  555. const response = JSON.parse(responseString);
  556. expect(response.message).to.equal('Hello World!');
  557. expect(response.tabId).to.equal(w.webContents.id);
  558. });
  559. it('chrome.tabs.executeScript receives the response', async function () {
  560. await w.loadURL('about:blank');
  561. const promise = emittedOnce(w.webContents, 'console-message');
  562. const message = { method: 'executeScript', args: ['1 + 2'] };
  563. w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
  564. const [,, responseString] = await promise;
  565. const response = JSON.parse(responseString);
  566. expect(response).to.equal(3);
  567. });
  568. describe('extensions and dev tools extensions', () => {
  569. let showPanelTimeoutId: NodeJS.Timeout | null = null;
  570. const showLastDevToolsPanel = (w: BrowserWindow) => {
  571. w.webContents.once('devtools-opened', () => {
  572. const show = () => {
  573. if (w == null || w.isDestroyed()) return;
  574. const { devToolsWebContents } = w as unknown as { devToolsWebContents: WebContents | undefined };
  575. if (devToolsWebContents == null || devToolsWebContents.isDestroyed()) {
  576. return;
  577. }
  578. const showLastPanel = () => {
  579. // this is executed in the devtools context, where UI is a global
  580. const { UI } = (window as any);
  581. const lastPanelId = UI.inspectorView._tabbedPane._tabs.peekLast().id;
  582. UI.inspectorView.showPanel(lastPanelId);
  583. };
  584. devToolsWebContents.executeJavaScript(`(${showLastPanel})()`, false).then(() => {
  585. showPanelTimeoutId = setTimeout(show, 100);
  586. });
  587. };
  588. showPanelTimeoutId = setTimeout(show, 100);
  589. });
  590. };
  591. afterEach(() => {
  592. if (showPanelTimeoutId != null) {
  593. clearTimeout(showPanelTimeoutId);
  594. showPanelTimeoutId = null;
  595. }
  596. });
  597. describe('BrowserWindow.addDevToolsExtension', () => {
  598. describe('for invalid extensions', () => {
  599. it('throws errors for missing manifest.json files', () => {
  600. const nonexistentExtensionPath = path.join(__dirname, 'does-not-exist');
  601. expect(() => {
  602. BrowserWindow.addDevToolsExtension(nonexistentExtensionPath);
  603. }).to.throw(/ENOENT: no such file or directory/);
  604. });
  605. it('throws errors for invalid manifest.json files', () => {
  606. const badManifestExtensionPath = path.join(__dirname, 'fixtures', 'devtools-extensions', 'bad-manifest');
  607. expect(() => {
  608. BrowserWindow.addDevToolsExtension(badManifestExtensionPath);
  609. }).to.throw(/Unexpected token }/);
  610. });
  611. });
  612. describe('for a valid extension', () => {
  613. const extensionName = 'foo';
  614. before(() => {
  615. const extensionPath = path.join(__dirname, 'fixtures', 'devtools-extensions', 'foo');
  616. BrowserWindow.addDevToolsExtension(extensionPath);
  617. expect(BrowserWindow.getDevToolsExtensions()).to.have.property(extensionName);
  618. });
  619. after(() => {
  620. BrowserWindow.removeDevToolsExtension('foo');
  621. expect(BrowserWindow.getDevToolsExtensions()).to.not.have.property(extensionName);
  622. });
  623. describe('when the devtools is docked', () => {
  624. let message: any;
  625. let w: BrowserWindow;
  626. before(async () => {
  627. w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } });
  628. const p = new Promise(resolve => ipcMain.once('answer', (event, message) => {
  629. resolve(message);
  630. }));
  631. showLastDevToolsPanel(w);
  632. w.loadURL('about:blank');
  633. w.webContents.openDevTools({ mode: 'bottom' });
  634. message = await p;
  635. });
  636. after(closeAllWindows);
  637. describe('created extension info', function () {
  638. it('has proper "runtimeId"', async function () {
  639. expect(message).to.have.ownProperty('runtimeId');
  640. expect(message.runtimeId).to.equal(extensionName);
  641. });
  642. it('has "tabId" matching webContents id', function () {
  643. expect(message).to.have.ownProperty('tabId');
  644. expect(message.tabId).to.equal(w.webContents.id);
  645. });
  646. it('has "i18nString" with proper contents', function () {
  647. expect(message).to.have.ownProperty('i18nString');
  648. expect(message.i18nString).to.equal('foo - bar (baz)');
  649. });
  650. it('has "storageItems" with proper contents', function () {
  651. expect(message).to.have.ownProperty('storageItems');
  652. expect(message.storageItems).to.deep.equal({
  653. local: {
  654. set: { hello: 'world', world: 'hello' },
  655. remove: { world: 'hello' },
  656. clear: {}
  657. },
  658. sync: {
  659. set: { foo: 'bar', bar: 'foo' },
  660. remove: { foo: 'bar' },
  661. clear: {}
  662. }
  663. });
  664. });
  665. });
  666. });
  667. describe('when the devtools is undocked', () => {
  668. let message: any;
  669. let w: BrowserWindow;
  670. before(async () => {
  671. w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } });
  672. showLastDevToolsPanel(w);
  673. w.loadURL('about:blank');
  674. w.webContents.openDevTools({ mode: 'undocked' });
  675. message = await new Promise(resolve => ipcMain.once('answer', (event, message) => {
  676. resolve(message);
  677. }));
  678. });
  679. after(closeAllWindows);
  680. describe('created extension info', function () {
  681. it('has proper "runtimeId"', function () {
  682. expect(message).to.have.ownProperty('runtimeId');
  683. expect(message.runtimeId).to.equal(extensionName);
  684. });
  685. it('has "tabId" matching webContents id', function () {
  686. expect(message).to.have.ownProperty('tabId');
  687. expect(message.tabId).to.equal(w.webContents.id);
  688. });
  689. });
  690. });
  691. });
  692. });
  693. it('works when used with partitions', async () => {
  694. const w = new BrowserWindow({
  695. show: false,
  696. webPreferences: {
  697. nodeIntegration: true,
  698. partition: 'temp'
  699. }
  700. });
  701. const extensionPath = path.join(__dirname, 'fixtures', 'devtools-extensions', 'foo');
  702. BrowserWindow.addDevToolsExtension(extensionPath);
  703. try {
  704. showLastDevToolsPanel(w);
  705. const p: Promise<any> = new Promise(resolve => ipcMain.once('answer', function (event, message) {
  706. resolve(message);
  707. }));
  708. w.loadURL('about:blank');
  709. w.webContents.openDevTools({ mode: 'bottom' });
  710. const message = await p;
  711. expect(message.runtimeId).to.equal('foo');
  712. } finally {
  713. BrowserWindow.removeDevToolsExtension('foo');
  714. await closeAllWindows();
  715. }
  716. });
  717. it('serializes the registered extensions on quit', () => {
  718. const extensionName = 'foo';
  719. const extensionPath = path.join(__dirname, 'fixtures', 'devtools-extensions', extensionName);
  720. const serializedPath = path.join(app.getPath('userData'), 'DevTools Extensions');
  721. BrowserWindow.addDevToolsExtension(extensionPath);
  722. app.emit('will-quit');
  723. expect(JSON.parse(fs.readFileSync(serializedPath, 'utf8'))).to.deep.equal([extensionPath]);
  724. BrowserWindow.removeDevToolsExtension(extensionName);
  725. app.emit('will-quit');
  726. expect(fs.existsSync(serializedPath)).to.be.false('file exists');
  727. });
  728. describe('BrowserWindow.addExtension', () => {
  729. it('throws errors for missing manifest.json files', () => {
  730. expect(() => {
  731. BrowserWindow.addExtension(path.join(__dirname, 'does-not-exist'));
  732. }).to.throw('ENOENT: no such file or directory');
  733. });
  734. it('throws errors for invalid manifest.json files', () => {
  735. expect(() => {
  736. BrowserWindow.addExtension(path.join(__dirname, 'fixtures', 'devtools-extensions', 'bad-manifest'));
  737. }).to.throw('Unexpected token }');
  738. });
  739. });
  740. });
  741. });