@xiboplayer/utils 0.3.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -782,6 +782,918 @@ describe('CmsApiClient', () => {
782
782
  });
783
783
  });
784
784
 
785
+ // ── Layout Copy / Discard (#25) ──
786
+
787
+ describe('Layout Copy / Discard', () => {
788
+ beforeEach(() => stubAuth());
789
+
790
+ it('copyLayout() should POST to /layout/copy/{id}', async () => {
791
+ mockFetch.mockResolvedValue(jsonResponse({ layoutId: 99 }));
792
+
793
+ const result = await api.copyLayout(10, { name: 'Copy of Layout' });
794
+
795
+ const [url, opts] = mockFetch.mock.calls[0];
796
+ expect(url.toString()).toContain('/layout/copy/10');
797
+ expect(opts.method).toBe('POST');
798
+ expect(opts.body.get('name')).toBe('Copy of Layout');
799
+ expect(result.layoutId).toBe(99);
800
+ });
801
+
802
+ it('discardLayout() should PUT to /layout/discard/{id}', async () => {
803
+ mockFetch.mockResolvedValue(emptyResponse());
804
+
805
+ await api.discardLayout(10);
806
+
807
+ const [url, opts] = mockFetch.mock.calls[0];
808
+ expect(url.toString()).toContain('/layout/discard/10');
809
+ expect(opts.method).toBe('PUT');
810
+ });
811
+ });
812
+
813
+ // ── Campaign Edit / Unassign (#26) ──
814
+
815
+ describe('Campaign Edit / Unassign', () => {
816
+ beforeEach(() => stubAuth());
817
+
818
+ it('editCampaign() should PUT to /campaign/{id}', async () => {
819
+ mockFetch.mockResolvedValue(jsonResponse({ campaignId: 20 }));
820
+
821
+ const result = await api.editCampaign(20, { name: 'Updated Campaign' });
822
+
823
+ const [url, opts] = mockFetch.mock.calls[0];
824
+ expect(url.toString()).toContain('/campaign/20');
825
+ expect(opts.method).toBe('PUT');
826
+ expect(opts.body.get('name')).toBe('Updated Campaign');
827
+ expect(result.campaignId).toBe(20);
828
+ });
829
+
830
+ it('getCampaign() should GET /campaign/{id}', async () => {
831
+ mockFetch.mockResolvedValue(jsonResponse({ campaignId: 20, name: 'My Campaign' }));
832
+
833
+ const result = await api.getCampaign(20);
834
+
835
+ const [url] = mockFetch.mock.calls[0];
836
+ expect(url.toString()).toContain('/campaign/20');
837
+ expect(result.name).toBe('My Campaign');
838
+ });
839
+
840
+ it('unassignLayoutFromCampaign() should POST with layoutId', async () => {
841
+ mockFetch.mockResolvedValue(emptyResponse());
842
+
843
+ await api.unassignLayoutFromCampaign(20, 10);
844
+
845
+ const [url, opts] = mockFetch.mock.calls[0];
846
+ expect(url.toString()).toContain('/campaign/layout/unassign/20');
847
+ expect(opts.method).toBe('POST');
848
+ expect(opts.body.get('layoutId')).toBe('10');
849
+ });
850
+ });
851
+
852
+ // ── Schedule Edit (#27) ──
853
+
854
+ describe('Schedule Edit', () => {
855
+ beforeEach(() => stubAuth());
856
+
857
+ it('editSchedule() should PUT to /schedule/{id}', async () => {
858
+ mockFetch.mockResolvedValue(jsonResponse({ eventId: 99 }));
859
+
860
+ const result = await api.editSchedule(99, { isPriority: 1 });
861
+
862
+ const [url, opts] = mockFetch.mock.calls[0];
863
+ expect(url.toString()).toContain('/schedule/99');
864
+ expect(opts.method).toBe('PUT');
865
+ expect(opts.body.get('isPriority')).toBe('1');
866
+ expect(result.eventId).toBe(99);
867
+ });
868
+ });
869
+
870
+ // ── Layout Retire / Status / Tag (#34) ──
871
+
872
+ describe('Layout Retire / Status / Tag', () => {
873
+ beforeEach(() => stubAuth());
874
+
875
+ it('retireLayout() should PUT to /layout/retire/{id}', async () => {
876
+ mockFetch.mockResolvedValue(emptyResponse());
877
+
878
+ await api.retireLayout(10);
879
+
880
+ const [url, opts] = mockFetch.mock.calls[0];
881
+ expect(url.toString()).toContain('/layout/retire/10');
882
+ expect(opts.method).toBe('PUT');
883
+ });
884
+
885
+ it('unretireLayout() should PUT to /layout/unretire/{id}', async () => {
886
+ mockFetch.mockResolvedValue(emptyResponse());
887
+
888
+ await api.unretireLayout(10);
889
+
890
+ const [url, opts] = mockFetch.mock.calls[0];
891
+ expect(url.toString()).toContain('/layout/unretire/10');
892
+ expect(opts.method).toBe('PUT');
893
+ });
894
+
895
+ it('getLayoutStatus() should GET /layout/status/{id}', async () => {
896
+ mockFetch.mockResolvedValue(jsonResponse({ status: 3, description: 'Valid' }));
897
+
898
+ const result = await api.getLayoutStatus(10);
899
+
900
+ const [url] = mockFetch.mock.calls[0];
901
+ expect(url.toString()).toContain('/layout/status/10');
902
+ expect(result.status).toBe(3);
903
+ });
904
+
905
+ it('tagLayout() should POST comma-separated tags', async () => {
906
+ mockFetch.mockResolvedValue(emptyResponse());
907
+
908
+ await api.tagLayout(10, ['lobby', 'hd']);
909
+
910
+ const [url, opts] = mockFetch.mock.calls[0];
911
+ expect(url.toString()).toContain('/layout/10/tag');
912
+ expect(opts.method).toBe('POST');
913
+ expect(opts.body.get('tag')).toBe('lobby,hd');
914
+ });
915
+
916
+ it('untagLayout() should POST comma-separated tags to untag', async () => {
917
+ mockFetch.mockResolvedValue(emptyResponse());
918
+
919
+ await api.untagLayout(10, ['old']);
920
+
921
+ const [url, opts] = mockFetch.mock.calls[0];
922
+ expect(url.toString()).toContain('/layout/10/untag');
923
+ expect(opts.body.get('tag')).toBe('old');
924
+ });
925
+ });
926
+
927
+ // ── Command CRUD (#36) ──
928
+
929
+ describe('Command CRUD', () => {
930
+ beforeEach(() => stubAuth());
931
+
932
+ it('listCommands() should GET and return array', async () => {
933
+ mockFetch.mockResolvedValue(jsonResponse([{ commandId: 1, command: 'reboot' }]));
934
+
935
+ const cmds = await api.listCommands();
936
+
937
+ expect(cmds).toHaveLength(1);
938
+ expect(cmds[0].command).toBe('reboot');
939
+ });
940
+
941
+ it('createCommand() should POST', async () => {
942
+ mockFetch.mockResolvedValue(jsonResponse({ commandId: 2 }));
943
+
944
+ const result = await api.createCommand({ command: 'reboot', code: 'sudo reboot' });
945
+
946
+ const [, opts] = mockFetch.mock.calls[0];
947
+ expect(opts.method).toBe('POST');
948
+ expect(opts.body.get('command')).toBe('reboot');
949
+ expect(result.commandId).toBe(2);
950
+ });
951
+
952
+ it('editCommand() should PUT to /command/{id}', async () => {
953
+ mockFetch.mockResolvedValue(jsonResponse({ commandId: 2 }));
954
+
955
+ await api.editCommand(2, { description: 'Updated' });
956
+
957
+ const [url, opts] = mockFetch.mock.calls[0];
958
+ expect(url.toString()).toContain('/command/2');
959
+ expect(opts.method).toBe('PUT');
960
+ });
961
+
962
+ it('deleteCommand() should DELETE', async () => {
963
+ mockFetch.mockResolvedValue(emptyResponse());
964
+
965
+ await api.deleteCommand(2);
966
+
967
+ const [url, opts] = mockFetch.mock.calls[0];
968
+ expect(url.toString()).toContain('/command/2');
969
+ expect(opts.method).toBe('DELETE');
970
+ });
971
+ });
972
+
973
+ // ── Display Extras (#41) ──
974
+
975
+ describe('Display Extras', () => {
976
+ beforeEach(() => stubAuth());
977
+
978
+ it('deleteDisplay() should DELETE /display/{id}', async () => {
979
+ mockFetch.mockResolvedValue(emptyResponse());
980
+
981
+ await api.deleteDisplay(42);
982
+
983
+ const [url, opts] = mockFetch.mock.calls[0];
984
+ expect(url.toString()).toContain('/display/42');
985
+ expect(opts.method).toBe('DELETE');
986
+ });
987
+
988
+ it('wolDisplay() should POST to /display/wol/{id}', async () => {
989
+ mockFetch.mockResolvedValue(emptyResponse());
990
+
991
+ await api.wolDisplay(42);
992
+
993
+ const [url, opts] = mockFetch.mock.calls[0];
994
+ expect(url.toString()).toContain('/display/wol/42');
995
+ expect(opts.method).toBe('POST');
996
+ });
997
+
998
+ it('setDefaultLayout() should PUT defaultLayoutId', async () => {
999
+ mockFetch.mockResolvedValue(jsonResponse({ displayId: 42 }));
1000
+
1001
+ await api.setDefaultLayout(42, 10);
1002
+
1003
+ const [url, opts] = mockFetch.mock.calls[0];
1004
+ expect(url.toString()).toContain('/display/42');
1005
+ expect(opts.method).toBe('PUT');
1006
+ expect(opts.body.get('defaultLayoutId')).toBe('10');
1007
+ });
1008
+
1009
+ it('purgeDisplay() should POST to /display/purge/{id}', async () => {
1010
+ mockFetch.mockResolvedValue(emptyResponse());
1011
+
1012
+ await api.purgeDisplay(42);
1013
+
1014
+ const [url, opts] = mockFetch.mock.calls[0];
1015
+ expect(url.toString()).toContain('/display/purge/42');
1016
+ expect(opts.method).toBe('POST');
1017
+ });
1018
+ });
1019
+
1020
+ // ── DayPart CRUD (#24) ──
1021
+
1022
+ describe('DayPart CRUD', () => {
1023
+ beforeEach(() => stubAuth());
1024
+
1025
+ it('listDayParts() should GET and return array', async () => {
1026
+ mockFetch.mockResolvedValue(jsonResponse([{ dayPartId: 1, name: 'Business Hours' }]));
1027
+
1028
+ const parts = await api.listDayParts();
1029
+
1030
+ expect(parts).toHaveLength(1);
1031
+ expect(parts[0].name).toBe('Business Hours');
1032
+ });
1033
+
1034
+ it('createDayPart() should POST', async () => {
1035
+ mockFetch.mockResolvedValue(jsonResponse({ dayPartId: 2 }));
1036
+
1037
+ const result = await api.createDayPart({ name: 'Evening', startTime: '18:00', endTime: '22:00' });
1038
+
1039
+ const [, opts] = mockFetch.mock.calls[0];
1040
+ expect(opts.method).toBe('POST');
1041
+ expect(opts.body.get('name')).toBe('Evening');
1042
+ expect(result.dayPartId).toBe(2);
1043
+ });
1044
+
1045
+ it('editDayPart() should PUT to /daypart/{id}', async () => {
1046
+ mockFetch.mockResolvedValue(jsonResponse({ dayPartId: 2 }));
1047
+
1048
+ await api.editDayPart(2, { name: 'Updated Evening' });
1049
+
1050
+ const [url, opts] = mockFetch.mock.calls[0];
1051
+ expect(url.toString()).toContain('/daypart/2');
1052
+ expect(opts.method).toBe('PUT');
1053
+ });
1054
+
1055
+ it('deleteDayPart() should DELETE', async () => {
1056
+ mockFetch.mockResolvedValue(emptyResponse());
1057
+
1058
+ await api.deleteDayPart(2);
1059
+
1060
+ const [url, opts] = mockFetch.mock.calls[0];
1061
+ expect(url.toString()).toContain('/daypart/2');
1062
+ expect(opts.method).toBe('DELETE');
1063
+ });
1064
+ });
1065
+
1066
+ // ── Library Extensions (#33) ──
1067
+
1068
+ describe('Library Extensions', () => {
1069
+ beforeEach(() => stubAuth());
1070
+
1071
+ it('uploadMediaUrl() should POST with url and name', async () => {
1072
+ mockFetch.mockResolvedValue(jsonResponse({ mediaId: 60 }));
1073
+
1074
+ const result = await api.uploadMediaUrl('https://example.com/image.jpg', 'Test Image');
1075
+
1076
+ const [, opts] = mockFetch.mock.calls[0];
1077
+ expect(opts.body.get('url')).toBe('https://example.com/image.jpg');
1078
+ expect(opts.body.get('name')).toBe('Test Image');
1079
+ expect(result.mediaId).toBe(60);
1080
+ });
1081
+
1082
+ it('copyMedia() should POST to /library/copy/{id}', async () => {
1083
+ mockFetch.mockResolvedValue(jsonResponse({ mediaId: 61 }));
1084
+
1085
+ const result = await api.copyMedia(50);
1086
+
1087
+ const [url, opts] = mockFetch.mock.calls[0];
1088
+ expect(url.toString()).toContain('/library/copy/50');
1089
+ expect(opts.method).toBe('POST');
1090
+ expect(result.mediaId).toBe(61);
1091
+ });
1092
+
1093
+ it('downloadMedia() should GET raw response', async () => {
1094
+ const mockResponse = { ok: true, status: 200, text: () => Promise.resolve('binary data') };
1095
+ mockFetch.mockResolvedValue(mockResponse);
1096
+
1097
+ const response = await api.downloadMedia(50);
1098
+
1099
+ const [url] = mockFetch.mock.calls[0];
1100
+ expect(url).toContain('/library/download/50');
1101
+ expect(response).toBe(mockResponse);
1102
+ });
1103
+
1104
+ it('downloadMedia() should throw on error', async () => {
1105
+ mockFetch.mockResolvedValue({
1106
+ ok: false,
1107
+ status: 404,
1108
+ text: () => Promise.resolve('Not found')
1109
+ });
1110
+
1111
+ await expect(api.downloadMedia(999)).rejects.toThrow('404');
1112
+ });
1113
+
1114
+ it('editMedia() should PUT to /library/{id}', async () => {
1115
+ mockFetch.mockResolvedValue(jsonResponse({ mediaId: 50 }));
1116
+
1117
+ await api.editMedia(50, { name: 'Renamed' });
1118
+
1119
+ const [url, opts] = mockFetch.mock.calls[0];
1120
+ expect(url.toString()).toContain('/library/50');
1121
+ expect(opts.method).toBe('PUT');
1122
+ expect(opts.body.get('name')).toBe('Renamed');
1123
+ });
1124
+
1125
+ it('getMediaUsage() should GET /library/usage/{id}', async () => {
1126
+ mockFetch.mockResolvedValue(jsonResponse({ layouts: [1, 2] }));
1127
+
1128
+ const result = await api.getMediaUsage(50);
1129
+
1130
+ const [url] = mockFetch.mock.calls[0];
1131
+ expect(url.toString()).toContain('/library/usage/50');
1132
+ expect(result.layouts).toEqual([1, 2]);
1133
+ });
1134
+
1135
+ it('tidyLibrary() should POST to /library/tidy', async () => {
1136
+ mockFetch.mockResolvedValue(emptyResponse());
1137
+
1138
+ await api.tidyLibrary();
1139
+
1140
+ const [url, opts] = mockFetch.mock.calls[0];
1141
+ expect(url.toString()).toContain('/library/tidy');
1142
+ expect(opts.method).toBe('POST');
1143
+ });
1144
+ });
1145
+
1146
+ // ── Playlist CRUD (#35) ──
1147
+
1148
+ describe('Playlist CRUD', () => {
1149
+ beforeEach(() => stubAuth());
1150
+
1151
+ it('listPlaylists() should GET and return array', async () => {
1152
+ mockFetch.mockResolvedValue(jsonResponse([{ playlistId: 1, name: 'Default' }]));
1153
+
1154
+ const playlists = await api.listPlaylists();
1155
+
1156
+ expect(playlists).toHaveLength(1);
1157
+ });
1158
+
1159
+ it('createPlaylist() should POST with name', async () => {
1160
+ mockFetch.mockResolvedValue(jsonResponse({ playlistId: 10 }));
1161
+
1162
+ const result = await api.createPlaylist('My Playlist');
1163
+
1164
+ const [, opts] = mockFetch.mock.calls[0];
1165
+ expect(opts.body.get('name')).toBe('My Playlist');
1166
+ expect(result.playlistId).toBe(10);
1167
+ });
1168
+
1169
+ it('getPlaylist() should GET /playlist/{id}', async () => {
1170
+ mockFetch.mockResolvedValue(jsonResponse({ playlistId: 10, name: 'My Playlist' }));
1171
+
1172
+ const result = await api.getPlaylist(10);
1173
+
1174
+ expect(result.name).toBe('My Playlist');
1175
+ });
1176
+
1177
+ it('editPlaylist() should PUT', async () => {
1178
+ mockFetch.mockResolvedValue(jsonResponse({ playlistId: 10 }));
1179
+
1180
+ await api.editPlaylist(10, { name: 'Renamed' });
1181
+
1182
+ const [url, opts] = mockFetch.mock.calls[0];
1183
+ expect(url.toString()).toContain('/playlist/10');
1184
+ expect(opts.method).toBe('PUT');
1185
+ });
1186
+
1187
+ it('deletePlaylist() should DELETE', async () => {
1188
+ mockFetch.mockResolvedValue(emptyResponse());
1189
+
1190
+ await api.deletePlaylist(10);
1191
+
1192
+ const [url, opts] = mockFetch.mock.calls[0];
1193
+ expect(url.toString()).toContain('/playlist/10');
1194
+ expect(opts.method).toBe('DELETE');
1195
+ });
1196
+
1197
+ it('reorderPlaylist() should POST widgets[] array params', async () => {
1198
+ mockFetch.mockResolvedValue({ ok: true, status: 200, headers: new Headers({}) });
1199
+
1200
+ await api.reorderPlaylist(10, [3, 1, 2]);
1201
+
1202
+ const [url, opts] = mockFetch.mock.calls[0];
1203
+ expect(url).toContain('/playlist/order/10');
1204
+ expect(opts.method).toBe('POST');
1205
+ expect(opts.body.getAll('widgets[]')).toEqual(['3', '1', '2']);
1206
+ });
1207
+
1208
+ it('reorderPlaylist() should throw on error', async () => {
1209
+ mockFetch.mockResolvedValue({
1210
+ ok: false,
1211
+ status: 422,
1212
+ text: () => Promise.resolve('Invalid order')
1213
+ });
1214
+
1215
+ await expect(api.reorderPlaylist(10, [1])).rejects.toThrow('422');
1216
+ });
1217
+
1218
+ it('copyPlaylist() should POST to /playlist/copy/{id}', async () => {
1219
+ mockFetch.mockResolvedValue(jsonResponse({ playlistId: 11 }));
1220
+
1221
+ const result = await api.copyPlaylist(10);
1222
+
1223
+ const [url, opts] = mockFetch.mock.calls[0];
1224
+ expect(url.toString()).toContain('/playlist/copy/10');
1225
+ expect(opts.method).toBe('POST');
1226
+ expect(result.playlistId).toBe(11);
1227
+ });
1228
+ });
1229
+
1230
+ // ── Widget Extras (#37) ──
1231
+
1232
+ describe('Widget Extras', () => {
1233
+ beforeEach(() => stubAuth());
1234
+
1235
+ it('setWidgetTransition() should PUT type and config', async () => {
1236
+ mockFetch.mockResolvedValue(jsonResponse({ widgetId: 77 }));
1237
+
1238
+ await api.setWidgetTransition(77, 'fade', { duration: 1000 });
1239
+
1240
+ const [url, opts] = mockFetch.mock.calls[0];
1241
+ expect(url.toString()).toContain('/playlist/widget/transition/77');
1242
+ expect(opts.method).toBe('PUT');
1243
+ expect(opts.body.get('type')).toBe('fade');
1244
+ expect(opts.body.get('duration')).toBe('1000');
1245
+ });
1246
+
1247
+ it('setWidgetAudio() should PUT to /playlist/widget/{id}/audio', async () => {
1248
+ mockFetch.mockResolvedValue(jsonResponse({ widgetId: 77 }));
1249
+
1250
+ await api.setWidgetAudio(77, { mediaId: 50, volume: 80 });
1251
+
1252
+ const [url, opts] = mockFetch.mock.calls[0];
1253
+ expect(url.toString()).toContain('/playlist/widget/77/audio');
1254
+ expect(opts.method).toBe('PUT');
1255
+ expect(opts.body.get('mediaId')).toBe('50');
1256
+ });
1257
+
1258
+ it('removeWidgetAudio() should DELETE /playlist/widget/{id}/audio', async () => {
1259
+ mockFetch.mockResolvedValue(emptyResponse());
1260
+
1261
+ await api.removeWidgetAudio(77);
1262
+
1263
+ const [url, opts] = mockFetch.mock.calls[0];
1264
+ expect(url.toString()).toContain('/playlist/widget/77/audio');
1265
+ expect(opts.method).toBe('DELETE');
1266
+ });
1267
+
1268
+ it('setWidgetExpiry() should PUT to /playlist/widget/{id}/expiry', async () => {
1269
+ mockFetch.mockResolvedValue(jsonResponse({ widgetId: 77 }));
1270
+
1271
+ await api.setWidgetExpiry(77, { fromDt: '2026-01-01', toDt: '2026-12-31' });
1272
+
1273
+ const [url, opts] = mockFetch.mock.calls[0];
1274
+ expect(url.toString()).toContain('/playlist/widget/77/expiry');
1275
+ expect(opts.method).toBe('PUT');
1276
+ expect(opts.body.get('fromDt')).toBe('2026-01-01');
1277
+ });
1278
+ });
1279
+
1280
+ // ── Template Save / Manage (#39) ──
1281
+
1282
+ describe('Template Save / Manage', () => {
1283
+ beforeEach(() => stubAuth());
1284
+
1285
+ it('saveAsTemplate() should POST to /template/{layoutId}', async () => {
1286
+ mockFetch.mockResolvedValue(jsonResponse({ templateId: 5 }));
1287
+
1288
+ const result = await api.saveAsTemplate(10, { name: 'My Template', includeWidgets: 1 });
1289
+
1290
+ const [url, opts] = mockFetch.mock.calls[0];
1291
+ expect(url.toString()).toContain('/template/10');
1292
+ expect(opts.method).toBe('POST');
1293
+ expect(opts.body.get('name')).toBe('My Template');
1294
+ expect(result.templateId).toBe(5);
1295
+ });
1296
+
1297
+ it('getTemplate() should GET /template/{id}', async () => {
1298
+ mockFetch.mockResolvedValue(jsonResponse({ templateId: 5, layout: 'My Template' }));
1299
+
1300
+ const result = await api.getTemplate(5);
1301
+
1302
+ expect(result.layout).toBe('My Template');
1303
+ });
1304
+
1305
+ it('deleteTemplate() should DELETE', async () => {
1306
+ mockFetch.mockResolvedValue(emptyResponse());
1307
+
1308
+ await api.deleteTemplate(5);
1309
+
1310
+ const [url, opts] = mockFetch.mock.calls[0];
1311
+ expect(url.toString()).toContain('/template/5');
1312
+ expect(opts.method).toBe('DELETE');
1313
+ });
1314
+ });
1315
+
1316
+ // ── Dataset CRUD (#28) ──
1317
+
1318
+ describe('Dataset CRUD', () => {
1319
+ beforeEach(() => stubAuth());
1320
+
1321
+ it('listDatasets() should GET and return array', async () => {
1322
+ mockFetch.mockResolvedValue(jsonResponse([{ dataSetId: 1, dataSet: 'Sales' }]));
1323
+
1324
+ const datasets = await api.listDatasets();
1325
+
1326
+ expect(datasets).toHaveLength(1);
1327
+ expect(datasets[0].dataSet).toBe('Sales');
1328
+ });
1329
+
1330
+ it('createDataset() should POST', async () => {
1331
+ mockFetch.mockResolvedValue(jsonResponse({ dataSetId: 2 }));
1332
+
1333
+ const result = await api.createDataset({ dataSet: 'Inventory', description: 'Stock levels' });
1334
+
1335
+ const [, opts] = mockFetch.mock.calls[0];
1336
+ expect(opts.method).toBe('POST');
1337
+ expect(opts.body.get('dataSet')).toBe('Inventory');
1338
+ expect(result.dataSetId).toBe(2);
1339
+ });
1340
+
1341
+ it('editDataset() should PUT to /dataset/{id}', async () => {
1342
+ mockFetch.mockResolvedValue(jsonResponse({ dataSetId: 2 }));
1343
+
1344
+ await api.editDataset(2, { description: 'Updated' });
1345
+
1346
+ const [url, opts] = mockFetch.mock.calls[0];
1347
+ expect(url.toString()).toContain('/dataset/2');
1348
+ expect(opts.method).toBe('PUT');
1349
+ });
1350
+
1351
+ it('deleteDataset() should DELETE', async () => {
1352
+ mockFetch.mockResolvedValue(emptyResponse());
1353
+
1354
+ await api.deleteDataset(2);
1355
+
1356
+ const [url, opts] = mockFetch.mock.calls[0];
1357
+ expect(url.toString()).toContain('/dataset/2');
1358
+ expect(opts.method).toBe('DELETE');
1359
+ });
1360
+
1361
+ it('listDatasetColumns() should GET /dataset/{id}/column', async () => {
1362
+ mockFetch.mockResolvedValue(jsonResponse([{ dataSetColumnId: 1, heading: 'Name' }]));
1363
+
1364
+ const cols = await api.listDatasetColumns(2);
1365
+
1366
+ const [url] = mockFetch.mock.calls[0];
1367
+ expect(url.toString()).toContain('/dataset/2/column');
1368
+ expect(cols).toHaveLength(1);
1369
+ });
1370
+
1371
+ it('createDatasetColumn() should POST', async () => {
1372
+ mockFetch.mockResolvedValue(jsonResponse({ dataSetColumnId: 3 }));
1373
+
1374
+ const result = await api.createDatasetColumn(2, { heading: 'Price', dataTypeId: 2 });
1375
+
1376
+ const [url, opts] = mockFetch.mock.calls[0];
1377
+ expect(url.toString()).toContain('/dataset/2/column');
1378
+ expect(opts.method).toBe('POST');
1379
+ expect(opts.body.get('heading')).toBe('Price');
1380
+ expect(result.dataSetColumnId).toBe(3);
1381
+ });
1382
+
1383
+ it('editDatasetColumn() should PUT to /dataset/{id}/column/{colId}', async () => {
1384
+ mockFetch.mockResolvedValue(jsonResponse({ dataSetColumnId: 3 }));
1385
+
1386
+ await api.editDatasetColumn(2, 3, { heading: 'Unit Price' });
1387
+
1388
+ const [url, opts] = mockFetch.mock.calls[0];
1389
+ expect(url.toString()).toContain('/dataset/2/column/3');
1390
+ expect(opts.method).toBe('PUT');
1391
+ });
1392
+
1393
+ it('deleteDatasetColumn() should DELETE', async () => {
1394
+ mockFetch.mockResolvedValue(emptyResponse());
1395
+
1396
+ await api.deleteDatasetColumn(2, 3);
1397
+
1398
+ const [url, opts] = mockFetch.mock.calls[0];
1399
+ expect(url.toString()).toContain('/dataset/2/column/3');
1400
+ expect(opts.method).toBe('DELETE');
1401
+ });
1402
+
1403
+ it('listDatasetData() should GET /dataset/data/{id}', async () => {
1404
+ mockFetch.mockResolvedValue(jsonResponse([{ id: 1, Name: 'Widget A' }]));
1405
+
1406
+ const rows = await api.listDatasetData(2);
1407
+
1408
+ const [url] = mockFetch.mock.calls[0];
1409
+ expect(url.toString()).toContain('/dataset/data/2');
1410
+ expect(rows).toHaveLength(1);
1411
+ });
1412
+
1413
+ it('addDatasetRow() should POST to /dataset/data/{id}', async () => {
1414
+ mockFetch.mockResolvedValue(jsonResponse({ id: 5 }));
1415
+
1416
+ const result = await api.addDatasetRow(2, { Name: 'Widget B', Price: '9.99' });
1417
+
1418
+ const [url, opts] = mockFetch.mock.calls[0];
1419
+ expect(url.toString()).toContain('/dataset/data/2');
1420
+ expect(opts.method).toBe('POST');
1421
+ expect(opts.body.get('Name')).toBe('Widget B');
1422
+ expect(result.id).toBe(5);
1423
+ });
1424
+
1425
+ it('editDatasetRow() should PUT to /dataset/data/{id}/{rowId}', async () => {
1426
+ mockFetch.mockResolvedValue(jsonResponse({ id: 5 }));
1427
+
1428
+ await api.editDatasetRow(2, 5, { Price: '12.99' });
1429
+
1430
+ const [url, opts] = mockFetch.mock.calls[0];
1431
+ expect(url.toString()).toContain('/dataset/data/2/5');
1432
+ expect(opts.method).toBe('PUT');
1433
+ });
1434
+
1435
+ it('deleteDatasetRow() should DELETE', async () => {
1436
+ mockFetch.mockResolvedValue(emptyResponse());
1437
+
1438
+ await api.deleteDatasetRow(2, 5);
1439
+
1440
+ const [url, opts] = mockFetch.mock.calls[0];
1441
+ expect(url.toString()).toContain('/dataset/data/2/5');
1442
+ expect(opts.method).toBe('DELETE');
1443
+ });
1444
+
1445
+ it('importDatasetCsv() should use requestMultipart', async () => {
1446
+ mockFetch.mockResolvedValue(jsonResponse({ imported: 10 }));
1447
+
1448
+ const formData = new FormData();
1449
+ formData.append('file', new Blob(['a,b\n1,2']), 'data.csv');
1450
+
1451
+ const result = await api.importDatasetCsv(2, formData);
1452
+
1453
+ const [url, opts] = mockFetch.mock.calls[0];
1454
+ expect(url).toContain('/dataset/import/2');
1455
+ expect(opts.body).toBe(formData);
1456
+ expect(result.imported).toBe(10);
1457
+ });
1458
+
1459
+ it('clearDataset() should DELETE /dataset/data/{id}', async () => {
1460
+ mockFetch.mockResolvedValue(emptyResponse());
1461
+
1462
+ await api.clearDataset(2);
1463
+
1464
+ const [url, opts] = mockFetch.mock.calls[0];
1465
+ expect(url.toString()).toContain('/dataset/data/2');
1466
+ expect(opts.method).toBe('DELETE');
1467
+ });
1468
+ });
1469
+
1470
+ // ── Notification CRUD (#29) ──
1471
+
1472
+ describe('Notification CRUD', () => {
1473
+ beforeEach(() => stubAuth());
1474
+
1475
+ it('listNotifications() should GET and return array', async () => {
1476
+ mockFetch.mockResolvedValue(jsonResponse([{ notificationId: 1, subject: 'Alert' }]));
1477
+
1478
+ const notifs = await api.listNotifications();
1479
+
1480
+ expect(notifs).toHaveLength(1);
1481
+ expect(notifs[0].subject).toBe('Alert');
1482
+ });
1483
+
1484
+ it('createNotification() should POST', async () => {
1485
+ mockFetch.mockResolvedValue(jsonResponse({ notificationId: 2 }));
1486
+
1487
+ const result = await api.createNotification({ subject: 'Emergency', body: 'Evacuate' });
1488
+
1489
+ const [, opts] = mockFetch.mock.calls[0];
1490
+ expect(opts.method).toBe('POST');
1491
+ expect(opts.body.get('subject')).toBe('Emergency');
1492
+ expect(result.notificationId).toBe(2);
1493
+ });
1494
+
1495
+ it('editNotification() should PUT', async () => {
1496
+ mockFetch.mockResolvedValue(jsonResponse({ notificationId: 2 }));
1497
+
1498
+ await api.editNotification(2, { body: 'Updated' });
1499
+
1500
+ const [url, opts] = mockFetch.mock.calls[0];
1501
+ expect(url.toString()).toContain('/notification/2');
1502
+ expect(opts.method).toBe('PUT');
1503
+ });
1504
+
1505
+ it('deleteNotification() should DELETE', async () => {
1506
+ mockFetch.mockResolvedValue(emptyResponse());
1507
+
1508
+ await api.deleteNotification(2);
1509
+
1510
+ const [url, opts] = mockFetch.mock.calls[0];
1511
+ expect(url.toString()).toContain('/notification/2');
1512
+ expect(opts.method).toBe('DELETE');
1513
+ });
1514
+ });
1515
+
1516
+ // ── Folder CRUD (#30) ──
1517
+
1518
+ describe('Folder CRUD', () => {
1519
+ beforeEach(() => stubAuth());
1520
+
1521
+ it('listFolders() should GET and return array', async () => {
1522
+ mockFetch.mockResolvedValue(jsonResponse([{ folderId: 1, text: 'Root' }]));
1523
+
1524
+ const folders = await api.listFolders();
1525
+
1526
+ expect(folders).toHaveLength(1);
1527
+ expect(folders[0].text).toBe('Root');
1528
+ });
1529
+
1530
+ it('createFolder() should POST', async () => {
1531
+ mockFetch.mockResolvedValue(jsonResponse({ folderId: 2 }));
1532
+
1533
+ const result = await api.createFolder({ text: 'Marketing', parentId: 1 });
1534
+
1535
+ const [, opts] = mockFetch.mock.calls[0];
1536
+ expect(opts.method).toBe('POST');
1537
+ expect(opts.body.get('text')).toBe('Marketing');
1538
+ expect(result.folderId).toBe(2);
1539
+ });
1540
+
1541
+ it('editFolder() should PUT', async () => {
1542
+ mockFetch.mockResolvedValue(jsonResponse({ folderId: 2 }));
1543
+
1544
+ await api.editFolder(2, { text: 'Rebranded' });
1545
+
1546
+ const [url, opts] = mockFetch.mock.calls[0];
1547
+ expect(url.toString()).toContain('/folder/2');
1548
+ expect(opts.method).toBe('PUT');
1549
+ });
1550
+
1551
+ it('deleteFolder() should DELETE', async () => {
1552
+ mockFetch.mockResolvedValue(emptyResponse());
1553
+
1554
+ await api.deleteFolder(2);
1555
+
1556
+ const [url, opts] = mockFetch.mock.calls[0];
1557
+ expect(url.toString()).toContain('/folder/2');
1558
+ expect(opts.method).toBe('DELETE');
1559
+ });
1560
+ });
1561
+
1562
+ // ── Tag CRUD + Entity Tagging (#31) ──
1563
+
1564
+ describe('Tag CRUD + Entity Tagging', () => {
1565
+ beforeEach(() => stubAuth());
1566
+
1567
+ it('listTags() should GET and return array', async () => {
1568
+ mockFetch.mockResolvedValue(jsonResponse([{ tagId: 1, tag: 'lobby' }]));
1569
+
1570
+ const tags = await api.listTags();
1571
+
1572
+ expect(tags).toHaveLength(1);
1573
+ expect(tags[0].tag).toBe('lobby');
1574
+ });
1575
+
1576
+ it('createTag() should POST', async () => {
1577
+ mockFetch.mockResolvedValue(jsonResponse({ tagId: 2 }));
1578
+
1579
+ const result = await api.createTag({ tag: 'outdoor' });
1580
+
1581
+ const [, opts] = mockFetch.mock.calls[0];
1582
+ expect(opts.body.get('tag')).toBe('outdoor');
1583
+ expect(result.tagId).toBe(2);
1584
+ });
1585
+
1586
+ it('editTag() should PUT', async () => {
1587
+ mockFetch.mockResolvedValue(jsonResponse({ tagId: 2 }));
1588
+
1589
+ await api.editTag(2, { tag: 'indoor' });
1590
+
1591
+ const [url, opts] = mockFetch.mock.calls[0];
1592
+ expect(url.toString()).toContain('/tag/2');
1593
+ expect(opts.method).toBe('PUT');
1594
+ });
1595
+
1596
+ it('deleteTag() should DELETE', async () => {
1597
+ mockFetch.mockResolvedValue(emptyResponse());
1598
+
1599
+ await api.deleteTag(2);
1600
+
1601
+ const [url, opts] = mockFetch.mock.calls[0];
1602
+ expect(url.toString()).toContain('/tag/2');
1603
+ expect(opts.method).toBe('DELETE');
1604
+ });
1605
+
1606
+ it('tagEntity() should POST comma-separated tags to /{entity}/{id}/tag', async () => {
1607
+ mockFetch.mockResolvedValue(emptyResponse());
1608
+
1609
+ await api.tagEntity('media', 50, ['outdoor', 'hd']);
1610
+
1611
+ const [url, opts] = mockFetch.mock.calls[0];
1612
+ expect(url.toString()).toContain('/media/50/tag');
1613
+ expect(opts.method).toBe('POST');
1614
+ expect(opts.body.get('tag')).toBe('outdoor,hd');
1615
+ });
1616
+
1617
+ it('untagEntity() should POST to /{entity}/{id}/untag', async () => {
1618
+ mockFetch.mockResolvedValue(emptyResponse());
1619
+
1620
+ await api.untagEntity('campaign', 20, ['old']);
1621
+
1622
+ const [url, opts] = mockFetch.mock.calls[0];
1623
+ expect(url.toString()).toContain('/campaign/20/untag');
1624
+ expect(opts.body.get('tag')).toBe('old');
1625
+ });
1626
+ });
1627
+
1628
+ // ── DisplayGroup Actions (#32) ──
1629
+
1630
+ describe('DisplayGroup Actions', () => {
1631
+ beforeEach(() => stubAuth());
1632
+
1633
+ it('dgChangeLayout() should POST layoutId to action/changeLayout', async () => {
1634
+ mockFetch.mockResolvedValue(emptyResponse());
1635
+
1636
+ await api.dgChangeLayout(5, 10);
1637
+
1638
+ const [url, opts] = mockFetch.mock.calls[0];
1639
+ expect(url.toString()).toContain('/displaygroup/5/action/changeLayout');
1640
+ expect(opts.method).toBe('POST');
1641
+ expect(opts.body.get('layoutId')).toBe('10');
1642
+ });
1643
+
1644
+ it('dgOverlayLayout() should POST to action/overlayLayout', async () => {
1645
+ mockFetch.mockResolvedValue(emptyResponse());
1646
+
1647
+ await api.dgOverlayLayout(5, 10);
1648
+
1649
+ const [url, opts] = mockFetch.mock.calls[0];
1650
+ expect(url.toString()).toContain('/displaygroup/5/action/overlayLayout');
1651
+ expect(opts.body.get('layoutId')).toBe('10');
1652
+ });
1653
+
1654
+ it('dgRevertToSchedule() should POST to action/revertToSchedule', async () => {
1655
+ mockFetch.mockResolvedValue(emptyResponse());
1656
+
1657
+ await api.dgRevertToSchedule(5);
1658
+
1659
+ const [url, opts] = mockFetch.mock.calls[0];
1660
+ expect(url.toString()).toContain('/displaygroup/5/action/revertToSchedule');
1661
+ expect(opts.method).toBe('POST');
1662
+ });
1663
+
1664
+ it('dgCollectNow() should POST to action/collectNow', async () => {
1665
+ mockFetch.mockResolvedValue(emptyResponse());
1666
+
1667
+ await api.dgCollectNow(5);
1668
+
1669
+ const [url, opts] = mockFetch.mock.calls[0];
1670
+ expect(url.toString()).toContain('/displaygroup/5/action/collectNow');
1671
+ expect(opts.method).toBe('POST');
1672
+ });
1673
+
1674
+ it('dgSendCommand() should POST commandId to action/command', async () => {
1675
+ mockFetch.mockResolvedValue(emptyResponse());
1676
+
1677
+ await api.dgSendCommand(5, 2);
1678
+
1679
+ const [url, opts] = mockFetch.mock.calls[0];
1680
+ expect(url.toString()).toContain('/displaygroup/5/action/command');
1681
+ expect(opts.body.get('commandId')).toBe('2');
1682
+ });
1683
+
1684
+ it('editDisplayGroup() should PUT to /displaygroup/{id}', async () => {
1685
+ mockFetch.mockResolvedValue(jsonResponse({ displayGroupId: 5 }));
1686
+
1687
+ const result = await api.editDisplayGroup(5, { displayGroup: 'Renamed', description: 'New desc' });
1688
+
1689
+ const [url, opts] = mockFetch.mock.calls[0];
1690
+ expect(url.toString()).toContain('/displaygroup/5');
1691
+ expect(opts.method).toBe('PUT');
1692
+ expect(opts.body.get('displayGroup')).toBe('Renamed');
1693
+ expect(result.displayGroupId).toBe(5);
1694
+ });
1695
+ });
1696
+
785
1697
  // ── Token Auto-Refresh Integration ──
786
1698
 
787
1699
  describe('Token auto-refresh', () => {