api-web-frame-main-spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. import { expect } from 'chai';
  2. import * as http from 'http';
  3. import * as path from 'path';
  4. import * as url from 'url';
  5. import { BrowserWindow, WebFrameMain, webFrameMain, ipcMain } from 'electron/main';
  6. import { closeAllWindows } from './window-helpers';
  7. import { emittedOnce, emittedNTimes } from './events-helpers';
  8. import { AddressInfo } from 'net';
  9. import { waitUntil } from './spec-helpers';
  10. describe('webFrameMain module', () => {
  11. const fixtures = path.resolve(__dirname, '..', 'spec-main', 'fixtures');
  12. const subframesPath = path.join(fixtures, 'sub-frames');
  13. const fileUrl = (filename: string) => url.pathToFileURL(path.join(subframesPath, filename)).href;
  14. type Server = { server: http.Server, url: string }
  15. /** Creates an HTTP server whose handler embeds the given iframe src. */
  16. const createServer = () => new Promise<Server>(resolve => {
  17. const server = http.createServer((req, res) => {
  18. const params = new URLSearchParams(url.parse(req.url || '').search || '');
  19. if (params.has('frameSrc')) {
  20. res.end(`<iframe src="${params.get('frameSrc')}"></iframe>`);
  21. } else {
  22. res.end('');
  23. }
  24. });
  25. server.listen(0, '127.0.0.1', () => {
  26. const url = `http://127.0.0.1:${(server.address() as AddressInfo).port}/`;
  27. resolve({ server, url });
  28. });
  29. });
  30. afterEach(closeAllWindows);
  31. describe('WebFrame traversal APIs', () => {
  32. let w: BrowserWindow;
  33. let webFrame: WebFrameMain;
  34. beforeEach(async () => {
  35. w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } });
  36. await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
  37. webFrame = w.webContents.mainFrame;
  38. });
  39. it('can access top frame', () => {
  40. expect(webFrame.top).to.equal(webFrame);
  41. });
  42. it('has no parent on top frame', () => {
  43. expect(webFrame.parent).to.be.null();
  44. });
  45. it('can access immediate frame descendents', () => {
  46. const { frames } = webFrame;
  47. expect(frames).to.have.lengthOf(1);
  48. const subframe = frames[0];
  49. expect(subframe).not.to.equal(webFrame);
  50. expect(subframe.parent).to.equal(webFrame);
  51. });
  52. it('can access deeply nested frames', () => {
  53. const subframe = webFrame.frames[0];
  54. expect(subframe).not.to.equal(webFrame);
  55. expect(subframe.parent).to.equal(webFrame);
  56. const nestedSubframe = subframe.frames[0];
  57. expect(nestedSubframe).not.to.equal(webFrame);
  58. expect(nestedSubframe).not.to.equal(subframe);
  59. expect(nestedSubframe.parent).to.equal(subframe);
  60. });
  61. it('can traverse all frames in root', () => {
  62. const urls = webFrame.framesInSubtree.map(frame => frame.url);
  63. expect(urls).to.deep.equal([
  64. fileUrl('frame-with-frame-container.html'),
  65. fileUrl('frame-with-frame.html'),
  66. fileUrl('frame.html')
  67. ]);
  68. });
  69. it('can traverse all frames in subtree', () => {
  70. const urls = webFrame.frames[0].framesInSubtree.map(frame => frame.url);
  71. expect(urls).to.deep.equal([
  72. fileUrl('frame-with-frame.html'),
  73. fileUrl('frame.html')
  74. ]);
  75. });
  76. describe('cross-origin', () => {
  77. let serverA = null as unknown as Server;
  78. let serverB = null as unknown as Server;
  79. before(async () => {
  80. serverA = await createServer();
  81. serverB = await createServer();
  82. });
  83. after(() => {
  84. serverA.server.close();
  85. serverB.server.close();
  86. });
  87. it('can access cross-origin frames', async () => {
  88. await w.loadURL(`${serverA.url}?frameSrc=${serverB.url}`);
  89. webFrame = w.webContents.mainFrame;
  90. expect(webFrame.url.startsWith(serverA.url)).to.be.true();
  91. expect(webFrame.frames[0].url).to.equal(serverB.url);
  92. });
  93. });
  94. });
  95. describe('WebFrame.url', () => {
  96. it('should report correct address for each subframe', async () => {
  97. const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } });
  98. await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
  99. const webFrame = w.webContents.mainFrame;
  100. expect(webFrame.url).to.equal(fileUrl('frame-with-frame-container.html'));
  101. expect(webFrame.frames[0].url).to.equal(fileUrl('frame-with-frame.html'));
  102. expect(webFrame.frames[0].frames[0].url).to.equal(fileUrl('frame.html'));
  103. });
  104. });
  105. describe('WebFrame IDs', () => {
  106. it('has properties for various identifiers', async () => {
  107. const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } });
  108. await w.loadFile(path.join(subframesPath, 'frame.html'));
  109. const webFrame = w.webContents.mainFrame;
  110. expect(webFrame).to.have.ownProperty('url').that.is.a('string');
  111. expect(webFrame).to.have.ownProperty('frameTreeNodeId').that.is.a('number');
  112. expect(webFrame).to.have.ownProperty('name').that.is.a('string');
  113. expect(webFrame).to.have.ownProperty('osProcessId').that.is.a('number');
  114. expect(webFrame).to.have.ownProperty('processId').that.is.a('number');
  115. expect(webFrame).to.have.ownProperty('routingId').that.is.a('number');
  116. });
  117. });
  118. describe('WebFrame.visibilityState', () => {
  119. // TODO(MarshallOfSound): Fix flaky test
  120. // @flaky-test
  121. it.skip('should match window state', async () => {
  122. const w = new BrowserWindow({ show: true });
  123. await w.loadURL('about:blank');
  124. const webFrame = w.webContents.mainFrame;
  125. expect(webFrame.visibilityState).to.equal('visible');
  126. w.hide();
  127. await expect(
  128. waitUntil(() => webFrame.visibilityState === 'hidden')
  129. ).to.eventually.be.fulfilled();
  130. });
  131. });
  132. describe('WebFrame.executeJavaScript', () => {
  133. it('can inject code into any subframe', async () => {
  134. const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } });
  135. await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
  136. const webFrame = w.webContents.mainFrame;
  137. const getUrl = (frame: WebFrameMain) => frame.executeJavaScript('location.href');
  138. expect(await getUrl(webFrame)).to.equal(fileUrl('frame-with-frame-container.html'));
  139. expect(await getUrl(webFrame.frames[0])).to.equal(fileUrl('frame-with-frame.html'));
  140. expect(await getUrl(webFrame.frames[0].frames[0])).to.equal(fileUrl('frame.html'));
  141. });
  142. });
  143. describe('WebFrame.reload', () => {
  144. it('reloads a frame', async () => {
  145. const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } });
  146. await w.loadFile(path.join(subframesPath, 'frame.html'));
  147. const webFrame = w.webContents.mainFrame;
  148. await webFrame.executeJavaScript('window.TEMP = 1', false);
  149. expect(webFrame.reload()).to.be.true();
  150. await emittedOnce(w.webContents, 'dom-ready');
  151. expect(await webFrame.executeJavaScript('window.TEMP', false)).to.be.null();
  152. });
  153. });
  154. describe('WebFrame.send', () => {
  155. it('works', async () => {
  156. const w = new BrowserWindow({
  157. show: false,
  158. webPreferences: {
  159. preload: path.join(subframesPath, 'preload.js'),
  160. nodeIntegrationInSubFrames: true
  161. }
  162. });
  163. await w.loadURL('about:blank');
  164. const webFrame = w.webContents.mainFrame;
  165. const pongPromise = emittedOnce(ipcMain, 'preload-pong');
  166. webFrame.send('preload-ping');
  167. const [, routingId] = await pongPromise;
  168. expect(routingId).to.equal(webFrame.routingId);
  169. });
  170. });
  171. describe('RenderFrame lifespan', () => {
  172. let w: BrowserWindow;
  173. beforeEach(async () => {
  174. w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } });
  175. });
  176. it('throws upon accessing properties when disposed', async () => {
  177. await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
  178. const { mainFrame } = w.webContents;
  179. w.destroy();
  180. // Wait for WebContents, and thus RenderFrameHost, to be destroyed.
  181. await new Promise(resolve => setTimeout(resolve, 0));
  182. expect(() => mainFrame.url).to.throw();
  183. });
  184. it('persists through cross-origin navigation', async () => {
  185. const server = await createServer();
  186. // 'localhost' is treated as a separate origin.
  187. const crossOriginUrl = server.url.replace('127.0.0.1', 'localhost');
  188. await w.loadURL(server.url);
  189. const { mainFrame } = w.webContents;
  190. expect(mainFrame.url).to.equal(server.url);
  191. await w.loadURL(crossOriginUrl);
  192. expect(w.webContents.mainFrame).to.equal(mainFrame);
  193. expect(mainFrame.url).to.equal(crossOriginUrl);
  194. });
  195. });
  196. describe('webFrameMain.fromId', () => {
  197. it('returns undefined for unknown IDs', () => {
  198. expect(webFrameMain.fromId(0, 0)).to.be.undefined();
  199. });
  200. it('can find each frame from navigation events', async () => {
  201. const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } });
  202. // frame-with-frame-container.html, frame-with-frame.html, frame.html
  203. const didFrameFinishLoad = emittedNTimes(w.webContents, 'did-frame-finish-load', 3);
  204. w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
  205. for (const [, isMainFrame, frameProcessId, frameRoutingId] of await didFrameFinishLoad) {
  206. const frame = webFrameMain.fromId(frameProcessId, frameRoutingId);
  207. expect(frame).not.to.be.null();
  208. expect(frame?.processId).to.be.equal(frameProcessId);
  209. expect(frame?.routingId).to.be.equal(frameRoutingId);
  210. expect(frame?.top === frame).to.be.equal(isMainFrame);
  211. }
  212. });
  213. });
  214. describe('"frame-created" event', () => {
  215. it('emits when the main frame is created', async () => {
  216. const w = new BrowserWindow({ show: false });
  217. const promise = emittedOnce(w.webContents, 'frame-created');
  218. w.webContents.loadFile(path.join(subframesPath, 'frame.html'));
  219. const [, details] = await promise;
  220. expect(details.frame).to.equal(w.webContents.mainFrame);
  221. });
  222. it('emits when nested frames are created', async () => {
  223. const w = new BrowserWindow({ show: false });
  224. const promise = emittedNTimes(w.webContents, 'frame-created', 2);
  225. w.webContents.loadFile(path.join(subframesPath, 'frame-container.html'));
  226. const [[, mainDetails], [, nestedDetails]] = await promise;
  227. expect(mainDetails.frame).to.equal(w.webContents.mainFrame);
  228. expect(nestedDetails.frame).to.equal(w.webContents.mainFrame.frames[0]);
  229. });
  230. it('is not emitted upon cross-origin navigation', async () => {
  231. const server = await createServer();
  232. // HACK: Use 'localhost' instead of '127.0.0.1' so Chromium treats it as
  233. // a separate origin because differing ports aren't enough 🤔
  234. const secondUrl = `http://localhost:${new URL(server.url).port}`;
  235. const w = new BrowserWindow({ show: false });
  236. await w.webContents.loadURL(server.url);
  237. let frameCreatedEmitted = false;
  238. w.webContents.once('frame-created', () => {
  239. frameCreatedEmitted = true;
  240. });
  241. await w.webContents.loadURL(secondUrl);
  242. expect(frameCreatedEmitted).to.be.false();
  243. });
  244. });
  245. describe('"dom-ready" event', () => {
  246. it('emits for top-level frame', async () => {
  247. const w = new BrowserWindow({ show: false });
  248. const promise = emittedOnce(w.webContents.mainFrame, 'dom-ready');
  249. w.webContents.loadURL('about:blank');
  250. await promise;
  251. });
  252. it('emits for sub frame', async () => {
  253. const w = new BrowserWindow({ show: false });
  254. const promise = new Promise<void>(resolve => {
  255. w.webContents.on('frame-created', (e, { frame }) => {
  256. frame.on('dom-ready', () => {
  257. if (frame.name === 'frameA') {
  258. resolve();
  259. }
  260. });
  261. });
  262. });
  263. w.webContents.loadFile(path.join(subframesPath, 'frame-with-frame.html'));
  264. await promise;
  265. });
  266. });
  267. });