reducer.spec.ts 14 KB

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