guest-window-manager-spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  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. }
  36. }
  37. };
  38. for (const testName of Object.keys(testConfig) as (keyof typeof testConfig)[]) {
  39. const { snapshotFileName, browserWindowOptions } = testConfig[testName];
  40. describe(`for ${testName} window opening`, () => {
  41. const snapshotFile = resolve(__dirname, 'fixtures', 'snapshots', snapshotFileName);
  42. let browserWindow: BrowserWindow;
  43. let existingSnapshots: any[];
  44. before(() => {
  45. existingSnapshots = parseSnapshots(readFileSync(snapshotFile, { encoding: 'utf8' }));
  46. });
  47. beforeEach((done) => {
  48. browserWindow = new BrowserWindow(browserWindowOptions);
  49. browserWindow.loadURL('about:blank');
  50. browserWindow.on('ready-to-show', () => { done(); });
  51. });
  52. afterEach(closeAllWindows);
  53. const newSnapshots: any[] = [];
  54. [
  55. 'top=5,left=10,resizable=no',
  56. 'zoomFactor=2,resizable=0,x=0,y=10',
  57. 'backgroundColor=gray,webPreferences=0,x=100,y=100',
  58. 'x=50,y=20,title=sup',
  59. 'show=false,top=1,left=1'
  60. ].forEach((features, index) => {
  61. /**
  62. * ATTN: If this test is failing, you likely just need to change
  63. * `shouldOverwriteSnapshot` to true and then evaluate the snapshot diff
  64. * to see if the change is harmless.
  65. */
  66. it(`matches snapshot for ${features}`, async () => {
  67. const newSnapshot = await genSnapshot(browserWindow, features);
  68. newSnapshots.push(newSnapshot);
  69. // TODO: The output when these fail could be friendlier.
  70. expect(stringifySnapshots(newSnapshot)).to.equal(stringifySnapshots(existingSnapshots[index]));
  71. });
  72. });
  73. after(() => {
  74. const shouldOverwriteSnapshot = false;
  75. if (shouldOverwriteSnapshot) writeFileSync(snapshotFile, stringifySnapshots(newSnapshots, true));
  76. });
  77. });
  78. }
  79. });
  80. describe('webContents.setWindowOpenHandler', () => {
  81. const testConfig = {
  82. native: {
  83. browserWindowOptions: {
  84. show: false,
  85. webPreferences: {
  86. nativeWindowOpen: true
  87. }
  88. }
  89. },
  90. proxy: {
  91. browserWindowOptions: {
  92. show: false,
  93. webPreferences: {
  94. nativeWindowOpen: false
  95. }
  96. }
  97. }
  98. };
  99. for (const testName of Object.keys(testConfig) as (keyof typeof testConfig)[]) {
  100. let browserWindow: BrowserWindow;
  101. const { browserWindowOptions } = testConfig[testName];
  102. describe(testName, () => {
  103. beforeEach(async () => {
  104. browserWindow = new BrowserWindow(browserWindowOptions);
  105. await browserWindow.loadURL('about:blank');
  106. });
  107. afterEach(closeAllWindows);
  108. it('does not fire window creation events if an override returns action: deny', async () => {
  109. const denied = new Promise((resolve) => {
  110. browserWindow.webContents.setWindowOpenHandler(() => {
  111. setTimeout(resolve);
  112. return { action: 'deny' };
  113. });
  114. });
  115. browserWindow.webContents.on('new-window', () => {
  116. assert.fail('new-window should not to be called with an overridden window.open');
  117. });
  118. browserWindow.webContents.on('did-create-window', () => {
  119. assert.fail('did-create-window should not to be called with an overridden window.open');
  120. });
  121. browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
  122. await denied;
  123. });
  124. it('is called when clicking on a target=_blank link', async () => {
  125. const denied = new Promise((resolve) => {
  126. browserWindow.webContents.setWindowOpenHandler(() => {
  127. setTimeout(resolve);
  128. return { action: 'deny' };
  129. });
  130. });
  131. browserWindow.webContents.on('new-window', () => {
  132. assert.fail('new-window should not to be called with an overridden window.open');
  133. });
  134. browserWindow.webContents.on('did-create-window', () => {
  135. assert.fail('did-create-window should not to be called with an overridden window.open');
  136. });
  137. 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>');
  138. browserWindow.webContents.sendInputEvent({ type: 'mouseDown', x: 10, y: 10, button: 'left', clickCount: 1 });
  139. browserWindow.webContents.sendInputEvent({ type: 'mouseUp', x: 10, y: 10, button: 'left', clickCount: 1 });
  140. await denied;
  141. });
  142. it('is called when shift-clicking on a link', async () => {
  143. const denied = new Promise((resolve) => {
  144. browserWindow.webContents.setWindowOpenHandler(() => {
  145. setTimeout(resolve);
  146. return { action: 'deny' };
  147. });
  148. });
  149. browserWindow.webContents.on('new-window', () => {
  150. assert.fail('new-window should not to be called with an overridden window.open');
  151. });
  152. browserWindow.webContents.on('did-create-window', () => {
  153. assert.fail('did-create-window should not to be called with an overridden window.open');
  154. });
  155. 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>');
  156. browserWindow.webContents.sendInputEvent({ type: 'mouseDown', x: 10, y: 10, button: 'left', clickCount: 1, modifiers: ['shift'] });
  157. browserWindow.webContents.sendInputEvent({ type: 'mouseUp', x: 10, y: 10, button: 'left', clickCount: 1, modifiers: ['shift'] });
  158. await denied;
  159. });
  160. it('fires handler with correct params', async () => {
  161. const testFrameName = 'test-frame-name';
  162. const testFeatures = 'top=10&left=10&something-unknown&show=no';
  163. const testUrl = 'app://does-not-exist/';
  164. const details = await new Promise<Electron.HandlerDetails>(resolve => {
  165. browserWindow.webContents.setWindowOpenHandler((details) => {
  166. setTimeout(() => resolve(details));
  167. return { action: 'deny' };
  168. });
  169. browserWindow.webContents.executeJavaScript(`window.open('${testUrl}', '${testFrameName}', '${testFeatures}') && true`);
  170. });
  171. const { url, frameName, features, disposition, referrer } = details;
  172. expect(url).to.equal(testUrl);
  173. expect(frameName).to.equal(testFrameName);
  174. expect(features).to.equal(testFeatures);
  175. expect(disposition).to.equal('new-window');
  176. expect(referrer).to.deep.equal({
  177. policy: 'strict-origin-when-cross-origin',
  178. url: ''
  179. });
  180. });
  181. it('includes post body', async () => {
  182. const details = await new Promise<Electron.HandlerDetails>(resolve => {
  183. browserWindow.webContents.setWindowOpenHandler((details) => {
  184. setTimeout(() => resolve(details));
  185. return { action: 'deny' };
  186. });
  187. browserWindow.webContents.loadURL(`data:text/html,${encodeURIComponent(`
  188. <form action="http://example.com" target="_blank" method="POST" id="form">
  189. <input name="key" value="value"></input>
  190. </form>
  191. <script>form.submit()</script>
  192. `)}`);
  193. });
  194. const { url, frameName, features, disposition, referrer, postBody } = details;
  195. expect(url).to.equal('http://example.com/');
  196. expect(frameName).to.equal('');
  197. expect(features).to.deep.equal('');
  198. expect(disposition).to.equal('foreground-tab');
  199. expect(referrer).to.deep.equal({
  200. policy: 'strict-origin-when-cross-origin',
  201. url: ''
  202. });
  203. expect(postBody).to.deep.equal({
  204. contentType: 'application/x-www-form-urlencoded',
  205. data: [{
  206. type: 'rawData',
  207. bytes: Buffer.from('key=value')
  208. }]
  209. });
  210. });
  211. it('does fire window creation events if an override returns action: allow', async () => {
  212. browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow' }));
  213. setImmediate(() => {
  214. browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
  215. });
  216. await Promise.all([
  217. emittedOnce(browserWindow.webContents, 'did-create-window'),
  218. emittedOnce(browserWindow.webContents, 'new-window')
  219. ]);
  220. });
  221. it('can change webPreferences of child windows', (done) => {
  222. browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow', overrideBrowserWindowOptions: { webPreferences: { defaultFontSize: 30 } } }));
  223. browserWindow.webContents.on('did-create-window', async (childWindow) => {
  224. await childWindow.webContents.executeJavaScript("document.write('hello')");
  225. const size = await childWindow.webContents.executeJavaScript("getComputedStyle(document.querySelector('body')).fontSize");
  226. expect(size).to.equal('30px');
  227. done();
  228. });
  229. browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
  230. });
  231. });
  232. }
  233. });
  234. function stringifySnapshots (snapshots: any, pretty = false) {
  235. return JSON.stringify(snapshots, (key, value) => {
  236. if (['sender', 'webContents'].includes(key)) {
  237. return '[WebContents]';
  238. }
  239. if (key === 'openerId' && typeof value === 'number') {
  240. return 'placeholder-opener-id';
  241. }
  242. if (key === 'processId' && typeof value === 'number') {
  243. return 'placeholder-process-id';
  244. }
  245. if (key === 'returnValue') {
  246. return 'placeholder-guest-contents-id';
  247. }
  248. return value;
  249. }, pretty ? 2 : undefined);
  250. }
  251. function parseSnapshots (snapshotsJson: string) {
  252. return JSON.parse(snapshotsJson, (key, value) => {
  253. if (key === 'openerId' && value === 'placeholder-opener-id') return 1;
  254. return value;
  255. });
  256. }