guest-window-manager-spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. import { BrowserWindow } from 'electron';
  2. import { writeFileSync, readFileSync } from 'fs';
  3. import { resolve } from 'path';
  4. import { expect, assert } from 'chai';
  5. import { closeAllWindows } from './window-helpers';
  6. const { emittedOnce } = require('./events-helpers');
  7. function genSnapshot (browserWindow: BrowserWindow, features: string) {
  8. return new Promise((resolve) => {
  9. browserWindow.webContents.on('new-window', (...args: any[]) => {
  10. resolve([features, ...args]);
  11. });
  12. browserWindow.webContents.executeJavaScript(`window.open('about:blank', 'frame-name', '${features}') && true`);
  13. });
  14. }
  15. describe('new-window event', () => {
  16. const testConfig = {
  17. native: {
  18. snapshotFileName: 'native-window-open.snapshot.txt',
  19. browserWindowOptions: {
  20. show: false,
  21. width: 200,
  22. title: 'cool',
  23. backgroundColor: 'blue',
  24. focusable: false,
  25. webPreferences: {
  26. nativeWindowOpen: true,
  27. sandbox: true
  28. }
  29. }
  30. },
  31. proxy: {
  32. snapshotFileName: 'proxy-window-open.snapshot.txt',
  33. browserWindowOptions: {
  34. show: false,
  35. webPreferences: {
  36. nativeWindowOpen: false,
  37. sandbox: false
  38. }
  39. }
  40. }
  41. };
  42. for (const testName of Object.keys(testConfig) as (keyof typeof testConfig)[]) {
  43. const { snapshotFileName, browserWindowOptions } = testConfig[testName];
  44. describe(`for ${testName} window opening`, () => {
  45. const snapshotFile = resolve(__dirname, 'fixtures', 'snapshots', snapshotFileName);
  46. let browserWindow: BrowserWindow;
  47. let existingSnapshots: any[];
  48. before(() => {
  49. existingSnapshots = parseSnapshots(readFileSync(snapshotFile, { encoding: 'utf8' }));
  50. });
  51. beforeEach((done) => {
  52. browserWindow = new BrowserWindow(browserWindowOptions);
  53. browserWindow.loadURL('about:blank');
  54. browserWindow.on('ready-to-show', () => { done(); });
  55. });
  56. afterEach(closeAllWindows);
  57. const newSnapshots: any[] = [];
  58. [
  59. 'top=5,left=10,resizable=no',
  60. 'zoomFactor=2,resizable=0,x=0,y=10',
  61. 'backgroundColor=gray,webPreferences=0,x=100,y=100',
  62. 'x=50,y=20,title=sup',
  63. 'show=false,top=1,left=1'
  64. ].forEach((features, index) => {
  65. /**
  66. * ATTN: If this test is failing, you likely just need to change
  67. * `shouldOverwriteSnapshot` to true and then evaluate the snapshot diff
  68. * to see if the change is harmless.
  69. */
  70. it(`matches snapshot for ${features}`, async () => {
  71. const newSnapshot = await genSnapshot(browserWindow, features);
  72. newSnapshots.push(newSnapshot);
  73. // TODO: The output when these fail could be friendlier.
  74. expect(stringifySnapshots(newSnapshot)).to.equal(stringifySnapshots(existingSnapshots[index]));
  75. });
  76. });
  77. after(() => {
  78. const shouldOverwriteSnapshot = false;
  79. if (shouldOverwriteSnapshot) writeFileSync(snapshotFile, stringifySnapshots(newSnapshots, true));
  80. });
  81. });
  82. }
  83. });
  84. describe('webContents.setWindowOpenHandler', () => {
  85. const testConfig = {
  86. native: {
  87. browserWindowOptions: {
  88. show: false,
  89. webPreferences: {
  90. nativeWindowOpen: true
  91. }
  92. }
  93. },
  94. proxy: {
  95. browserWindowOptions: {
  96. show: false,
  97. webPreferences: {
  98. nativeWindowOpen: false
  99. }
  100. }
  101. }
  102. };
  103. for (const testName of Object.keys(testConfig) as (keyof typeof testConfig)[]) {
  104. let browserWindow: BrowserWindow;
  105. const { browserWindowOptions } = testConfig[testName];
  106. describe(testName, () => {
  107. beforeEach(async () => {
  108. browserWindow = new BrowserWindow(browserWindowOptions);
  109. await browserWindow.loadURL('about:blank');
  110. });
  111. afterEach(closeAllWindows);
  112. it('does not fire window creation events if the handler callback throws an error', (done) => {
  113. const error = new Error('oh no');
  114. const listeners = process.listeners('uncaughtException');
  115. process.removeAllListeners('uncaughtException');
  116. process.on('uncaughtException', (thrown) => {
  117. try {
  118. expect(thrown).to.equal(error);
  119. done();
  120. } catch (e) {
  121. done(e);
  122. } finally {
  123. process.removeAllListeners('uncaughtException');
  124. listeners.forEach((listener) => process.on('uncaughtException', listener));
  125. }
  126. });
  127. browserWindow.webContents.on('new-window', () => {
  128. assert.fail('new-window should not be called with an overridden window.open');
  129. });
  130. browserWindow.webContents.on('did-create-window', () => {
  131. assert.fail('did-create-window should not be called with an overridden window.open');
  132. });
  133. browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
  134. browserWindow.webContents.setWindowOpenHandler(() => {
  135. throw error;
  136. });
  137. });
  138. it('does not fire window creation events if the handler callback returns a bad result', async () => {
  139. const bad = new Promise((resolve) => {
  140. browserWindow.webContents.setWindowOpenHandler(() => {
  141. setTimeout(resolve);
  142. return [1, 2, 3] as any;
  143. });
  144. });
  145. browserWindow.webContents.on('new-window', () => {
  146. assert.fail('new-window should not be called with an overridden window.open');
  147. });
  148. browserWindow.webContents.on('did-create-window', () => {
  149. assert.fail('did-create-window should not be called with an overridden window.open');
  150. });
  151. browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
  152. await bad;
  153. });
  154. it('does not fire window creation events if an override returns action: deny', async () => {
  155. const denied = new Promise((resolve) => {
  156. browserWindow.webContents.setWindowOpenHandler(() => {
  157. setTimeout(resolve);
  158. return { action: 'deny' };
  159. });
  160. });
  161. browserWindow.webContents.on('new-window', () => {
  162. assert.fail('new-window should not be called with an overridden window.open');
  163. });
  164. browserWindow.webContents.on('did-create-window', () => {
  165. assert.fail('did-create-window should not be called with an overridden window.open');
  166. });
  167. browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
  168. await denied;
  169. });
  170. it('is called when clicking on a target=_blank link', async () => {
  171. const denied = new Promise((resolve) => {
  172. browserWindow.webContents.setWindowOpenHandler(() => {
  173. setTimeout(resolve);
  174. return { action: 'deny' };
  175. });
  176. });
  177. browserWindow.webContents.on('new-window', () => {
  178. assert.fail('new-window should not to called with an overridden window.open');
  179. });
  180. browserWindow.webContents.on('did-create-window', () => {
  181. assert.fail('did-create-window should not be called with an overridden window.open');
  182. });
  183. await browserWindow.webContents.loadURL('data:text/html,<a target="_blank" href="http://example.com" style="display: block; width: 100%; height: 100%; position: fixed; left: 0; top: 0;">link</a>');
  184. browserWindow.webContents.sendInputEvent({ type: 'mouseDown', x: 10, y: 10, button: 'left', clickCount: 1 });
  185. browserWindow.webContents.sendInputEvent({ type: 'mouseUp', x: 10, y: 10, button: 'left', clickCount: 1 });
  186. await denied;
  187. });
  188. it('is called when shift-clicking on a link', async () => {
  189. const denied = new Promise((resolve) => {
  190. browserWindow.webContents.setWindowOpenHandler(() => {
  191. setTimeout(resolve);
  192. return { action: 'deny' };
  193. });
  194. });
  195. browserWindow.webContents.on('new-window', () => {
  196. assert.fail('new-window should not to called with an overridden window.open');
  197. });
  198. browserWindow.webContents.on('did-create-window', () => {
  199. assert.fail('did-create-window should not be called with an overridden window.open');
  200. });
  201. await browserWindow.webContents.loadURL('data:text/html,<a href="http://example.com" style="display: block; width: 100%; height: 100%; position: fixed; left: 0; top: 0;">link</a>');
  202. browserWindow.webContents.sendInputEvent({ type: 'mouseDown', x: 10, y: 10, button: 'left', clickCount: 1, modifiers: ['shift'] });
  203. browserWindow.webContents.sendInputEvent({ type: 'mouseUp', x: 10, y: 10, button: 'left', clickCount: 1, modifiers: ['shift'] });
  204. await denied;
  205. });
  206. it('fires handler with correct params', async () => {
  207. const testFrameName = 'test-frame-name';
  208. const testFeatures = 'top=10&left=10&something-unknown&show=no';
  209. const testUrl = 'app://does-not-exist/';
  210. const details = await new Promise<Electron.HandlerDetails>(resolve => {
  211. browserWindow.webContents.setWindowOpenHandler((details) => {
  212. setTimeout(() => resolve(details));
  213. return { action: 'deny' };
  214. });
  215. browserWindow.webContents.executeJavaScript(`window.open('${testUrl}', '${testFrameName}', '${testFeatures}') && true`);
  216. });
  217. const { url, frameName, features, disposition, referrer } = details;
  218. expect(url).to.equal(testUrl);
  219. expect(frameName).to.equal(testFrameName);
  220. expect(features).to.equal(testFeatures);
  221. expect(disposition).to.equal('new-window');
  222. expect(referrer).to.deep.equal({
  223. policy: 'strict-origin-when-cross-origin',
  224. url: ''
  225. });
  226. });
  227. it('includes post body', async () => {
  228. const details = await new Promise<Electron.HandlerDetails>(resolve => {
  229. browserWindow.webContents.setWindowOpenHandler((details) => {
  230. setTimeout(() => resolve(details));
  231. return { action: 'deny' };
  232. });
  233. browserWindow.webContents.loadURL(`data:text/html,${encodeURIComponent(`
  234. <form action="http://example.com" target="_blank" method="POST" id="form">
  235. <input name="key" value="value"></input>
  236. </form>
  237. <script>form.submit()</script>
  238. `)}`);
  239. });
  240. const { url, frameName, features, disposition, referrer, postBody } = details;
  241. expect(url).to.equal('http://example.com/');
  242. expect(frameName).to.equal('');
  243. expect(features).to.deep.equal('');
  244. expect(disposition).to.equal('foreground-tab');
  245. expect(referrer).to.deep.equal({
  246. policy: 'strict-origin-when-cross-origin',
  247. url: ''
  248. });
  249. expect(postBody).to.deep.equal({
  250. contentType: 'application/x-www-form-urlencoded',
  251. data: [{
  252. type: 'rawData',
  253. bytes: Buffer.from('key=value')
  254. }]
  255. });
  256. });
  257. it('does fire window creation events if an override returns action: allow', async () => {
  258. browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow' }));
  259. setImmediate(() => {
  260. browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
  261. });
  262. await Promise.all([
  263. emittedOnce(browserWindow.webContents, 'did-create-window'),
  264. emittedOnce(browserWindow.webContents, 'new-window')
  265. ]);
  266. });
  267. it('can change webPreferences of child windows', (done) => {
  268. browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow', overrideBrowserWindowOptions: { webPreferences: { defaultFontSize: 30 } } }));
  269. browserWindow.webContents.on('did-create-window', async (childWindow) => {
  270. await childWindow.webContents.executeJavaScript("document.write('hello')");
  271. const size = await childWindow.webContents.executeJavaScript("getComputedStyle(document.querySelector('body')).fontSize");
  272. expect(size).to.equal('30px');
  273. done();
  274. });
  275. browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
  276. });
  277. it('does not hang parent window when denying window.open', async () => {
  278. browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
  279. browserWindow.webContents.executeJavaScript("window.open('https://127.0.0.1')");
  280. expect(await browserWindow.webContents.executeJavaScript('42')).to.equal(42);
  281. });
  282. });
  283. }
  284. });
  285. function stringifySnapshots (snapshots: any, pretty = false) {
  286. return JSON.stringify(snapshots, (key, value) => {
  287. if (['sender', 'webContents'].includes(key)) {
  288. return '[WebContents]';
  289. }
  290. if (key === 'openerId' && typeof value === 'number') {
  291. return 'placeholder-opener-id';
  292. }
  293. if (key === 'processId' && typeof value === 'number') {
  294. return 'placeholder-process-id';
  295. }
  296. if (key === 'returnValue') {
  297. return 'placeholder-guest-contents-id';
  298. }
  299. return value;
  300. }, pretty ? 2 : undefined);
  301. }
  302. function parseSnapshots (snapshotsJson: string) {
  303. return JSON.parse(snapshotsJson, (key, value) => {
  304. if (key === 'openerId' && value === 'placeholder-opener-id') return 1;
  305. return value;
  306. });
  307. }