guest-window-manager-spec.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. import { BrowserWindow, screen } from 'electron';
  2. import { expect, assert } from 'chai';
  3. import { once } from 'node:events';
  4. import * as http from 'node:http';
  5. import { HexColors, ScreenCapture, hasCapturableScreen } from './lib/screen-helpers';
  6. import { ifit, listen } from './lib/spec-helpers';
  7. import { closeAllWindows } from './lib/window-helpers';
  8. describe('webContents.setWindowOpenHandler', () => {
  9. describe('native window', () => {
  10. let browserWindow: BrowserWindow;
  11. beforeEach(async () => {
  12. browserWindow = new BrowserWindow({ show: false });
  13. await browserWindow.loadURL('about:blank');
  14. });
  15. afterEach(closeAllWindows);
  16. it('does not fire window creation events if the handler callback throws an error', (done) => {
  17. const error = new Error('oh no');
  18. const listeners = process.listeners('uncaughtException');
  19. process.removeAllListeners('uncaughtException');
  20. process.on('uncaughtException', (thrown) => {
  21. try {
  22. expect(thrown).to.equal(error);
  23. done();
  24. } catch (e) {
  25. done(e);
  26. } finally {
  27. process.removeAllListeners('uncaughtException');
  28. for (const listener of listeners) {
  29. process.on('uncaughtException', listener);
  30. }
  31. }
  32. });
  33. browserWindow.webContents.on('did-create-window', () => {
  34. assert.fail('did-create-window should not be called with an overridden window.open');
  35. });
  36. browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
  37. browserWindow.webContents.setWindowOpenHandler(() => {
  38. throw error;
  39. });
  40. });
  41. it('does not fire window creation events if the handler callback returns a bad result', async () => {
  42. const bad = new Promise((resolve) => {
  43. browserWindow.webContents.setWindowOpenHandler(() => {
  44. setTimeout(resolve);
  45. return [1, 2, 3] as any;
  46. });
  47. });
  48. browserWindow.webContents.on('did-create-window', () => {
  49. assert.fail('did-create-window should not be called with an overridden window.open');
  50. });
  51. browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
  52. await bad;
  53. });
  54. it('does not fire window creation events if an override returns action: deny', async () => {
  55. const denied = new Promise((resolve) => {
  56. browserWindow.webContents.setWindowOpenHandler(() => {
  57. setTimeout(resolve);
  58. return { action: 'deny' };
  59. });
  60. });
  61. browserWindow.webContents.on('did-create-window', () => {
  62. assert.fail('did-create-window should not be called with an overridden window.open');
  63. });
  64. browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
  65. await denied;
  66. });
  67. it('is called when clicking on a target=_blank link', async () => {
  68. const denied = new Promise((resolve) => {
  69. browserWindow.webContents.setWindowOpenHandler(() => {
  70. setTimeout(resolve);
  71. return { action: 'deny' };
  72. });
  73. });
  74. browserWindow.webContents.on('did-create-window', () => {
  75. assert.fail('did-create-window should not be called with an overridden window.open');
  76. });
  77. 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>');
  78. browserWindow.webContents.sendInputEvent({ type: 'mouseDown', x: 10, y: 10, button: 'left', clickCount: 1 });
  79. browserWindow.webContents.sendInputEvent({ type: 'mouseUp', x: 10, y: 10, button: 'left', clickCount: 1 });
  80. await denied;
  81. });
  82. it('is called when shift-clicking on a link', async () => {
  83. const denied = new Promise((resolve) => {
  84. browserWindow.webContents.setWindowOpenHandler(() => {
  85. setTimeout(resolve);
  86. return { action: 'deny' };
  87. });
  88. });
  89. browserWindow.webContents.on('did-create-window', () => {
  90. assert.fail('did-create-window should not be called with an overridden window.open');
  91. });
  92. 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>');
  93. browserWindow.webContents.sendInputEvent({ type: 'mouseDown', x: 10, y: 10, button: 'left', clickCount: 1, modifiers: ['shift'] });
  94. browserWindow.webContents.sendInputEvent({ type: 'mouseUp', x: 10, y: 10, button: 'left', clickCount: 1, modifiers: ['shift'] });
  95. await denied;
  96. });
  97. it('fires handler with correct params', async () => {
  98. const testFrameName = 'test-frame-name';
  99. const testFeatures = 'top=10&left=10&something-unknown&show=no';
  100. const testUrl = 'app://does-not-exist/';
  101. const details = await new Promise<Electron.HandlerDetails>(resolve => {
  102. browserWindow.webContents.setWindowOpenHandler((details) => {
  103. setTimeout(() => resolve(details));
  104. return { action: 'deny' };
  105. });
  106. browserWindow.webContents.executeJavaScript(`window.open('${testUrl}', '${testFrameName}', '${testFeatures}') && true`);
  107. });
  108. const { url, frameName, features, disposition, referrer } = details;
  109. expect(url).to.equal(testUrl);
  110. expect(frameName).to.equal(testFrameName);
  111. expect(features).to.equal(testFeatures);
  112. expect(disposition).to.equal('new-window');
  113. expect(referrer).to.deep.equal({
  114. policy: 'strict-origin-when-cross-origin',
  115. url: ''
  116. });
  117. });
  118. it('includes post body', async () => {
  119. const details = await new Promise<Electron.HandlerDetails>(resolve => {
  120. browserWindow.webContents.setWindowOpenHandler((details) => {
  121. setTimeout(() => resolve(details));
  122. return { action: 'deny' };
  123. });
  124. browserWindow.webContents.loadURL(`data:text/html,${encodeURIComponent(`
  125. <form action="http://example.com" target="_blank" method="POST" id="form">
  126. <input name="key" value="value"></input>
  127. </form>
  128. <script>form.submit()</script>
  129. `)}`);
  130. });
  131. const { url, frameName, features, disposition, referrer, postBody } = details;
  132. expect(url).to.equal('http://example.com/');
  133. expect(frameName).to.equal('');
  134. expect(features).to.deep.equal('');
  135. expect(disposition).to.equal('foreground-tab');
  136. expect(referrer).to.deep.equal({
  137. policy: 'strict-origin-when-cross-origin',
  138. url: ''
  139. });
  140. expect(postBody).to.deep.equal({
  141. contentType: 'application/x-www-form-urlencoded',
  142. data: [{
  143. type: 'rawData',
  144. bytes: Buffer.from('key=value')
  145. }]
  146. });
  147. });
  148. it('does fire window creation events if an override returns action: allow', async () => {
  149. browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow' }));
  150. setImmediate(() => {
  151. browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
  152. });
  153. await once(browserWindow.webContents, 'did-create-window');
  154. });
  155. it('can change webPreferences of child windows', async () => {
  156. browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow', overrideBrowserWindowOptions: { webPreferences: { defaultFontSize: 30 } } }));
  157. const didCreateWindow = once(browserWindow.webContents, 'did-create-window') as Promise<[BrowserWindow, Electron.DidCreateWindowDetails]>;
  158. browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
  159. const [childWindow] = await didCreateWindow;
  160. await childWindow.webContents.executeJavaScript("document.write('hello')");
  161. const size = await childWindow.webContents.executeJavaScript("getComputedStyle(document.querySelector('body')).fontSize");
  162. expect(size).to.equal('30px');
  163. });
  164. it('does not hang parent window when denying window.open', async () => {
  165. browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
  166. browserWindow.webContents.executeJavaScript("window.open('https://127.0.0.1')");
  167. expect(await browserWindow.webContents.executeJavaScript('42')).to.equal(42);
  168. });
  169. ifit(hasCapturableScreen())('should not make child window background transparent', async () => {
  170. browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow' }));
  171. const didCreateWindow = once(browserWindow.webContents, 'did-create-window');
  172. browserWindow.webContents.executeJavaScript("window.open('about:blank') && true");
  173. const [childWindow] = await didCreateWindow;
  174. const display = screen.getPrimaryDisplay();
  175. childWindow.setBounds(display.bounds);
  176. await childWindow.webContents.executeJavaScript("const meta = document.createElement('meta'); meta.name = 'color-scheme'; meta.content = 'dark'; document.head.appendChild(meta); true;");
  177. const screenCapture = new ScreenCapture(display);
  178. // color-scheme is set to dark so background should not be white
  179. await screenCapture.expectColorAtCenterDoesNotMatch(HexColors.WHITE);
  180. });
  181. });
  182. describe('custom window', () => {
  183. let browserWindow: BrowserWindow;
  184. let server: http.Server;
  185. let url: string;
  186. before(async () => {
  187. server = http.createServer((request, response) => {
  188. switch (request.url) {
  189. case '/index':
  190. response.statusCode = 200;
  191. response.end('<title>Index page</title>');
  192. break;
  193. case '/child':
  194. response.statusCode = 200;
  195. response.end('<title>Child page</title>');
  196. break;
  197. case '/test':
  198. response.statusCode = 200;
  199. response.end('<title>Test page</title>');
  200. break;
  201. default:
  202. throw new Error(`Unsupported endpoint: ${request.url}`);
  203. }
  204. });
  205. url = (await listen(server)).url;
  206. });
  207. after(() => {
  208. server.close();
  209. });
  210. beforeEach(async () => {
  211. browserWindow = new BrowserWindow({ show: false });
  212. await browserWindow.loadURL(`${url}/index`);
  213. });
  214. afterEach(closeAllWindows);
  215. it('throws error when created window uses invalid webcontents', async () => {
  216. const listeners = process.listeners('uncaughtException');
  217. process.removeAllListeners('uncaughtException');
  218. const uncaughtExceptionEmitted = new Promise<void>((resolve, reject) => {
  219. process.on('uncaughtException', (thrown) => {
  220. try {
  221. expect(thrown.message).to.equal('Invalid webContents. Created window should be connected to webContents passed with options object.');
  222. resolve();
  223. } catch (e) {
  224. reject(e);
  225. } finally {
  226. process.removeAllListeners('uncaughtException');
  227. listeners.forEach((listener) => process.on('uncaughtException', listener));
  228. }
  229. });
  230. });
  231. browserWindow.webContents.setWindowOpenHandler(() => {
  232. return {
  233. action: 'allow',
  234. createWindow: () => {
  235. const childWindow = new BrowserWindow({ title: 'New window' });
  236. return childWindow.webContents;
  237. }
  238. };
  239. });
  240. browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
  241. await uncaughtExceptionEmitted;
  242. });
  243. it('spawns browser window when createWindow is provided', async () => {
  244. const browserWindowTitle = 'Child browser window';
  245. const childWindow = await new Promise<Electron.BrowserWindow>(resolve => {
  246. browserWindow.webContents.setWindowOpenHandler(() => {
  247. return {
  248. action: 'allow',
  249. createWindow: (options) => {
  250. const childWindow = new BrowserWindow({ ...options, title: browserWindowTitle });
  251. resolve(childWindow);
  252. return childWindow.webContents;
  253. }
  254. };
  255. });
  256. browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
  257. });
  258. expect(childWindow.title).to.equal(browserWindowTitle);
  259. });
  260. it('should be able to access the child window document when createWindow is provided', async () => {
  261. browserWindow.webContents.setWindowOpenHandler(() => {
  262. return {
  263. action: 'allow',
  264. createWindow: (options) => {
  265. const child = new BrowserWindow(options);
  266. return child.webContents;
  267. }
  268. };
  269. });
  270. const aboutBlankTitle = await browserWindow.webContents.executeJavaScript(`
  271. const win1 = window.open('about:blank', '', 'show=no');
  272. win1.document.title = 'about-blank-title';
  273. win1.document.title;
  274. `);
  275. expect(aboutBlankTitle).to.equal('about-blank-title');
  276. const serverPageTitle = await browserWindow.webContents.executeJavaScript(`
  277. const win2 = window.open('${url}/child', '', 'show=no');
  278. win2.document.title = 'server-page-title';
  279. win2.document.title;
  280. `);
  281. expect(serverPageTitle).to.equal('server-page-title');
  282. });
  283. it('spawns browser window with overridden options', async () => {
  284. const childWindow = await new Promise<Electron.BrowserWindow>(resolve => {
  285. browserWindow.webContents.setWindowOpenHandler(() => {
  286. return {
  287. action: 'allow',
  288. overrideBrowserWindowOptions: {
  289. width: 640,
  290. height: 480
  291. },
  292. createWindow: (options) => {
  293. expect(options.width).to.equal(640);
  294. expect(options.height).to.equal(480);
  295. const childWindow = new BrowserWindow(options);
  296. resolve(childWindow);
  297. return childWindow.webContents;
  298. }
  299. };
  300. });
  301. browserWindow.webContents.executeJavaScript("window.open('about:blank', '', 'show=no') && true");
  302. });
  303. const size = childWindow.getSize();
  304. expect(size[0]).to.equal(640);
  305. expect(size[1]).to.equal(480);
  306. });
  307. it('spawns browser window with access to opener property', async () => {
  308. const childWindow = await new Promise<Electron.BrowserWindow>(resolve => {
  309. browserWindow.webContents.setWindowOpenHandler(() => {
  310. return {
  311. action: 'allow',
  312. createWindow: (options) => {
  313. const childWindow = new BrowserWindow(options);
  314. resolve(childWindow);
  315. return childWindow.webContents;
  316. }
  317. };
  318. });
  319. browserWindow.webContents.executeJavaScript(`window.open('${url}/child', '', 'show=no') && true`);
  320. });
  321. await once(childWindow.webContents, 'ready-to-show');
  322. const childWindowOpenerTitle = await childWindow.webContents.executeJavaScript('window.opener.document.title');
  323. expect(childWindowOpenerTitle).to.equal(browserWindow.title);
  324. });
  325. it('spawns browser window without access to opener property because of noopener attribute ', async () => {
  326. const childWindow = await new Promise<Electron.BrowserWindow>(resolve => {
  327. browserWindow.webContents.setWindowOpenHandler(() => {
  328. return {
  329. action: 'allow',
  330. createWindow: (options) => {
  331. const childWindow = new BrowserWindow(options);
  332. resolve(childWindow);
  333. return childWindow.webContents;
  334. }
  335. };
  336. });
  337. browserWindow.webContents.executeJavaScript(`window.open('${url}/child', '', 'noopener,show=no') && true`);
  338. });
  339. await once(childWindow.webContents, 'ready-to-show');
  340. await expect(childWindow.webContents.executeJavaScript('window.opener.document.title')).to.be.rejectedWith('Script failed to execute, this normally means an error was thrown. Check the renderer console for the error.');
  341. });
  342. });
  343. });