effects.spec.ts 11 KB

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