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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  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, app, WebContents } from 'electron/main';
  6. import { closeAllWindows } from './lib/window-helpers';
  7. import { emittedOnce, emittedNTimes } from './lib/events-helpers';
  8. import { AddressInfo } from 'net';
  9. import { defer, ifit, waitUntil } from './lib/spec-helpers';
  10. describe('webFrameMain module', () => {
  11. const fixtures = path.resolve(__dirname, '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 });
  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: Server;
  78. let serverB: 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 });
  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.origin', () => {
  106. it('should be null for a fresh WebContents', () => {
  107. const w = new BrowserWindow({ show: false });
  108. expect(w.webContents.mainFrame.origin).to.equal('null');
  109. });
  110. it('should be file:// for file frames', async () => {
  111. const w = new BrowserWindow({ show: false });
  112. await w.loadFile(path.join(fixtures, 'pages', 'blank.html'));
  113. expect(w.webContents.mainFrame.origin).to.equal('file://');
  114. });
  115. it('should be http:// for an http frame', async () => {
  116. const w = new BrowserWindow({ show: false });
  117. const s = await createServer();
  118. defer(() => s.server.close());
  119. await w.loadURL(s.url);
  120. expect(w.webContents.mainFrame.origin).to.equal(s.url.replace(/\/$/, ''));
  121. });
  122. it('should show parent origin when child page is about:blank', async () => {
  123. const w = new BrowserWindow({ show: false });
  124. await w.loadFile(path.join(fixtures, 'pages', 'blank.html'));
  125. const webContentsCreated: Promise<[unknown, WebContents]> = emittedOnce(app, 'web-contents-created') as any;
  126. expect(w.webContents.mainFrame.origin).to.equal('file://');
  127. await w.webContents.executeJavaScript('window.open("", null, "show=false"), null');
  128. const [, childWebContents] = await webContentsCreated;
  129. expect(childWebContents.mainFrame.origin).to.equal('file://');
  130. });
  131. it('should show parent frame\'s origin when about:blank child window opened through cross-origin subframe', async () => {
  132. const w = new BrowserWindow({ show: false });
  133. const serverA = await createServer();
  134. const serverB = await createServer();
  135. defer(() => {
  136. serverA.server.close();
  137. serverB.server.close();
  138. });
  139. await w.loadURL(serverA.url + '?frameSrc=' + encodeURIComponent(serverB.url));
  140. const { mainFrame } = w.webContents;
  141. expect(mainFrame.origin).to.equal(serverA.url.replace(/\/$/, ''));
  142. const [childFrame] = mainFrame.frames;
  143. expect(childFrame.origin).to.equal(serverB.url.replace(/\/$/, ''));
  144. const webContentsCreated: Promise<[unknown, WebContents]> = emittedOnce(app, 'web-contents-created') as any;
  145. await childFrame.executeJavaScript('window.open("", null, "show=false"), null');
  146. const [, childWebContents] = await webContentsCreated;
  147. expect(childWebContents.mainFrame.origin).to.equal(childFrame.origin);
  148. });
  149. });
  150. describe('WebFrame IDs', () => {
  151. it('has properties for various identifiers', async () => {
  152. const w = new BrowserWindow({ show: false });
  153. await w.loadFile(path.join(subframesPath, 'frame.html'));
  154. const webFrame = w.webContents.mainFrame;
  155. expect(webFrame).to.have.property('url').that.is.a('string');
  156. expect(webFrame).to.have.property('frameTreeNodeId').that.is.a('number');
  157. expect(webFrame).to.have.property('name').that.is.a('string');
  158. expect(webFrame).to.have.property('osProcessId').that.is.a('number');
  159. expect(webFrame).to.have.property('processId').that.is.a('number');
  160. expect(webFrame).to.have.property('routingId').that.is.a('number');
  161. });
  162. });
  163. describe('WebFrame.visibilityState', () => {
  164. // TODO(MarshallOfSound): Fix flaky test
  165. // @flaky-test
  166. it.skip('should match window state', async () => {
  167. const w = new BrowserWindow({ show: true });
  168. await w.loadURL('about:blank');
  169. const webFrame = w.webContents.mainFrame;
  170. expect(webFrame.visibilityState).to.equal('visible');
  171. w.hide();
  172. await expect(
  173. waitUntil(() => webFrame.visibilityState === 'hidden')
  174. ).to.eventually.be.fulfilled();
  175. });
  176. });
  177. describe('WebFrame.executeJavaScript', () => {
  178. it('can inject code into any subframe', async () => {
  179. const w = new BrowserWindow({ show: false });
  180. await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
  181. const webFrame = w.webContents.mainFrame;
  182. const getUrl = (frame: WebFrameMain) => frame.executeJavaScript('location.href');
  183. expect(await getUrl(webFrame)).to.equal(fileUrl('frame-with-frame-container.html'));
  184. expect(await getUrl(webFrame.frames[0])).to.equal(fileUrl('frame-with-frame.html'));
  185. expect(await getUrl(webFrame.frames[0].frames[0])).to.equal(fileUrl('frame.html'));
  186. });
  187. it('can resolve promise', async () => {
  188. const w = new BrowserWindow({ show: false });
  189. await w.loadFile(path.join(subframesPath, 'frame.html'));
  190. const webFrame = w.webContents.mainFrame;
  191. const p = () => webFrame.executeJavaScript('new Promise(resolve => setTimeout(resolve(42), 2000));');
  192. const result = await p();
  193. expect(result).to.equal(42);
  194. });
  195. it('can reject with error', async () => {
  196. const w = new BrowserWindow({ show: false });
  197. await w.loadFile(path.join(subframesPath, 'frame.html'));
  198. const webFrame = w.webContents.mainFrame;
  199. const p = () => webFrame.executeJavaScript('new Promise((r,e) => setTimeout(e("error!"), 500));');
  200. await expect(p()).to.be.eventually.rejectedWith('error!');
  201. const errorTypes = new Set([
  202. Error,
  203. ReferenceError,
  204. EvalError,
  205. RangeError,
  206. SyntaxError,
  207. TypeError,
  208. URIError
  209. ]);
  210. for (const error of errorTypes) {
  211. await expect(webFrame.executeJavaScript(`Promise.reject(new ${error.name}("Wamp-wamp"))`))
  212. .to.eventually.be.rejectedWith(/Error/);
  213. }
  214. });
  215. it('can reject when script execution fails', async () => {
  216. const w = new BrowserWindow({ show: false });
  217. await w.loadFile(path.join(subframesPath, 'frame.html'));
  218. const webFrame = w.webContents.mainFrame;
  219. const p = () => webFrame.executeJavaScript('console.log(test)');
  220. await expect(p()).to.be.eventually.rejectedWith(/ReferenceError/);
  221. });
  222. });
  223. describe('WebFrame.reload', () => {
  224. it('reloads a frame', async () => {
  225. const w = new BrowserWindow({ show: false });
  226. await w.loadFile(path.join(subframesPath, 'frame.html'));
  227. const webFrame = w.webContents.mainFrame;
  228. await webFrame.executeJavaScript('window.TEMP = 1', false);
  229. expect(webFrame.reload()).to.be.true();
  230. await emittedOnce(w.webContents, 'dom-ready');
  231. expect(await webFrame.executeJavaScript('window.TEMP', false)).to.be.null();
  232. });
  233. });
  234. describe('WebFrame.send', () => {
  235. it('works', async () => {
  236. const w = new BrowserWindow({
  237. show: false,
  238. webPreferences: {
  239. preload: path.join(subframesPath, 'preload.js'),
  240. nodeIntegrationInSubFrames: true
  241. }
  242. });
  243. await w.loadURL('about:blank');
  244. const webFrame = w.webContents.mainFrame;
  245. const pongPromise = emittedOnce(ipcMain, 'preload-pong');
  246. webFrame.send('preload-ping');
  247. const [, routingId] = await pongPromise;
  248. expect(routingId).to.equal(webFrame.routingId);
  249. });
  250. });
  251. describe('RenderFrame lifespan', () => {
  252. let w: BrowserWindow;
  253. beforeEach(async () => {
  254. w = new BrowserWindow({ show: false });
  255. });
  256. // TODO(jkleinsc) fix this flaky test on linux
  257. ifit(process.platform !== 'linux')('throws upon accessing properties when disposed', async () => {
  258. await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
  259. const { mainFrame } = w.webContents;
  260. w.destroy();
  261. // Wait for WebContents, and thus RenderFrameHost, to be destroyed.
  262. await new Promise(resolve => setTimeout(resolve, 0));
  263. expect(() => mainFrame.url).to.throw();
  264. });
  265. it('persists through cross-origin navigation', async () => {
  266. const server = await createServer();
  267. // 'localhost' is treated as a separate origin.
  268. const crossOriginUrl = server.url.replace('127.0.0.1', 'localhost');
  269. await w.loadURL(server.url);
  270. const { mainFrame } = w.webContents;
  271. expect(mainFrame.url).to.equal(server.url);
  272. await w.loadURL(crossOriginUrl);
  273. expect(w.webContents.mainFrame).to.equal(mainFrame);
  274. expect(mainFrame.url).to.equal(crossOriginUrl);
  275. });
  276. it('recovers from renderer crash on same-origin', async () => {
  277. const server = await createServer();
  278. // Keep reference to mainFrame alive throughout crash and recovery.
  279. const { mainFrame } = w.webContents;
  280. await w.webContents.loadURL(server.url);
  281. const crashEvent = emittedOnce(w.webContents, 'render-process-gone');
  282. w.webContents.forcefullyCrashRenderer();
  283. await crashEvent;
  284. await w.webContents.loadURL(server.url);
  285. // Log just to keep mainFrame in scope.
  286. console.log('mainFrame.url', mainFrame.url);
  287. });
  288. // Fixed by #34411
  289. it('recovers from renderer crash on cross-origin', async () => {
  290. const server = await createServer();
  291. // 'localhost' is treated as a separate origin.
  292. const crossOriginUrl = server.url.replace('127.0.0.1', 'localhost');
  293. // Keep reference to mainFrame alive throughout crash and recovery.
  294. const { mainFrame } = w.webContents;
  295. await w.webContents.loadURL(server.url);
  296. const crashEvent = emittedOnce(w.webContents, 'render-process-gone');
  297. w.webContents.forcefullyCrashRenderer();
  298. await crashEvent;
  299. // A short wait seems to be required to reproduce the crash.
  300. await new Promise(resolve => setTimeout(resolve, 100));
  301. await w.webContents.loadURL(crossOriginUrl);
  302. // Log just to keep mainFrame in scope.
  303. console.log('mainFrame.url', mainFrame.url);
  304. });
  305. });
  306. describe('webFrameMain.fromId', () => {
  307. it('returns undefined for unknown IDs', () => {
  308. expect(webFrameMain.fromId(0, 0)).to.be.undefined();
  309. });
  310. it('can find each frame from navigation events', async () => {
  311. const w = new BrowserWindow({ show: false });
  312. // frame-with-frame-container.html, frame-with-frame.html, frame.html
  313. const didFrameFinishLoad = emittedNTimes(w.webContents, 'did-frame-finish-load', 3);
  314. w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
  315. for (const [, isMainFrame, frameProcessId, frameRoutingId] of await didFrameFinishLoad) {
  316. const frame = webFrameMain.fromId(frameProcessId, frameRoutingId);
  317. expect(frame).not.to.be.null();
  318. expect(frame?.processId).to.be.equal(frameProcessId);
  319. expect(frame?.routingId).to.be.equal(frameRoutingId);
  320. expect(frame?.top === frame).to.be.equal(isMainFrame);
  321. }
  322. });
  323. });
  324. describe('"frame-created" event', () => {
  325. it('emits when the main frame is created', async () => {
  326. const w = new BrowserWindow({ show: false });
  327. const promise = emittedOnce(w.webContents, 'frame-created');
  328. w.webContents.loadFile(path.join(subframesPath, 'frame.html'));
  329. const [, details] = await promise;
  330. expect(details.frame).to.equal(w.webContents.mainFrame);
  331. });
  332. it('emits when nested frames are created', async () => {
  333. const w = new BrowserWindow({ show: false });
  334. const promise = emittedNTimes(w.webContents, 'frame-created', 2);
  335. w.webContents.loadFile(path.join(subframesPath, 'frame-container.html'));
  336. const [[, mainDetails], [, nestedDetails]] = await promise;
  337. expect(mainDetails.frame).to.equal(w.webContents.mainFrame);
  338. expect(nestedDetails.frame).to.equal(w.webContents.mainFrame.frames[0]);
  339. });
  340. it('is not emitted upon cross-origin navigation', async () => {
  341. const server = await createServer();
  342. // HACK: Use 'localhost' instead of '127.0.0.1' so Chromium treats it as
  343. // a separate origin because differing ports aren't enough 🤔
  344. const secondUrl = `http://localhost:${new URL(server.url).port}`;
  345. const w = new BrowserWindow({ show: false });
  346. await w.webContents.loadURL(server.url);
  347. let frameCreatedEmitted = false;
  348. w.webContents.once('frame-created', () => {
  349. frameCreatedEmitted = true;
  350. });
  351. await w.webContents.loadURL(secondUrl);
  352. expect(frameCreatedEmitted).to.be.false();
  353. });
  354. });
  355. describe('"dom-ready" event', () => {
  356. it('emits for top-level frame', async () => {
  357. const w = new BrowserWindow({ show: false });
  358. const promise = emittedOnce(w.webContents.mainFrame, 'dom-ready');
  359. w.webContents.loadURL('about:blank');
  360. await promise;
  361. });
  362. it('emits for sub frame', async () => {
  363. const w = new BrowserWindow({ show: false });
  364. const promise = new Promise<void>(resolve => {
  365. w.webContents.on('frame-created', (e, { frame }) => {
  366. frame.on('dom-ready', () => {
  367. if (frame.name === 'frameA') {
  368. resolve();
  369. }
  370. });
  371. });
  372. });
  373. w.webContents.loadFile(path.join(subframesPath, 'frame-with-frame.html'));
  374. await promise;
  375. });
  376. });
  377. });