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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. import { BrowserWindow, WebFrameMain, webFrameMain, ipcMain, app, WebContents } from 'electron/main';
  2. import { expect } from 'chai';
  3. import { once } from 'node:events';
  4. import * as http from 'node:http';
  5. import * as path from 'node:path';
  6. import { setTimeout } from 'node:timers/promises';
  7. import * as url from 'node:url';
  8. import { emittedNTimes } from './lib/events-helpers';
  9. import { defer, ifit, listen, waitUntil } from './lib/spec-helpers';
  10. import { closeAllWindows } from './lib/window-helpers';
  11. describe('webFrameMain module', () => {
  12. const fixtures = path.resolve(__dirname, 'fixtures');
  13. const subframesPath = path.join(fixtures, 'sub-frames');
  14. const fileUrl = (filename: string) => url.pathToFileURL(path.join(subframesPath, filename)).href;
  15. type Server = { server: http.Server, url: string }
  16. /** Creates an HTTP server whose handler embeds the given iframe src. */
  17. const createServer = async () => {
  18. const server = http.createServer((req, res) => {
  19. const params = new URLSearchParams(new URL(req.url || '', `http://${req.headers.host}`).search || '');
  20. if (params.has('frameSrc')) {
  21. res.end(`<iframe src="${params.get('frameSrc')}"></iframe>`);
  22. } else {
  23. res.end('');
  24. }
  25. });
  26. return { server, url: (await listen(server)).url + '/' };
  27. };
  28. afterEach(closeAllWindows);
  29. describe('WebFrame traversal APIs', () => {
  30. let w: BrowserWindow;
  31. let webFrame: WebFrameMain;
  32. beforeEach(async () => {
  33. w = new BrowserWindow({ show: false });
  34. await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
  35. webFrame = w.webContents.mainFrame;
  36. });
  37. it('can access top frame', () => {
  38. expect(webFrame.top).to.equal(webFrame);
  39. });
  40. it('has no parent on top frame', () => {
  41. expect(webFrame.parent).to.be.null();
  42. });
  43. it('can access immediate frame descendents', () => {
  44. const { frames } = webFrame;
  45. expect(frames).to.have.lengthOf(1);
  46. const subframe = frames[0];
  47. expect(subframe).not.to.equal(webFrame);
  48. expect(subframe.parent).to.equal(webFrame);
  49. });
  50. it('can access deeply nested frames', () => {
  51. const subframe = webFrame.frames[0];
  52. expect(subframe).not.to.equal(webFrame);
  53. expect(subframe.parent).to.equal(webFrame);
  54. const nestedSubframe = subframe.frames[0];
  55. expect(nestedSubframe).not.to.equal(webFrame);
  56. expect(nestedSubframe).not.to.equal(subframe);
  57. expect(nestedSubframe.parent).to.equal(subframe);
  58. });
  59. it('can traverse all frames in root', () => {
  60. const urls = webFrame.framesInSubtree.map(frame => frame.url);
  61. expect(urls).to.deep.equal([
  62. fileUrl('frame-with-frame-container.html'),
  63. fileUrl('frame-with-frame.html'),
  64. fileUrl('frame.html')
  65. ]);
  66. });
  67. it('can traverse all frames in subtree', () => {
  68. const urls = webFrame.frames[0].framesInSubtree.map(frame => frame.url);
  69. expect(urls).to.deep.equal([
  70. fileUrl('frame-with-frame.html'),
  71. fileUrl('frame.html')
  72. ]);
  73. });
  74. describe('cross-origin', () => {
  75. let serverA: Server;
  76. let serverB: Server;
  77. before(async () => {
  78. serverA = await createServer();
  79. serverB = await createServer();
  80. });
  81. after(() => {
  82. serverA.server.close();
  83. serverB.server.close();
  84. });
  85. it('can access cross-origin frames', async () => {
  86. await w.loadURL(`${serverA.url}?frameSrc=${serverB.url}`);
  87. webFrame = w.webContents.mainFrame;
  88. expect(webFrame.url.startsWith(serverA.url)).to.be.true();
  89. expect(webFrame.frames[0].url).to.equal(serverB.url);
  90. });
  91. });
  92. });
  93. describe('WebFrame.url', () => {
  94. it('should report correct address for each subframe', async () => {
  95. const w = new BrowserWindow({ show: false });
  96. await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
  97. const webFrame = w.webContents.mainFrame;
  98. expect(webFrame.url).to.equal(fileUrl('frame-with-frame-container.html'));
  99. expect(webFrame.frames[0].url).to.equal(fileUrl('frame-with-frame.html'));
  100. expect(webFrame.frames[0].frames[0].url).to.equal(fileUrl('frame.html'));
  101. });
  102. });
  103. describe('WebFrame.origin', () => {
  104. it('should be null for a fresh WebContents', () => {
  105. const w = new BrowserWindow({ show: false });
  106. expect(w.webContents.mainFrame.origin).to.equal('null');
  107. });
  108. it('should be file:// for file frames', async () => {
  109. const w = new BrowserWindow({ show: false });
  110. await w.loadFile(path.join(fixtures, 'pages', 'blank.html'));
  111. expect(w.webContents.mainFrame.origin).to.equal('file://');
  112. });
  113. it('should be http:// for an http frame', async () => {
  114. const w = new BrowserWindow({ show: false });
  115. const s = await createServer();
  116. defer(() => s.server.close());
  117. await w.loadURL(s.url);
  118. expect(w.webContents.mainFrame.origin).to.equal(s.url.replace(/\/$/, ''));
  119. });
  120. it('should show parent origin when child page is about:blank', async () => {
  121. const w = new BrowserWindow({ show: false });
  122. await w.loadFile(path.join(fixtures, 'pages', 'blank.html'));
  123. const webContentsCreated = once(app, 'web-contents-created') as Promise<[any, WebContents]>;
  124. expect(w.webContents.mainFrame.origin).to.equal('file://');
  125. await w.webContents.executeJavaScript('window.open("", null, "show=false"), null');
  126. const [, childWebContents] = await webContentsCreated;
  127. expect(childWebContents.mainFrame.origin).to.equal('file://');
  128. });
  129. it('should show parent frame\'s origin when about:blank child window opened through cross-origin subframe', async () => {
  130. const w = new BrowserWindow({ show: false });
  131. const serverA = await createServer();
  132. const serverB = await createServer();
  133. defer(() => {
  134. serverA.server.close();
  135. serverB.server.close();
  136. });
  137. await w.loadURL(serverA.url + '?frameSrc=' + encodeURIComponent(serverB.url));
  138. const { mainFrame } = w.webContents;
  139. expect(mainFrame.origin).to.equal(serverA.url.replace(/\/$/, ''));
  140. const [childFrame] = mainFrame.frames;
  141. expect(childFrame.origin).to.equal(serverB.url.replace(/\/$/, ''));
  142. const webContentsCreated = once(app, 'web-contents-created') as Promise<[any, WebContents]>;
  143. await childFrame.executeJavaScript('window.open("", null, "show=false"), null');
  144. const [, childWebContents] = await webContentsCreated;
  145. expect(childWebContents.mainFrame.origin).to.equal(childFrame.origin);
  146. });
  147. });
  148. describe('WebFrame IDs', () => {
  149. it('has properties for various identifiers', async () => {
  150. const w = new BrowserWindow({ show: false });
  151. await w.loadFile(path.join(subframesPath, 'frame.html'));
  152. const webFrame = w.webContents.mainFrame;
  153. expect(webFrame).to.have.property('url').that.is.a('string');
  154. expect(webFrame).to.have.property('frameTreeNodeId').that.is.a('number');
  155. expect(webFrame).to.have.property('name').that.is.a('string');
  156. expect(webFrame).to.have.property('osProcessId').that.is.a('number');
  157. expect(webFrame).to.have.property('processId').that.is.a('number');
  158. expect(webFrame).to.have.property('routingId').that.is.a('number');
  159. });
  160. });
  161. describe('WebFrame.visibilityState', () => {
  162. // DISABLED-FIXME(MarshallOfSound): Fix flaky test
  163. it('should match window state', async () => {
  164. const w = new BrowserWindow({ show: true });
  165. await w.loadURL('about:blank');
  166. const webFrame = w.webContents.mainFrame;
  167. expect(webFrame.visibilityState).to.equal('visible');
  168. w.hide();
  169. await expect(
  170. waitUntil(() => webFrame.visibilityState === 'hidden')
  171. ).to.eventually.be.fulfilled();
  172. });
  173. });
  174. describe('WebFrame.executeJavaScript', () => {
  175. it('can inject code into any subframe', async () => {
  176. const w = new BrowserWindow({ show: false });
  177. await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
  178. const webFrame = w.webContents.mainFrame;
  179. const getUrl = (frame: WebFrameMain) => frame.executeJavaScript('location.href');
  180. expect(await getUrl(webFrame)).to.equal(fileUrl('frame-with-frame-container.html'));
  181. expect(await getUrl(webFrame.frames[0])).to.equal(fileUrl('frame-with-frame.html'));
  182. expect(await getUrl(webFrame.frames[0].frames[0])).to.equal(fileUrl('frame.html'));
  183. });
  184. it('can resolve promise', async () => {
  185. const w = new BrowserWindow({ show: false });
  186. await w.loadFile(path.join(subframesPath, 'frame.html'));
  187. const webFrame = w.webContents.mainFrame;
  188. const p = () => webFrame.executeJavaScript('new Promise(resolve => setTimeout(resolve(42), 2000));');
  189. const result = await p();
  190. expect(result).to.equal(42);
  191. });
  192. it('can reject with error', async () => {
  193. const w = new BrowserWindow({ show: false });
  194. await w.loadFile(path.join(subframesPath, 'frame.html'));
  195. const webFrame = w.webContents.mainFrame;
  196. const p = () => webFrame.executeJavaScript('new Promise((r,e) => setTimeout(e("error!"), 500));');
  197. await expect(p()).to.be.eventually.rejectedWith('error!');
  198. const errorTypes = new Set([
  199. Error,
  200. ReferenceError,
  201. EvalError,
  202. RangeError,
  203. SyntaxError,
  204. TypeError,
  205. URIError
  206. ]);
  207. for (const error of errorTypes) {
  208. await expect(webFrame.executeJavaScript(`Promise.reject(new ${error.name}("Wamp-wamp"))`))
  209. .to.eventually.be.rejectedWith(/Error/);
  210. }
  211. });
  212. it('can reject when script execution fails', async () => {
  213. const w = new BrowserWindow({ show: false });
  214. await w.loadFile(path.join(subframesPath, 'frame.html'));
  215. const webFrame = w.webContents.mainFrame;
  216. const p = () => webFrame.executeJavaScript('console.log(test)');
  217. await expect(p()).to.be.eventually.rejectedWith(/ReferenceError/);
  218. });
  219. });
  220. describe('WebFrame.reload', () => {
  221. it('reloads a frame', async () => {
  222. const w = new BrowserWindow({ show: false });
  223. await w.loadFile(path.join(subframesPath, 'frame.html'));
  224. const webFrame = w.webContents.mainFrame;
  225. await webFrame.executeJavaScript('window.TEMP = 1', false);
  226. expect(webFrame.reload()).to.be.true();
  227. await once(w.webContents, 'dom-ready');
  228. expect(await webFrame.executeJavaScript('window.TEMP', false)).to.be.null();
  229. });
  230. });
  231. describe('WebFrame.send', () => {
  232. it('works', async () => {
  233. const w = new BrowserWindow({
  234. show: false,
  235. webPreferences: {
  236. preload: path.join(subframesPath, 'preload.js'),
  237. nodeIntegrationInSubFrames: true
  238. }
  239. });
  240. await w.loadURL('about:blank');
  241. const webFrame = w.webContents.mainFrame;
  242. const pongPromise = once(ipcMain, 'preload-pong');
  243. webFrame.send('preload-ping');
  244. const [, routingId] = await pongPromise;
  245. expect(routingId).to.equal(webFrame.routingId);
  246. });
  247. });
  248. describe('RenderFrame lifespan', () => {
  249. let w: BrowserWindow;
  250. beforeEach(async () => {
  251. w = new BrowserWindow({ show: false });
  252. });
  253. // TODO(jkleinsc) fix this flaky test on linux
  254. ifit(process.platform !== 'linux')('throws upon accessing properties when disposed', async () => {
  255. await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
  256. const { mainFrame } = w.webContents;
  257. w.destroy();
  258. // Wait for WebContents, and thus RenderFrameHost, to be destroyed.
  259. await setTimeout();
  260. expect(() => mainFrame.url).to.throw();
  261. });
  262. it('persists through cross-origin navigation', async () => {
  263. const server = await createServer();
  264. // 'localhost' is treated as a separate origin.
  265. const crossOriginUrl = server.url.replace('127.0.0.1', 'localhost');
  266. await w.loadURL(server.url);
  267. const { mainFrame } = w.webContents;
  268. expect(mainFrame.url).to.equal(server.url);
  269. await w.loadURL(crossOriginUrl);
  270. expect(w.webContents.mainFrame).to.equal(mainFrame);
  271. expect(mainFrame.url).to.equal(crossOriginUrl);
  272. });
  273. it('recovers from renderer crash on same-origin', async () => {
  274. const server = await createServer();
  275. // Keep reference to mainFrame alive throughout crash and recovery.
  276. const { mainFrame } = w.webContents;
  277. await w.webContents.loadURL(server.url);
  278. const crashEvent = once(w.webContents, 'render-process-gone');
  279. w.webContents.forcefullyCrashRenderer();
  280. await crashEvent;
  281. await w.webContents.loadURL(server.url);
  282. // Log just to keep mainFrame in scope.
  283. console.log('mainFrame.url', mainFrame.url);
  284. });
  285. // Fixed by #34411
  286. it('recovers from renderer crash on cross-origin', async () => {
  287. const server = await createServer();
  288. // 'localhost' is treated as a separate origin.
  289. const crossOriginUrl = server.url.replace('127.0.0.1', 'localhost');
  290. // Keep reference to mainFrame alive throughout crash and recovery.
  291. const { mainFrame } = w.webContents;
  292. await w.webContents.loadURL(server.url);
  293. const crashEvent = once(w.webContents, 'render-process-gone');
  294. w.webContents.forcefullyCrashRenderer();
  295. await crashEvent;
  296. // A short wait seems to be required to reproduce the crash.
  297. await setTimeout(100);
  298. await w.webContents.loadURL(crossOriginUrl);
  299. // Log just to keep mainFrame in scope.
  300. console.log('mainFrame.url', mainFrame.url);
  301. });
  302. });
  303. describe('webFrameMain.fromId', () => {
  304. it('returns undefined for unknown IDs', () => {
  305. expect(webFrameMain.fromId(0, 0)).to.be.undefined();
  306. });
  307. it('can find each frame from navigation events', async () => {
  308. const w = new BrowserWindow({ show: false });
  309. // frame-with-frame-container.html, frame-with-frame.html, frame.html
  310. const didFrameFinishLoad = emittedNTimes(w.webContents, 'did-frame-finish-load', 3);
  311. w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
  312. for (const [, isMainFrame, frameProcessId, frameRoutingId] of await didFrameFinishLoad) {
  313. const frame = webFrameMain.fromId(frameProcessId, frameRoutingId);
  314. expect(frame).not.to.be.null();
  315. expect(frame?.processId).to.be.equal(frameProcessId);
  316. expect(frame?.routingId).to.be.equal(frameRoutingId);
  317. expect(frame?.top === frame).to.be.equal(isMainFrame);
  318. }
  319. });
  320. });
  321. describe('"frame-created" event', () => {
  322. it('emits when the main frame is created', async () => {
  323. const w = new BrowserWindow({ show: false });
  324. const promise = once(w.webContents, 'frame-created') as Promise<[any, Electron.FrameCreatedDetails]>;
  325. w.webContents.loadFile(path.join(subframesPath, 'frame.html'));
  326. const [, details] = await promise;
  327. expect(details.frame).to.equal(w.webContents.mainFrame);
  328. });
  329. it('emits when nested frames are created', async () => {
  330. const w = new BrowserWindow({ show: false });
  331. const promise = emittedNTimes(w.webContents, 'frame-created', 2) as Promise<[any, Electron.FrameCreatedDetails][]>;
  332. w.webContents.loadFile(path.join(subframesPath, 'frame-container.html'));
  333. const [[, mainDetails], [, nestedDetails]] = await promise;
  334. expect(mainDetails.frame).to.equal(w.webContents.mainFrame);
  335. expect(nestedDetails.frame).to.equal(w.webContents.mainFrame.frames[0]);
  336. });
  337. it('is not emitted upon cross-origin navigation', async () => {
  338. const server = await createServer();
  339. // HACK: Use 'localhost' instead of '127.0.0.1' so Chromium treats it as
  340. // a separate origin because differing ports aren't enough 🤔
  341. const secondUrl = server.url.replace('127.0.0.1', 'localhost');
  342. const w = new BrowserWindow({ show: false });
  343. await w.webContents.loadURL(server.url);
  344. let frameCreatedEmitted = false;
  345. w.webContents.once('frame-created', () => {
  346. frameCreatedEmitted = true;
  347. });
  348. await w.webContents.loadURL(secondUrl);
  349. expect(frameCreatedEmitted).to.be.false();
  350. });
  351. });
  352. describe('"dom-ready" event', () => {
  353. it('emits for top-level frame', async () => {
  354. const w = new BrowserWindow({ show: false });
  355. const promise = once(w.webContents.mainFrame, 'dom-ready');
  356. w.webContents.loadURL('about:blank');
  357. await promise;
  358. });
  359. it('emits for sub frame', async () => {
  360. const w = new BrowserWindow({ show: false });
  361. const promise = new Promise<void>(resolve => {
  362. w.webContents.on('frame-created', (e, { frame }) => {
  363. frame.on('dom-ready', () => {
  364. if (frame.name === 'frameA') {
  365. resolve();
  366. }
  367. });
  368. });
  369. });
  370. w.webContents.loadFile(path.join(subframesPath, 'frame-with-frame.html'));
  371. await promise;
  372. });
  373. });
  374. });