api-debugger-spec.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import { expect } from 'chai';
  2. import * as http from 'node:http';
  3. import * as path from 'node:path';
  4. import { BrowserWindow } from 'electron/main';
  5. import { closeAllWindows } from './lib/window-helpers';
  6. import { emittedUntil } from './lib/events-helpers';
  7. import { listen } from './lib/spec-helpers';
  8. import { once } from 'node:events';
  9. describe('debugger module', () => {
  10. const fixtures = path.resolve(__dirname, 'fixtures');
  11. let w: BrowserWindow;
  12. beforeEach(() => {
  13. w = new BrowserWindow({
  14. show: false,
  15. width: 400,
  16. height: 400
  17. });
  18. });
  19. afterEach(closeAllWindows);
  20. describe('debugger.attach', () => {
  21. it('succeeds when devtools is already open', async () => {
  22. await w.webContents.loadURL('about:blank');
  23. w.webContents.openDevTools();
  24. w.webContents.debugger.attach();
  25. expect(w.webContents.debugger.isAttached()).to.be.true();
  26. });
  27. it('fails when protocol version is not supported', () => {
  28. expect(() => w.webContents.debugger.attach('2.0')).to.throw();
  29. expect(w.webContents.debugger.isAttached()).to.be.false();
  30. });
  31. it('attaches when no protocol version is specified', () => {
  32. w.webContents.debugger.attach();
  33. expect(w.webContents.debugger.isAttached()).to.be.true();
  34. });
  35. });
  36. describe('debugger.detach', () => {
  37. it('fires detach event', async () => {
  38. const detach = once(w.webContents.debugger, 'detach');
  39. w.webContents.debugger.attach();
  40. w.webContents.debugger.detach();
  41. const [, reason] = await detach;
  42. expect(reason).to.equal('target closed');
  43. expect(w.webContents.debugger.isAttached()).to.be.false();
  44. });
  45. it('doesn\'t disconnect an active devtools session', async () => {
  46. w.webContents.loadURL('about:blank');
  47. const detach = once(w.webContents.debugger, 'detach');
  48. w.webContents.debugger.attach();
  49. w.webContents.openDevTools();
  50. w.webContents.once('devtools-opened', () => {
  51. w.webContents.debugger.detach();
  52. });
  53. await detach;
  54. expect(w.webContents.debugger.isAttached()).to.be.false();
  55. expect(w.devToolsWebContents.isDestroyed()).to.be.false();
  56. });
  57. });
  58. describe('debugger.sendCommand', () => {
  59. let server: http.Server;
  60. afterEach(() => {
  61. if (server != null) {
  62. server.close();
  63. server = null as any;
  64. }
  65. });
  66. it('returns response', async () => {
  67. w.webContents.loadURL('about:blank');
  68. w.webContents.debugger.attach();
  69. const params = { expression: '4+2' };
  70. const res = await w.webContents.debugger.sendCommand('Runtime.evaluate', params);
  71. expect(res.wasThrown).to.be.undefined();
  72. expect(res.result.value).to.equal(6);
  73. w.webContents.debugger.detach();
  74. });
  75. it('returns response when devtools is opened', async () => {
  76. w.webContents.loadURL('about:blank');
  77. w.webContents.debugger.attach();
  78. const opened = once(w.webContents, 'devtools-opened');
  79. w.webContents.openDevTools();
  80. await opened;
  81. const params = { expression: '4+2' };
  82. const res = await w.webContents.debugger.sendCommand('Runtime.evaluate', params);
  83. expect(res.wasThrown).to.be.undefined();
  84. expect(res.result.value).to.equal(6);
  85. w.webContents.debugger.detach();
  86. });
  87. it('fires message event', async () => {
  88. const url = process.platform !== 'win32'
  89. ? `file://${path.join(fixtures, 'pages', 'a.html')}`
  90. : `file:///${path.join(fixtures, 'pages', 'a.html').replaceAll('\\', '/')}`;
  91. w.webContents.loadURL(url);
  92. w.webContents.debugger.attach();
  93. const message = emittedUntil(w.webContents.debugger, 'message',
  94. (event: Electron.Event, method: string) => method === 'Console.messageAdded');
  95. w.webContents.debugger.sendCommand('Console.enable');
  96. const [,, params] = await message;
  97. w.webContents.debugger.detach();
  98. expect(params.message.level).to.equal('log');
  99. expect(params.message.url).to.equal(url);
  100. expect(params.message.text).to.equal('a');
  101. });
  102. it('returns error message when command fails', async () => {
  103. w.webContents.loadURL('about:blank');
  104. w.webContents.debugger.attach();
  105. const promise = w.webContents.debugger.sendCommand('Test');
  106. await expect(promise).to.be.eventually.rejectedWith(Error, "'Test' wasn't found");
  107. w.webContents.debugger.detach();
  108. });
  109. it('handles valid unicode characters in message', async () => {
  110. server = http.createServer((req, res) => {
  111. res.setHeader('Content-Type', 'text/plain; charset=utf-8');
  112. res.end('\u0024');
  113. });
  114. const { url } = await listen(server);
  115. w.loadURL(url);
  116. // If we do this synchronously, it's fast enough to attach and enable
  117. // network capture before the load. If we do it before the loadURL, for
  118. // some reason network capture doesn't get enabled soon enough and we get
  119. // an error when calling `Network.getResponseBody`.
  120. w.webContents.debugger.attach();
  121. w.webContents.debugger.sendCommand('Network.enable');
  122. const [,, { requestId }] = await emittedUntil(w.webContents.debugger, 'message', (_event: any, method: string, params: any) =>
  123. method === 'Network.responseReceived' && params.response.url.startsWith('http://127.0.0.1'));
  124. await emittedUntil(w.webContents.debugger, 'message', (_event: any, method: string, params: any) =>
  125. method === 'Network.loadingFinished' && params.requestId === requestId);
  126. const { body } = await w.webContents.debugger.sendCommand('Network.getResponseBody', { requestId });
  127. expect(body).to.equal('\u0024');
  128. });
  129. it('does not crash for invalid unicode characters in message', async () => {
  130. w.webContents.debugger.attach();
  131. const loadingFinished = new Promise<void>(resolve => {
  132. w.webContents.debugger.on('message', (event, method) => {
  133. // loadingFinished indicates that page has been loaded and it did not
  134. // crash because of invalid UTF-8 data
  135. if (method === 'Network.loadingFinished') {
  136. resolve();
  137. }
  138. });
  139. });
  140. server = http.createServer((req, res) => {
  141. res.setHeader('Content-Type', 'text/plain; charset=utf-8');
  142. res.end('\uFFFF');
  143. });
  144. const { url } = await listen(server);
  145. w.webContents.debugger.sendCommand('Network.enable');
  146. w.loadURL(url);
  147. await loadingFinished;
  148. });
  149. it('can get and set cookies using the Storage API', async () => {
  150. await w.webContents.loadURL('about:blank');
  151. w.webContents.debugger.attach('1.1');
  152. await w.webContents.debugger.sendCommand('Storage.clearCookies', {});
  153. await w.webContents.debugger.sendCommand('Storage.setCookies', {
  154. cookies: [
  155. {
  156. name: 'cookieOne',
  157. value: 'cookieValueOne',
  158. url: 'https://cookieone.com'
  159. },
  160. {
  161. name: 'cookieTwo',
  162. value: 'cookieValueTwo',
  163. url: 'https://cookietwo.com'
  164. }
  165. ]
  166. });
  167. const { cookies } = await w.webContents.debugger.sendCommand('Storage.getCookies', {});
  168. expect(cookies).to.have.lengthOf(2);
  169. const cookieOne = cookies.find((cookie: any) => cookie.name === 'cookieOne');
  170. expect(cookieOne.domain).to.equal('cookieone.com');
  171. expect(cookieOne.value).to.equal('cookieValueOne');
  172. const cookieTwo = cookies.find((cookie: any) => cookie.name === 'cookieTwo');
  173. expect(cookieTwo.domain).to.equal('cookietwo.com');
  174. expect(cookieTwo.value).to.equal('cookieValueTwo');
  175. });
  176. it('uses empty sessionId by default', async () => {
  177. w.webContents.loadURL('about:blank');
  178. w.webContents.debugger.attach();
  179. const onMessage = once(w.webContents.debugger, 'message');
  180. await w.webContents.debugger.sendCommand('Target.setDiscoverTargets', { discover: true });
  181. const [, method, params, sessionId] = await onMessage;
  182. expect(method).to.equal('Target.targetCreated');
  183. expect(params.targetInfo.targetId).to.not.be.empty();
  184. expect(sessionId).to.be.empty();
  185. w.webContents.debugger.detach();
  186. });
  187. it('creates unique session id for each target', (done) => {
  188. w.webContents.loadFile(path.join(__dirname, 'fixtures', 'sub-frames', 'debug-frames.html'));
  189. w.webContents.debugger.attach();
  190. let session: String;
  191. w.webContents.debugger.on('message', (event, ...args) => {
  192. const [method, params, sessionId] = args;
  193. if (method === 'Target.targetCreated') {
  194. w.webContents.debugger.sendCommand('Target.attachToTarget', { targetId: params.targetInfo.targetId, flatten: true }).then(result => {
  195. session = result.sessionId;
  196. w.webContents.debugger.sendCommand('Debugger.enable', {}, result.sessionId);
  197. });
  198. }
  199. if (method === 'Debugger.scriptParsed') {
  200. expect(sessionId).to.equal(session);
  201. w.webContents.debugger.detach();
  202. done();
  203. }
  204. });
  205. w.webContents.debugger.sendCommand('Target.setDiscoverTargets', { discover: true });
  206. });
  207. });
  208. });