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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  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 { ifit, 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. // TODO(jkleinsc) fix this flaky test on linux
  177. ifit(process.platform !== 'linux')('throws upon accessing properties when disposed', async () => {
  178. await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
  179. const { mainFrame } = w.webContents;
  180. w.destroy();
  181. // Wait for WebContents, and thus RenderFrameHost, to be destroyed.
  182. await new Promise(resolve => setTimeout(resolve, 0));
  183. expect(() => mainFrame.url).to.throw();
  184. });
  185. it('persists through cross-origin navigation', async () => {
  186. const server = await createServer();
  187. // 'localhost' is treated as a separate origin.
  188. const crossOriginUrl = server.url.replace('127.0.0.1', 'localhost');
  189. await w.loadURL(server.url);
  190. const { mainFrame } = w.webContents;
  191. expect(mainFrame.url).to.equal(server.url);
  192. await w.loadURL(crossOriginUrl);
  193. expect(w.webContents.mainFrame).to.equal(mainFrame);
  194. expect(mainFrame.url).to.equal(crossOriginUrl);
  195. });
  196. it('recovers from renderer crash on same-origin', async () => {
  197. const server = await createServer();
  198. // Keep reference to mainFrame alive throughout crash and recovery.
  199. const { mainFrame } = w.webContents;
  200. await w.webContents.loadURL(server.url);
  201. const crashEvent = emittedOnce(w.webContents, 'render-process-gone');
  202. w.webContents.forcefullyCrashRenderer();
  203. await crashEvent;
  204. await w.webContents.loadURL(server.url);
  205. // Log just to keep mainFrame in scope.
  206. console.log('mainFrame.url', mainFrame.url);
  207. });
  208. // Fixed by #34411
  209. it('recovers from renderer crash on cross-origin', async () => {
  210. const server = await createServer();
  211. // 'localhost' is treated as a separate origin.
  212. const crossOriginUrl = server.url.replace('127.0.0.1', 'localhost');
  213. // Keep reference to mainFrame alive throughout crash and recovery.
  214. const { mainFrame } = w.webContents;
  215. await w.webContents.loadURL(server.url);
  216. const crashEvent = emittedOnce(w.webContents, 'render-process-gone');
  217. w.webContents.forcefullyCrashRenderer();
  218. await crashEvent;
  219. // A short wait seems to be required to reproduce the crash.
  220. await new Promise(resolve => setTimeout(resolve, 100));
  221. await w.webContents.loadURL(crossOriginUrl);
  222. // Log just to keep mainFrame in scope.
  223. console.log('mainFrame.url', mainFrame.url);
  224. });
  225. });
  226. describe('webFrameMain.fromId', () => {
  227. it('returns undefined for unknown IDs', () => {
  228. expect(webFrameMain.fromId(0, 0)).to.be.undefined();
  229. });
  230. it('can find each frame from navigation events', async () => {
  231. const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } });
  232. // frame-with-frame-container.html, frame-with-frame.html, frame.html
  233. const didFrameFinishLoad = emittedNTimes(w.webContents, 'did-frame-finish-load', 3);
  234. w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
  235. for (const [, isMainFrame, frameProcessId, frameRoutingId] of await didFrameFinishLoad) {
  236. const frame = webFrameMain.fromId(frameProcessId, frameRoutingId);
  237. expect(frame).not.to.be.null();
  238. expect(frame?.processId).to.be.equal(frameProcessId);
  239. expect(frame?.routingId).to.be.equal(frameRoutingId);
  240. expect(frame?.top === frame).to.be.equal(isMainFrame);
  241. }
  242. });
  243. });
  244. describe('"frame-created" event', () => {
  245. it('emits when the main frame is created', async () => {
  246. const w = new BrowserWindow({ show: false });
  247. const promise = emittedOnce(w.webContents, 'frame-created');
  248. w.webContents.loadFile(path.join(subframesPath, 'frame.html'));
  249. const [, details] = await promise;
  250. expect(details.frame).to.equal(w.webContents.mainFrame);
  251. });
  252. it('emits when nested frames are created', async () => {
  253. const w = new BrowserWindow({ show: false });
  254. const promise = emittedNTimes(w.webContents, 'frame-created', 2);
  255. w.webContents.loadFile(path.join(subframesPath, 'frame-container.html'));
  256. const [[, mainDetails], [, nestedDetails]] = await promise;
  257. expect(mainDetails.frame).to.equal(w.webContents.mainFrame);
  258. expect(nestedDetails.frame).to.equal(w.webContents.mainFrame.frames[0]);
  259. });
  260. it('is not emitted upon cross-origin navigation', async () => {
  261. const server = await createServer();
  262. // HACK: Use 'localhost' instead of '127.0.0.1' so Chromium treats it as
  263. // a separate origin because differing ports aren't enough 🤔
  264. const secondUrl = `http://localhost:${new URL(server.url).port}`;
  265. const w = new BrowserWindow({ show: false });
  266. await w.webContents.loadURL(server.url);
  267. let frameCreatedEmitted = false;
  268. w.webContents.once('frame-created', () => {
  269. frameCreatedEmitted = true;
  270. });
  271. await w.webContents.loadURL(secondUrl);
  272. expect(frameCreatedEmitted).to.be.false();
  273. });
  274. });
  275. describe('"dom-ready" event', () => {
  276. it('emits for top-level frame', async () => {
  277. const w = new BrowserWindow({ show: false });
  278. const promise = emittedOnce(w.webContents.mainFrame, 'dom-ready');
  279. w.webContents.loadURL('about:blank');
  280. await promise;
  281. });
  282. it('emits for sub frame', async () => {
  283. const w = new BrowserWindow({ show: false });
  284. const promise = new Promise<void>(resolve => {
  285. w.webContents.on('frame-created', (e, { frame }) => {
  286. frame.on('dom-ready', () => {
  287. if (frame.name === 'frameA') {
  288. resolve();
  289. }
  290. });
  291. });
  292. });
  293. w.webContents.loadFile(path.join(subframesPath, 'frame-with-frame.html'));
  294. await promise;
  295. });
  296. });
  297. });