api-power-monitor-spec.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. // For these tests we use a fake DBus daemon to verify powerMonitor module
  2. // interaction with the system bus. This requires python-dbusmock installed and
  3. // running (with the DBUS_SYSTEM_BUS_ADDRESS environment variable set).
  4. // script/spec-runner.js will take care of spawning the fake DBus daemon and setting
  5. // DBUS_SYSTEM_BUS_ADDRESS when python-dbusmock is installed.
  6. //
  7. // See https://pypi.python.org/pypi/python-dbusmock for more information about
  8. // python-dbusmock.
  9. import { expect } from 'chai';
  10. import * as dbus from 'dbus-native';
  11. import { once } from 'node:events';
  12. import { setTimeout } from 'node:timers/promises';
  13. import { promisify } from 'node:util';
  14. import { ifdescribe, startRemoteControlApp } from './lib/spec-helpers';
  15. describe('powerMonitor', () => {
  16. let logindMock: any, dbusMockPowerMonitor: any, getCalls: any, emitSignal: any, reset: any;
  17. ifdescribe(process.platform === 'linux' && process.env.DBUS_SYSTEM_BUS_ADDRESS != null)('when powerMonitor module is loaded with dbus mock', () => {
  18. before(async () => {
  19. const systemBus = dbus.systemBus();
  20. const loginService = systemBus.getService('org.freedesktop.login1');
  21. const getInterface = promisify(loginService.getInterface.bind(loginService));
  22. logindMock = await getInterface('/org/freedesktop/login1', 'org.freedesktop.DBus.Mock');
  23. getCalls = promisify(logindMock.GetCalls.bind(logindMock));
  24. emitSignal = promisify(logindMock.EmitSignal.bind(logindMock));
  25. reset = promisify(logindMock.Reset.bind(logindMock));
  26. });
  27. after(async () => {
  28. await reset();
  29. });
  30. function onceMethodCalled (done: () => void) {
  31. function cb () {
  32. logindMock.removeListener('MethodCalled', cb);
  33. }
  34. done();
  35. return cb;
  36. }
  37. before(done => {
  38. logindMock.on('MethodCalled', onceMethodCalled(done));
  39. // lazy load powerMonitor after we listen to MethodCalled mock signal
  40. dbusMockPowerMonitor = require('electron').powerMonitor;
  41. });
  42. it('should call Inhibit to delay suspend once a listener is added', async () => {
  43. // No calls to dbus until a listener is added
  44. {
  45. const calls = await getCalls();
  46. expect(calls).to.be.an('array').that.has.lengthOf(0);
  47. }
  48. // Add a dummy listener to engage the monitors
  49. dbusMockPowerMonitor.on('dummy-event', () => {});
  50. try {
  51. let retriesRemaining = 3;
  52. // There doesn't seem to be a way to get a notification when a call
  53. // happens, so poll `getCalls` a few times to reduce flake.
  54. let calls: any[] = [];
  55. while (retriesRemaining-- > 0) {
  56. calls = await getCalls();
  57. if (calls.length > 0) break;
  58. await setTimeout(1000);
  59. }
  60. expect(calls).to.be.an('array').that.has.lengthOf(1);
  61. expect(calls[0].slice(1)).to.deep.equal([
  62. 'Inhibit', [
  63. [[{ type: 's', child: [] }], ['sleep']],
  64. [[{ type: 's', child: [] }], ['electron']],
  65. [[{ type: 's', child: [] }], ['Application cleanup before suspend']],
  66. [[{ type: 's', child: [] }], ['delay']]
  67. ]
  68. ]);
  69. } finally {
  70. dbusMockPowerMonitor.removeAllListeners('dummy-event');
  71. }
  72. });
  73. describe('when PrepareForSleep(true) signal is sent by logind', () => {
  74. it('should emit "suspend" event', async () => {
  75. const suspend = once(dbusMockPowerMonitor, 'suspend');
  76. emitSignal('org.freedesktop.login1.Manager', 'PrepareForSleep',
  77. 'b', [['b', true]]);
  78. await suspend;
  79. });
  80. describe('when PrepareForSleep(false) signal is sent by logind', () => {
  81. it('should emit "resume" event', async () => {
  82. const resume = once(dbusMockPowerMonitor, 'resume');
  83. emitSignal('org.freedesktop.login1.Manager', 'PrepareForSleep',
  84. 'b', [['b', false]]);
  85. await resume;
  86. });
  87. it('should have called Inhibit again', async () => {
  88. const calls = await getCalls();
  89. expect(calls).to.be.an('array').that.has.lengthOf(2);
  90. expect(calls[1].slice(1)).to.deep.equal([
  91. 'Inhibit', [
  92. [[{ type: 's', child: [] }], ['sleep']],
  93. [[{ type: 's', child: [] }], ['electron']],
  94. [[{ type: 's', child: [] }], ['Application cleanup before suspend']],
  95. [[{ type: 's', child: [] }], ['delay']]
  96. ]
  97. ]);
  98. });
  99. });
  100. });
  101. describe('when a listener is added to shutdown event', () => {
  102. before(async () => {
  103. const calls = await getCalls();
  104. expect(calls).to.be.an('array').that.has.lengthOf(2);
  105. dbusMockPowerMonitor.once('shutdown', () => { });
  106. });
  107. it('should call Inhibit to delay shutdown', async () => {
  108. const calls = await getCalls();
  109. expect(calls).to.be.an('array').that.has.lengthOf(3);
  110. expect(calls[2].slice(1)).to.deep.equal([
  111. 'Inhibit', [
  112. [[{ type: 's', child: [] }], ['shutdown']],
  113. [[{ type: 's', child: [] }], ['electron']],
  114. [[{ type: 's', child: [] }], ['Ensure a clean shutdown']],
  115. [[{ type: 's', child: [] }], ['delay']]
  116. ]
  117. ]);
  118. });
  119. describe('when PrepareForShutdown(true) signal is sent by logind', () => {
  120. it('should emit "shutdown" event', done => {
  121. dbusMockPowerMonitor.once('shutdown', () => { done(); });
  122. emitSignal('org.freedesktop.login1.Manager', 'PrepareForShutdown',
  123. 'b', [['b', true]]);
  124. });
  125. });
  126. });
  127. });
  128. it('is usable before app ready', async () => {
  129. const remoteApp = await startRemoteControlApp(['--boot-eval=globalThis.initialValue=require("electron").powerMonitor.getSystemIdleTime()']);
  130. expect(await remoteApp.remoteEval('globalThis.initialValue')).to.be.a('number');
  131. });
  132. describe('when powerMonitor module is loaded', () => {
  133. let powerMonitor: typeof Electron.powerMonitor;
  134. before(() => {
  135. powerMonitor = require('electron').powerMonitor;
  136. });
  137. describe('powerMonitor.getSystemIdleState', () => {
  138. it('gets current system idle state', () => {
  139. // this function is not mocked out, so we can test the result's
  140. // form and type but not its value.
  141. const idleState = powerMonitor.getSystemIdleState(1);
  142. expect(idleState).to.be.a('string');
  143. const validIdleStates = ['active', 'idle', 'locked', 'unknown'];
  144. expect(validIdleStates).to.include(idleState);
  145. });
  146. it('does not accept non positive integer threshold', () => {
  147. expect(() => {
  148. powerMonitor.getSystemIdleState(-1);
  149. }).to.throw(/must be greater than 0/);
  150. expect(() => {
  151. powerMonitor.getSystemIdleState(NaN);
  152. }).to.throw(/conversion failure/);
  153. expect(() => {
  154. powerMonitor.getSystemIdleState('a' as any);
  155. }).to.throw(/conversion failure/);
  156. });
  157. });
  158. describe('powerMonitor.getSystemIdleTime', () => {
  159. it('returns current system idle time', () => {
  160. const idleTime = powerMonitor.getSystemIdleTime();
  161. expect(idleTime).to.be.at.least(0);
  162. });
  163. });
  164. describe('powerMonitor.getCurrentThermalState', () => {
  165. it('returns a valid state', () => {
  166. expect(powerMonitor.getCurrentThermalState()).to.be.oneOf(['unknown', 'nominal', 'fair', 'serious', 'critical']);
  167. });
  168. });
  169. describe('powerMonitor.onBatteryPower', () => {
  170. it('returns a boolean', () => {
  171. expect(powerMonitor.onBatteryPower).to.be.a('boolean');
  172. expect(powerMonitor.isOnBatteryPower()).to.be.a('boolean');
  173. });
  174. });
  175. });
  176. });