effects.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. import {
  2. ActionStateSetRemote,
  3. ActionTypeLocal,
  4. ActionTypeRemote,
  5. activeClientToggled,
  6. masterSet,
  7. playPaused,
  8. queueOrdered,
  9. queuePushed,
  10. queueRemoved,
  11. queueShifted,
  12. seeked,
  13. songInfoFetched,
  14. stateSet,
  15. } from '../actions';
  16. import { globalReducer, GlobalState, initialState } from '../reducer';
  17. import { Song } from '../types';
  18. import { MusicPlayer } from '../types/state';
  19. import { globalEffects } from './effects';
  20. describe(globalEffects.name, () => {
  21. describe(ActionTypeLocal.StateSet, () => {
  22. const state: GlobalState = {
  23. ...initialState,
  24. myClientName: 'my-client-name',
  25. };
  26. it('should create a remote state set action', () => {
  27. expect.assertions(1);
  28. const localPlayer: MusicPlayer = {
  29. songId: 123,
  30. playing: false,
  31. currentTime: 83,
  32. seekTime: 87,
  33. master: 'my-client-name',
  34. activeClients: [],
  35. queue: [],
  36. shuffleMode: false,
  37. };
  38. const action = stateSet(localPlayer);
  39. const result = globalEffects(globalReducer(state, action), action);
  40. expect(result).toStrictEqual<ActionStateSetRemote>({
  41. type: ActionTypeRemote.StateSet,
  42. payload: { ...localPlayer, seekTime: 83 },
  43. });
  44. });
  45. });
  46. describe(ActionTypeLocal.Seeked, () => {
  47. const stateMaster: GlobalState = {
  48. ...initialState,
  49. player: {
  50. songId: 123,
  51. playing: false,
  52. currentTime: 83,
  53. seekTime: 87,
  54. master: 'my-client-name',
  55. activeClients: [],
  56. queue: [],
  57. shuffleMode: false,
  58. },
  59. myClientName: 'my-client-name',
  60. };
  61. const stateSlave: GlobalState = {
  62. ...initialState,
  63. player: { ...initialState.player, master: 'some-master-client' },
  64. myClientName: 'some-slave-client',
  65. };
  66. const action = seeked(776);
  67. describe.each`
  68. clientType | state
  69. ${'master'} | ${stateMaster}
  70. ${'a slave'} | ${stateSlave}
  71. `('when the client is $clientType', ({ state }) => {
  72. it('should create a remote state set action', () => {
  73. expect.assertions(1);
  74. const result = globalEffects(state, action);
  75. expect(result).toStrictEqual<ActionStateSetRemote>({
  76. type: ActionTypeRemote.StateSet,
  77. payload: { ...state.player, seekTime: 776 },
  78. });
  79. });
  80. });
  81. });
  82. describe(ActionTypeLocal.MasterSet, () => {
  83. const stateMasterWentAway: GlobalState = {
  84. ...initialState,
  85. clientList: [{ name: 'my-client-name', lastPing: 0 }],
  86. player: {
  87. songId: 123,
  88. playing: true,
  89. currentTime: 83,
  90. seekTime: 5,
  91. master: 'some-master-went-away',
  92. activeClients: [],
  93. queue: [],
  94. shuffleMode: false,
  95. },
  96. myClientName: 'my-client-name',
  97. };
  98. const action = masterSet();
  99. it('should return a StateSet action informing other clients that we are the new master', () => {
  100. expect.assertions(1);
  101. const result = globalEffects(stateMasterWentAway, action);
  102. expect(result).toStrictEqual<ActionStateSetRemote>({
  103. type: ActionTypeRemote.StateSet,
  104. payload: {
  105. songId: 123,
  106. playing: false,
  107. currentTime: 83,
  108. seekTime: -1,
  109. master: 'my-client-name',
  110. activeClients: [],
  111. queue: [],
  112. shuffleMode: false,
  113. },
  114. });
  115. });
  116. describe('when the action specified a particular client', () => {
  117. it('should return a StateSet action informing the new client to resume playback', () => {
  118. expect.assertions(1);
  119. const result = globalEffects(stateMasterWentAway, masterSet('other-client'));
  120. expect(result).toStrictEqual<ActionStateSetRemote>({
  121. type: ActionTypeRemote.StateSet,
  122. payload: {
  123. songId: 123,
  124. playing: true,
  125. currentTime: 83,
  126. seekTime: 83,
  127. master: 'other-client',
  128. activeClients: [],
  129. queue: [],
  130. shuffleMode: false,
  131. },
  132. });
  133. });
  134. });
  135. });
  136. describe(ActionTypeLocal.ActiveClientToggled, () => {
  137. const action = activeClientToggled('some-client');
  138. describe('when the given client is active', () => {
  139. const stateWithGivenClientActive: GlobalState = {
  140. ...initialState,
  141. player: {
  142. ...initialState.player,
  143. activeClients: ['some-client', 'other-client'],
  144. },
  145. };
  146. it('should remove the given client from the active clients list', () => {
  147. expect.assertions(1);
  148. const result = globalEffects(stateWithGivenClientActive, action);
  149. expect(result).toStrictEqual<ActionStateSetRemote>({
  150. type: ActionTypeRemote.StateSet,
  151. payload: expect.objectContaining({
  152. activeClients: ['other-client'],
  153. }),
  154. });
  155. });
  156. });
  157. describe('when the given client is not active', () => {
  158. const stateWithGivenClientInactive: GlobalState = {
  159. ...initialState,
  160. player: {
  161. ...initialState.player,
  162. activeClients: ['other-client'],
  163. },
  164. };
  165. it('should add the given client to the active clients list', () => {
  166. expect.assertions(1);
  167. const result = globalEffects(stateWithGivenClientInactive, action);
  168. expect(result).toStrictEqual<ActionStateSetRemote>({
  169. type: ActionTypeRemote.StateSet,
  170. payload: expect.objectContaining({
  171. activeClients: expect.arrayContaining(['some-client', 'other-client']),
  172. }),
  173. });
  174. });
  175. });
  176. });
  177. describe(ActionTypeLocal.PlayPaused, () => {
  178. const action = playPaused();
  179. describe.each`
  180. currentClient | myClientName
  181. ${'master'} | ${'some-master-client'}
  182. ${'a slave'} | ${'my client'}
  183. `('when the current client is $currentClient', ({ myClientName }) => {
  184. const statePrior: GlobalState = {
  185. ...initialState,
  186. player: {
  187. songId: 123,
  188. playing: true,
  189. currentTime: 83,
  190. seekTime: 5,
  191. master: 'some-master-client',
  192. activeClients: [],
  193. queue: [],
  194. shuffleMode: false,
  195. },
  196. myClientName,
  197. };
  198. it('should return a StateSet action informing other clients of the updated playing state', () => {
  199. expect.assertions(1);
  200. const result = globalEffects(statePrior, action);
  201. expect(result).toStrictEqual<ActionStateSetRemote>({
  202. type: ActionTypeRemote.StateSet,
  203. payload: {
  204. songId: 123,
  205. playing: false,
  206. currentTime: 83,
  207. seekTime: 5,
  208. master: 'some-master-client',
  209. activeClients: [],
  210. queue: [],
  211. shuffleMode: false,
  212. },
  213. });
  214. });
  215. });
  216. });
  217. describe(ActionTypeLocal.SongInfoFetched, () => {
  218. const statePriorMaster: GlobalState = {
  219. ...initialState,
  220. player: {
  221. songId: 123,
  222. playing: true,
  223. currentTime: 83,
  224. seekTime: 5,
  225. master: 'some-master-client',
  226. activeClients: [],
  227. queue: [],
  228. shuffleMode: false,
  229. },
  230. myClientName: 'some-master-client',
  231. };
  232. const action = songInfoFetched({ id: 185 } as Song, true);
  233. describe('when the client is master', () => {
  234. it('should return null', () => {
  235. expect.assertions(1);
  236. expect(globalEffects(statePriorMaster, action)).toBeNull();
  237. });
  238. });
  239. describe('when the client is a slave', () => {
  240. const stateSlave: GlobalState = {
  241. ...statePriorMaster,
  242. myClientName: 'some-slave-client',
  243. };
  244. it('should return a StateSet action informing other clients of the changed song', () => {
  245. expect.assertions(1);
  246. const result = globalEffects(stateSlave, action);
  247. expect(result).toStrictEqual<ActionStateSetRemote>({
  248. type: ActionTypeRemote.StateSet,
  249. payload: {
  250. songId: 185,
  251. playing: true,
  252. currentTime: 0,
  253. seekTime: 0,
  254. master: 'some-master-client',
  255. activeClients: [],
  256. queue: [],
  257. shuffleMode: false,
  258. },
  259. });
  260. });
  261. describe('when the action is not set to replace the current song', () => {
  262. const actionNoReplace = songInfoFetched({ id: 185 } as Song, false);
  263. it('should return null', () => {
  264. expect.assertions(1);
  265. const result = globalEffects(stateSlave, actionNoReplace);
  266. expect(result).toBeNull();
  267. });
  268. });
  269. });
  270. });
  271. describe(ActionTypeLocal.QueuePushed, () => {
  272. const action = queuePushed([184, 79]);
  273. it('should add to the end of the queue', () => {
  274. expect.assertions(1);
  275. const result = globalEffects(
  276. {
  277. ...initialState,
  278. player: { ...initialState.player, master: 'some-master', queue: [23] },
  279. },
  280. action,
  281. );
  282. expect(result).toStrictEqual<ActionStateSetRemote>({
  283. type: ActionTypeRemote.StateSet,
  284. payload: {
  285. ...initialState.player,
  286. master: 'some-master',
  287. queue: [23, 184, 79],
  288. },
  289. });
  290. });
  291. describe('when the songs are already in the queue', () => {
  292. it('should not modify the queue', () => {
  293. expect.assertions(1);
  294. const result = globalEffects(
  295. {
  296. ...initialState,
  297. player: { ...initialState.player, queue: [184, 23, 79] },
  298. },
  299. action,
  300. );
  301. expect(result).toBeNull();
  302. });
  303. });
  304. });
  305. describe(ActionTypeLocal.QueueShifted, () => {
  306. const action = queueShifted();
  307. const stateWithQueue: GlobalState = {
  308. ...initialState,
  309. player: { ...initialState.player, master: 'some-master', queue: [8843, 23] },
  310. };
  311. it('should play the first song on the queue and remove it from the queue', () => {
  312. expect.assertions(1);
  313. const result = globalEffects(stateWithQueue, action);
  314. expect(result).toStrictEqual<ActionStateSetRemote>({
  315. type: ActionTypeRemote.StateSet,
  316. payload: {
  317. ...initialState.player,
  318. master: 'some-master',
  319. playing: true,
  320. songId: 8843,
  321. currentTime: 0,
  322. seekTime: 0,
  323. queue: [23],
  324. },
  325. });
  326. });
  327. });
  328. describe(ActionTypeLocal.QueueRemoved, () => {
  329. const action = queueRemoved(84);
  330. it('should remove the given song ID from the queue', () => {
  331. expect.assertions(1);
  332. const result = globalEffects(
  333. {
  334. ...initialState,
  335. player: { ...initialState.player, master: 'some-master', queue: [17, 84, 23] },
  336. },
  337. action,
  338. );
  339. expect(result).toStrictEqual<ActionStateSetRemote>({
  340. type: ActionTypeRemote.StateSet,
  341. payload: {
  342. ...initialState.player,
  343. master: 'some-master',
  344. queue: [17, 23],
  345. },
  346. });
  347. });
  348. });
  349. describe(ActionTypeLocal.QueueOrdered, () => {
  350. it.each`
  351. direction | delta | expectedResult
  352. ${'forwards'} | ${1} | ${[17, 23, 84]}
  353. ${'backwards'} | ${-1} | ${[84, 17, 23]}
  354. `('should reorder ($direction) the given song ID', ({ delta, expectedResult }) => {
  355. const action = queueOrdered(84, delta);
  356. expect.assertions(1);
  357. const result = globalEffects(
  358. {
  359. ...initialState,
  360. player: { ...initialState.player, master: 'some-master', queue: [17, 84, 23] },
  361. },
  362. action,
  363. );
  364. expect(result).toStrictEqual<ActionStateSetRemote>({
  365. type: ActionTypeRemote.StateSet,
  366. payload: {
  367. ...initialState.player,
  368. master: 'some-master',
  369. queue: expectedResult,
  370. },
  371. });
  372. });
  373. });
  374. });