reducer.spec.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. import {
  2. ActionClientListUpdated,
  3. ActionStateSetLocal,
  4. ActionStateSetRemote,
  5. ActionTypeLocal,
  6. ActionTypeRemote,
  7. masterSet,
  8. nameSet,
  9. seeked,
  10. songInfoFetched,
  11. stateSet,
  12. } from '../actions';
  13. import { Song } from '../types';
  14. import { MusicPlayer } from '../types/state';
  15. import { globalReducer, initialState, nullPlayer } from './reducer';
  16. import { GlobalState } from './types';
  17. describe(globalReducer.name, () => {
  18. describe(ActionTypeRemote.StateSet, () => {
  19. describe('when the client is master', () => {
  20. const stateMaster: GlobalState = {
  21. ...initialState,
  22. player: {
  23. ...nullPlayer,
  24. master: 'some-master-client',
  25. currentTime: 31,
  26. seekTime: -1,
  27. },
  28. myClientName: 'some-master-client',
  29. };
  30. describe('and the action came from a different client', () => {
  31. const actionFromOtherClient: ActionStateSetRemote = {
  32. type: ActionTypeRemote.StateSet,
  33. fromClient: 'other-client',
  34. payload: {
  35. songId: 123,
  36. playing: true,
  37. currentTime: 75,
  38. seekTime: 87,
  39. master: 'some-master-client',
  40. activeClients: [],
  41. queue: [],
  42. shuffleMode: false,
  43. },
  44. };
  45. it('should update the state, including seekTime', () => {
  46. expect.assertions(1);
  47. const result = globalReducer(stateMaster, actionFromOtherClient);
  48. expect(result.player).toStrictEqual<MusicPlayer>({
  49. songId: 123,
  50. playing: true,
  51. currentTime: 75,
  52. seekTime: 87,
  53. master: 'some-master-client',
  54. activeClients: [],
  55. queue: [],
  56. shuffleMode: false,
  57. });
  58. });
  59. describe('when the payload is partial', () => {
  60. const actionPartial: ActionStateSetRemote = {
  61. type: ActionTypeRemote.StateSet,
  62. payload: { currentTime: 102, playing: true },
  63. };
  64. it('should only update the given properties', () => {
  65. expect.assertions(1);
  66. const result = globalReducer(stateMaster, actionPartial);
  67. expect(result.player).toStrictEqual<MusicPlayer>({
  68. ...stateMaster.player,
  69. currentTime: 102,
  70. playing: true,
  71. });
  72. });
  73. });
  74. });
  75. describe('and the action came from ourselves', () => {
  76. const actionFromOurselves: ActionStateSetRemote = {
  77. type: ActionTypeRemote.StateSet,
  78. fromClient: 'some-master-client',
  79. payload: {
  80. songId: 123,
  81. playing: true,
  82. currentTime: 75,
  83. seekTime: 87,
  84. master: 'some-master-client',
  85. activeClients: [],
  86. queue: [],
  87. shuffleMode: false,
  88. },
  89. };
  90. it('should update the state, except the seekTime', () => {
  91. expect.assertions(1);
  92. const result = globalReducer(stateMaster, actionFromOurselves);
  93. expect(result.player).toStrictEqual<MusicPlayer>({
  94. songId: 123,
  95. playing: true,
  96. currentTime: 75,
  97. seekTime: -1,
  98. master: 'some-master-client',
  99. activeClients: [],
  100. queue: [],
  101. shuffleMode: false,
  102. });
  103. });
  104. });
  105. describe('but will no longer be master', () => {
  106. const actionToSlave: ActionStateSetRemote = {
  107. type: ActionTypeRemote.StateSet,
  108. fromClient: 'other-client',
  109. payload: {
  110. songId: 123,
  111. playing: true,
  112. currentTime: 75,
  113. seekTime: 87,
  114. master: 'other-master-client',
  115. activeClients: [],
  116. queue: [],
  117. shuffleMode: false,
  118. },
  119. };
  120. it('should update the state, setting seekTime to -1', () => {
  121. expect.assertions(1);
  122. const result = globalReducer(stateMaster, actionToSlave);
  123. expect(result.player).toStrictEqual<MusicPlayer>({
  124. songId: 123,
  125. playing: true,
  126. currentTime: 75,
  127. seekTime: -1,
  128. master: 'other-master-client',
  129. activeClients: [],
  130. queue: [],
  131. shuffleMode: false,
  132. });
  133. });
  134. });
  135. });
  136. describe('when the client is a slave', () => {
  137. const stateSlave: GlobalState = {
  138. ...initialState,
  139. player: {
  140. ...initialState.player,
  141. master: 'some-master-client',
  142. },
  143. myClientName: 'my-client',
  144. };
  145. describe.each`
  146. case | fromClient
  147. ${'the master client'} | ${'some-master-client'}
  148. ${'a different client'} | ${'other-client'}
  149. `('and the action came from $case', ({ fromClient }) => {
  150. const action: ActionStateSetRemote = {
  151. type: ActionTypeRemote.StateSet,
  152. fromClient,
  153. payload: {
  154. songId: 123,
  155. playing: true,
  156. currentTime: 75,
  157. seekTime: 87,
  158. master: 'some-master-client',
  159. activeClients: [],
  160. queue: [],
  161. shuffleMode: false,
  162. },
  163. };
  164. it('should set the player state, except seekTime', () => {
  165. expect.assertions(1);
  166. const result = globalReducer(stateSlave, action);
  167. expect(result.player).toStrictEqual<MusicPlayer>({
  168. songId: 123,
  169. playing: true,
  170. currentTime: 75,
  171. seekTime: -1,
  172. master: 'some-master-client',
  173. activeClients: [],
  174. queue: [],
  175. shuffleMode: false,
  176. });
  177. });
  178. });
  179. describe('but will be set to master', () => {
  180. const actionToMaster: ActionStateSetRemote = {
  181. type: ActionTypeRemote.StateSet,
  182. fromClient: 'other-client',
  183. payload: {
  184. songId: 123,
  185. playing: true,
  186. currentTime: 75,
  187. seekTime: 87,
  188. master: 'my-client',
  189. activeClients: [],
  190. queue: [],
  191. shuffleMode: false,
  192. },
  193. };
  194. it('should set the player state, including seekTime', () => {
  195. expect.assertions(1);
  196. const result = globalReducer(stateSlave, actionToMaster);
  197. expect(result.player).toStrictEqual<MusicPlayer>({
  198. songId: 123,
  199. playing: true,
  200. currentTime: 75,
  201. seekTime: 87,
  202. master: 'my-client',
  203. activeClients: [],
  204. queue: [],
  205. shuffleMode: false,
  206. });
  207. });
  208. });
  209. });
  210. });
  211. describe(ActionTypeRemote.ClientListUpdated, () => {
  212. const action: ActionClientListUpdated = {
  213. type: ActionTypeRemote.ClientListUpdated,
  214. payload: [
  215. {
  216. name: 'client1-ab54x',
  217. lastPing: 1665912239,
  218. },
  219. {
  220. name: 'client1-ab54x',
  221. lastPing: 1665912262,
  222. },
  223. ],
  224. };
  225. it('should update the client list', () => {
  226. expect.assertions(1);
  227. const result = globalReducer(initialState, action);
  228. expect(result.clientList).toStrictEqual(action.payload);
  229. });
  230. });
  231. describe(ActionTypeLocal.NameSet, () => {
  232. it('should set the name', () => {
  233. expect.assertions(1);
  234. expect(globalReducer(initialState, nameSet('foo')).myClientName).toBe('foo');
  235. });
  236. });
  237. describe(ActionTypeLocal.StateSet, () => {
  238. const action = stateSet({
  239. songId: 123,
  240. playing: true,
  241. currentTime: 75,
  242. seekTime: 87,
  243. master: 'some-master-client',
  244. });
  245. describe('when the client is master', () => {
  246. const stateMaster: GlobalState = {
  247. ...initialState,
  248. player: {
  249. ...nullPlayer,
  250. master: 'some-master-client',
  251. currentTime: 31,
  252. seekTime: -1,
  253. },
  254. myClientName: 'some-master-client',
  255. };
  256. it('should set the player state optimistically', () => {
  257. expect.assertions(1);
  258. const result = globalReducer(stateMaster, action);
  259. expect(result.player).toStrictEqual<MusicPlayer>({
  260. songId: 123,
  261. playing: true,
  262. currentTime: 75,
  263. seekTime: 87,
  264. master: 'some-master-client',
  265. activeClients: [],
  266. queue: [],
  267. shuffleMode: false,
  268. });
  269. });
  270. describe('when the state update is partial', () => {
  271. const actionPartial: ActionStateSetLocal = stateSet({
  272. songId: 3,
  273. playing: true,
  274. });
  275. it('should update the given part of the state', () => {
  276. expect.assertions(1);
  277. const result = globalReducer(stateMaster, actionPartial);
  278. expect(result.player).toStrictEqual<MusicPlayer>({
  279. ...nullPlayer,
  280. master: 'some-master-client',
  281. currentTime: 31,
  282. seekTime: -1,
  283. songId: 3,
  284. playing: true,
  285. });
  286. });
  287. });
  288. describe('when the state update is a function', () => {
  289. const actionFn = stateSet((last) => ({
  290. ...last,
  291. currentTime: (last.currentTime ?? 0) + 4,
  292. }));
  293. it('should set the state from the given function', () => {
  294. expect.assertions(1);
  295. const result = globalReducer(stateMaster, actionFn);
  296. expect(result.player).toStrictEqual<MusicPlayer>({
  297. ...nullPlayer,
  298. master: 'some-master-client',
  299. seekTime: -1,
  300. songId: null,
  301. playing: false,
  302. currentTime: 31 + 4,
  303. });
  304. });
  305. });
  306. });
  307. describe('when the client is a slave', () => {
  308. const stateSlave: GlobalState = {
  309. ...initialState,
  310. player: {
  311. ...initialState.player,
  312. master: 'some-master-client',
  313. },
  314. myClientName: 'my-client',
  315. };
  316. it('should not update the state optimistically', () => {
  317. expect.assertions(1);
  318. const result = globalReducer(stateSlave, action);
  319. expect(result.player).toBe(stateSlave.player);
  320. });
  321. describe('but will be master', () => {
  322. const actionToMaster = stateSet({
  323. master: 'my-client',
  324. });
  325. it('should set the seekTime to the currentTime value', () => {
  326. expect.assertions(1);
  327. const result = globalReducer(stateSlave, actionToMaster);
  328. expect(result.player.seekTime).toBe(stateSlave.player.currentTime);
  329. });
  330. });
  331. });
  332. });
  333. describe(ActionTypeLocal.Seeked, () => {
  334. const action = seeked(173);
  335. describe('when the client is master', () => {
  336. const stateMaster: GlobalState = {
  337. ...initialState,
  338. player: {
  339. songId: 123,
  340. playing: true,
  341. currentTime: 31,
  342. seekTime: -1,
  343. master: 'some-master-client',
  344. activeClients: [],
  345. queue: [],
  346. shuffleMode: false,
  347. },
  348. myClientName: 'some-master-client',
  349. };
  350. it('should set the current and seek time', () => {
  351. expect.assertions(1);
  352. const result = globalReducer(stateMaster, action);
  353. expect(result.player).toStrictEqual<MusicPlayer>({
  354. songId: 123,
  355. playing: true,
  356. currentTime: 173,
  357. seekTime: 173,
  358. master: 'some-master-client',
  359. activeClients: [],
  360. queue: [],
  361. shuffleMode: false,
  362. });
  363. });
  364. });
  365. describe('when the client is a slave', () => {
  366. const stateSlave: GlobalState = {
  367. ...initialState,
  368. player: {
  369. ...initialState.player,
  370. seekTime: 101,
  371. master: 'some-master-client',
  372. },
  373. myClientName: 'my-client',
  374. };
  375. it('should only set the current time (optimistically)', () => {
  376. expect.assertions(2);
  377. const result = globalReducer(stateSlave, action);
  378. expect(result.player.seekTime).toBe(stateSlave.player.seekTime);
  379. expect(result.player.currentTime).toBe(173);
  380. });
  381. });
  382. });
  383. describe(ActionTypeLocal.MasterSet, () => {
  384. const action = masterSet();
  385. const stateBefore: GlobalState = {
  386. ...initialState,
  387. myClientName: 'my-client',
  388. player: {
  389. songId: 174,
  390. playing: true,
  391. master: 'some-master-client',
  392. currentTime: 13,
  393. seekTime: -1,
  394. activeClients: [],
  395. queue: [],
  396. shuffleMode: false,
  397. },
  398. };
  399. it('should set the master player to the current client', () => {
  400. expect.assertions(1);
  401. const result = globalReducer(stateBefore, action);
  402. expect(result.player.master).toBe('my-client');
  403. });
  404. it('should seek to the current time', () => {
  405. expect.assertions(1);
  406. const result = globalReducer(stateBefore, action);
  407. expect(result.player.seekTime).toBe(13);
  408. });
  409. it('should pause the client', () => {
  410. expect.assertions(1);
  411. const result = globalReducer(stateBefore, action);
  412. expect(result.player.playing).toBe(false);
  413. });
  414. describe('when a particular client is given in the action', () => {
  415. const actionToOtherClient = masterSet('other-client');
  416. it('should set the master player to the given client', () => {
  417. expect.assertions(2);
  418. const result = globalReducer(stateBefore, actionToOtherClient);
  419. expect(result.player.master).toBe('other-client');
  420. expect(result.player.seekTime).toBe(stateBefore.player.currentTime);
  421. });
  422. it('should not pause the client', () => {
  423. expect.assertions(1);
  424. const result = globalReducer(stateBefore, actionToOtherClient);
  425. expect(result.player.playing).toBe(true);
  426. });
  427. });
  428. });
  429. describe(ActionTypeLocal.SongInfoFetched, () => {
  430. const song: Song = {
  431. id: 123,
  432. track: 17,
  433. title: 'Some song',
  434. artist: 'Some artist',
  435. album: 'Some album',
  436. time: 214,
  437. };
  438. const action = songInfoFetched(song);
  439. it('should set the song info in state', () => {
  440. expect.assertions(1);
  441. const result = globalReducer(initialState, action);
  442. expect(result.songInfo).toStrictEqual<Song>(song);
  443. });
  444. describe('when set to replace the current song', () => {
  445. const actionReplace = songInfoFetched(song, true);
  446. it('should play the given song from the start', () => {
  447. expect.assertions(4);
  448. const result = globalReducer(initialState, actionReplace);
  449. expect(result.songInfo).toStrictEqual<Song>(song);
  450. expect(result.player.playing).toBe(true);
  451. expect(result.player.songId).toBe(song.id);
  452. expect(result.player.seekTime).toBe(0);
  453. });
  454. });
  455. });
  456. });