guest-window-manager-spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  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 an override returns action: deny', async () => {
  113. const denied = new Promise((resolve) => {
  114. browserWindow.webContents.setWindowOpenHandler(() => {
  115. setTimeout(resolve);
  116. return { action: 'deny' };
  117. });
  118. });
  119. browserWindow.webContents.on('new-window', () => {
  120. assert.fail('new-window should not to be called with an overridden window.open');
  121. });
  122. browserWindow.webContents.on('did-create-window', () => {
  123. assert.fail('did-create-window should not to be called with an overridden window.open');
  124. });
  125. browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
  126. await denied;
  127. });
  128. it('is called when clicking on a target=_blank link', async () => {
  129. const denied = new Promise((resolve) => {
  130. browserWindow.webContents.setWindowOpenHandler(() => {
  131. setTimeout(resolve);
  132. return { action: 'deny' };
  133. });
  134. });
  135. browserWindow.webContents.on('new-window', () => {
  136. assert.fail('new-window should not to be called with an overridden window.open');
  137. });
  138. browserWindow.webContents.on('did-create-window', () => {
  139. assert.fail('did-create-window should not to be called with an overridden window.open');
  140. });
  141. 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>');
  142. browserWindow.webContents.sendInputEvent({ type: 'mouseDown', x: 10, y: 10, button: 'left', clickCount: 1 });
  143. browserWindow.webContents.sendInputEvent({ type: 'mouseUp', x: 10, y: 10, button: 'left', clickCount: 1 });
  144. await denied;
  145. });
  146. it('is called when shift-clicking on a link', async () => {
  147. const denied = new Promise((resolve) => {
  148. browserWindow.webContents.setWindowOpenHandler(() => {
  149. setTimeout(resolve);
  150. return { action: 'deny' };
  151. });
  152. });
  153. browserWindow.webContents.on('new-window', () => {
  154. assert.fail('new-window should not to be called with an overridden window.open');
  155. });
  156. browserWindow.webContents.on('did-create-window', () => {
  157. assert.fail('did-create-window should not to be called with an overridden window.open');
  158. });
  159. 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>');
  160. browserWindow.webContents.sendInputEvent({ type: 'mouseDown', x: 10, y: 10, button: 'left', clickCount: 1, modifiers: ['shift'] });
  161. browserWindow.webContents.sendInputEvent({ type: 'mouseUp', x: 10, y: 10, button: 'left', clickCount: 1, modifiers: ['shift'] });
  162. await denied;
  163. });
  164. it('fires handler with correct params', async () => {
  165. const testFrameName = 'test-frame-name';
  166. const testFeatures = 'top=10&left=10&something-unknown&show=no';
  167. const testUrl = 'app://does-not-exist/';
  168. const details = await new Promise<Electron.HandlerDetails>(resolve => {
  169. browserWindow.webContents.setWindowOpenHandler((details) => {
  170. setTimeout(() => resolve(details));
  171. return { action: 'deny' };
  172. });
  173. browserWindow.webContents.executeJavaScript(`window.open('${testUrl}', '${testFrameName}', '${testFeatures}') && true`);
  174. });
  175. const { url, frameName, features, disposition, referrer } = details;
  176. expect(url).to.equal(testUrl);
  177. expect(frameName).to.equal(testFrameName);
  178. expect(features).to.equal(testFeatures);
  179. expect(disposition).to.equal('new-window');
  180. expect(referrer).to.deep.equal({
  181. policy: 'strict-origin-when-cross-origin',
  182. url: ''
  183. });
  184. });
  185. it('includes post body', async () => {
  186. const details = await new Promise<Electron.HandlerDetails>(resolve => {
  187. browserWindow.webContents.setWindowOpenHandler((details) => {
  188. setTimeout(() => resolve(details));
  189. return { action: 'deny' };
  190. });
  191. browserWindow.webContents.loadURL(`data:text/html,${encodeURIComponent(`
  192. <form action="http://example.com" target="_blank" method="POST" id="form">
  193. <input name="key" value="value"></input>
  194. </form>
  195. <script>form.submit()</script>
  196. `)}`);
  197. });
  198. const { url, frameName, features, disposition, referrer, postBody } = details;
  199. expect(url).to.equal('http://example.com/');
  200. expect(frameName).to.equal('');
  201. expect(features).to.deep.equal('');
  202. expect(disposition).to.equal('foreground-tab');
  203. expect(referrer).to.deep.equal({
  204. policy: 'strict-origin-when-cross-origin',
  205. url: ''
  206. });
  207. expect(postBody).to.deep.equal({
  208. contentType: 'application/x-www-form-urlencoded',
  209. data: [{
  210. type: 'rawData',
  211. bytes: Buffer.from('key=value')
  212. }]
  213. });
  214. });
  215. it('does fire window creation events if an override returns action: allow', async () => {
  216. browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow' }));
  217. setImmediate(() => {
  218. browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
  219. });
  220. await Promise.all([
  221. emittedOnce(browserWindow.webContents, 'did-create-window'),
  222. emittedOnce(browserWindow.webContents, 'new-window')
  223. ]);
  224. });
  225. it('can change webPreferences of child windows', (done) => {
  226. browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow', overrideBrowserWindowOptions: { webPreferences: { defaultFontSize: 30 } } }));
  227. browserWindow.webContents.on('did-create-window', async (childWindow) => {
  228. await childWindow.webContents.executeJavaScript("document.write('hello')");
  229. const size = await childWindow.webContents.executeJavaScript("getComputedStyle(document.querySelector('body')).fontSize");
  230. expect(size).to.equal('30px');
  231. done();
  232. });
  233. browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
  234. });
  235. it('does not hang parent window when denying window.open', async () => {
  236. browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
  237. browserWindow.webContents.executeJavaScript("window.open('https://127.0.0.1')");
  238. expect(await browserWindow.webContents.executeJavaScript('42')).to.equal(42);
  239. });
  240. });
  241. }
  242. });
  243. function stringifySnapshots (snapshots: any, pretty = false) {
  244. return JSON.stringify(snapshots, (key, value) => {
  245. if (['sender', 'webContents'].includes(key)) {
  246. return '[WebContents]';
  247. }
  248. if (key === 'openerId' && typeof value === 'number') {
  249. return 'placeholder-opener-id';
  250. }
  251. if (key === 'processId' && typeof value === 'number') {
  252. return 'placeholder-process-id';
  253. }
  254. if (key === 'returnValue') {
  255. return 'placeholder-guest-contents-id';
  256. }
  257. return value;
  258. }, pretty ? 2 : undefined);
  259. }
  260. function parseSnapshots (snapshotsJson: string) {
  261. return JSON.parse(snapshotsJson, (key, value) => {
  262. if (key === 'openerId' && value === 'placeholder-opener-id') return 1;
  263. return value;
  264. });
  265. }