guest-window-manager-spec.ts 15 KB

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