musora-content-services 1.0.99 → 1.0.100

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [1.0.100](https://github.com/railroadmedia/musora-content-services/compare/v1.0.99...v1.0.100) (2024-09-16)
6
+
5
7
  ### [1.0.99](https://github.com/railroadmedia/musora-content-services/compare/v1.0.98...v1.0.99) (2024-09-12)
6
8
 
7
9
  ### [1.0.98](https://github.com/railroadmedia/musora-content-services/compare/v1.0.97...v1.0.98) (2024-09-12)
package/link_mcs.sh CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "1.0.99",
3
+ "version": "1.0.100",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -78,13 +78,15 @@ let contentTypeConfig = {
78
78
  '"instructors": instructor[]->name',
79
79
  `"description": ${descriptionField}`,
80
80
  'resource',
81
+ 'xp',
82
+ 'total_xp',
81
83
  `"lessons": child[]->{
82
84
  "id": railcontent_id,
83
85
  title,
84
86
  "image": thumbnail.asset->url,
85
87
  "instructors": instructor[]->name,
86
88
  length_in_seconds,
87
- }`,
89
+ }`,
88
90
  ]
89
91
  },
90
92
  'method': {
@@ -182,9 +184,19 @@ let contentTypeConfig = {
182
184
  },
183
185
  'instructor': {
184
186
  'fields': [
185
- 'name',
187
+ '"coach_top_banner_image": coach_top_banner_image.asset->url',
188
+ '"coach_bottom_banner_image": coach_bottom_banner_image.asset->url',
186
189
  '"coach_card_image": coach_card_image.asset->url',
187
- 'focus'
190
+ '"coach_featured_image": coach_featured_image.asset->url',
191
+ '"coach_top_banner_image": coach_top_banner_image.asset->url',
192
+ 'focus',
193
+ 'focus_text',
194
+ 'forum_thread_id',
195
+ 'long_bio',
196
+ 'name',
197
+ 'short_bio',
198
+ 'bands',
199
+ 'endorsements',
188
200
  ]
189
201
  },
190
202
  // content with just the added 'instructors' Field
@@ -215,6 +227,34 @@ let contentTypeConfig = {
215
227
  'exploring-beats': contentWithSortField,
216
228
  }
217
229
 
230
+ function getNewReleasesTypes(brand) {
231
+ const baseNewTypes = ["student-review", "student-reviews", "student-focus", "coach-stream", "live", "question-and-answer", "boot-camps", "quick-tips", "workout", "challenge", "challenge-part", "podcasts", "pack", "song", "learning-path-level", "play-along", "course", "unit"];
232
+ switch(brand) {
233
+ case 'drumeo':
234
+ return [...baseNewTypes, "drum-fest-international-2022", "spotlight", "the-history-of-electronic-drums", "backstage-secrets", "student-collaborations", "live-streams", "solos", "gear-guides", "performances", "in-rhythm", "challenges", "on-the-road", "diy-drum-experiments", "rhythmic-adventures-of-captain-carson", "study-the-greats", "rhythms-from-another-planet", "tama-drums", "paiste-cymbals", "behind-the-scenes", "exploring-beats", "sonor-drums"];
235
+ case 'guitareo':
236
+ return [...baseNewTypes, "archives", "recording", "chords-and-scales"];
237
+ case 'pianote':
238
+ case 'singeo':
239
+ default:
240
+ return baseNewTypes
241
+ }
242
+ }
243
+
244
+ function getUpcomingEventsTypes(brand) {
245
+ const baseLiveTypes = ["student-review", "student-reviews", "student-focus", "coach-stream", "live", "question-and-answer", "boot-camps", "quick-tips", "recording", "pack-bundle-lesson"];
246
+ switch(brand) {
247
+ case 'drumeo':
248
+ return [...baseLiveTypes, "drum-fest-international-2022", "spotlight", "the-history-of-electronic-drums", "backstage-secrets", "student-collaborations", "live-streams", "podcasts", "solos", "gear-guides", "performances", "in-rhythm", "challenges", "on-the-road", "diy-drum-experiments", "rhythmic-adventures-of-captain-carson", "study-the-greats", "rhythms-from-another-planet", "tama-drums", "paiste-cymbals", "behind-the-scenes", "exploring-beats", "sonor-drums"];
249
+ case 'guitareo':
250
+ return [...baseLiveTypes, "archives"];
251
+ case 'pianote':
252
+ case 'singeo':
253
+ default:
254
+ return baseLiveTypes;
255
+ }
256
+ }
257
+
218
258
  function artistOrInstructorName(key='artist_name') {
219
259
  return `'${key}': coalesce(artist->name, instructor[0]->name)`;
220
260
  }
@@ -264,4 +304,6 @@ module.exports = {
264
304
  DEFAULT_FIELDS,
265
305
  assignmentsField,
266
306
  filtersToGroq,
307
+ getNewReleasesTypes,
308
+ getUpcomingEventsTypes,
267
309
  }
package/src/index.d.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  fetchLessonContent,
29
29
  fetchCourseOverview,
30
30
  fetchChallengeOverview,
31
+ fetchScheduledReleases,
31
32
  fetchArtistLessons,
32
33
  fetchGenreLessons,
33
34
  } from './services/sanity.js';
@@ -80,6 +81,7 @@ declare module 'musora-content-services' {
80
81
  fetchAllCompletedStates,
81
82
  fetchContentInProgress,
82
83
  fetchContentPageUserData,
84
+ fetchScheduledReleases,
83
85
  fetchArtistLessons,
84
86
  fetchGenreLessons,
85
87
  }
package/src/index.js CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  fetchChallengeOverview,
36
36
  fetchCoachLessons,
37
37
  fetchByReference,
38
+ fetchScheduledReleases,
38
39
  fetchArtistLessons,
39
40
  fetchGenreLessons,
40
41
  } from './services/sanity.js';
@@ -91,6 +92,7 @@ export {
91
92
  fetchContentPageUserData,
92
93
  fetchCoachLessons,
93
94
  fetchByReference,
95
+ fetchScheduledReleases,
94
96
  fetchArtistLessons,
95
97
  fetchGenreLessons,
96
98
  }
@@ -261,4 +261,54 @@ function fetchAbsolute(url, params) {
261
261
  }
262
262
  }
263
263
  return fetch(url, params);
264
+ }
265
+
266
+ export async function fetchUserContext() {
267
+ let url = `/content/user_data_all`;
268
+ const headers = {
269
+ 'Content-Type': 'application/json',
270
+ 'X-CSRF-TOKEN': globalConfig.railcontentConfig.token
271
+ };
272
+ try {
273
+ const response = await fetch(url, {headers});
274
+ const result = await response.json();
275
+ if (result) {
276
+ console.log('fetchUserContext', result);
277
+ return result;
278
+ } else {
279
+ console.log('result not json');
280
+ }
281
+ } catch (error) {
282
+ console.error('Fetch error:', error);
283
+ return null;
284
+ }
285
+ }
286
+
287
+ export async function fetchHandler(url, method = "get") {
288
+ const headers = {
289
+ 'Content-Type': 'application/json',
290
+ 'X-CSRF-TOKEN': globalConfig.railcontentConfig.token
291
+ };
292
+ try {
293
+ const response = await fetch(url, {method, headers});
294
+ const result = await response.json();
295
+ if (result) {
296
+ return result;
297
+ } else {
298
+ console.log('result not json');
299
+ }
300
+ } catch (error) {
301
+ console.error('Fetch error:', error);
302
+ return null;
303
+ }
304
+ }
305
+
306
+ export async function fetchLikeContent(contentId) {
307
+ let url = `/content/${contentId}/like`;
308
+ return await fetchHandler(url, "post");
309
+ }
310
+
311
+ export async function fetchUnlikeContent(contentId) {
312
+ let url = `/content/${contentId}/unlike`;
313
+ return await fetchHandler(url, "post");
264
314
  }
@@ -9,7 +9,9 @@ import {
9
9
  contentTypeConfig,
10
10
  DEFAULT_FIELDS,
11
11
  getFieldsForContentType,
12
- filtersToGroq
12
+ filtersToGroq,
13
+ getUpcomingEventsTypes,
14
+ getNewReleasesTypes,
13
15
  } from "../contentTypeConfig";
14
16
  import {globalConfig} from "./config";
15
17
 
@@ -254,14 +256,8 @@ export async function fetchWorkouts(brand) {
254
256
  * @returns {Promise<Object|null>} - The fetched new releases data or null if not found.
255
257
  */
256
258
  export async function fetchNewReleases(brand, { page = 1, limit = 10, sort="-published_on" } = {}) {
257
- const newTypes = {
258
- 'drumeo': ["drum-fest-international-2022", "spotlight", "the-history-of-electronic-drums", "backstage-secrets", "quick-tips", "question-and-answer", "student-collaborations", "live-streams", "live", "podcasts", "solos", "boot-camps", "gear-guides", "performances", "in-rhythm", "challenges", "on-the-road", "diy-drum-experiments", "rhythmic-adventures-of-captain-carson", "study-the-greats", "rhythms-from-another-planet", "tama-drums", "paiste-cymbals", "behind-the-scenes", "exploring-beats", "sonor-drums", "course", "play-along", "student-focus", "coach-stream", "learning-path-level", "unit", "quick-tips", "live", "question-and-answer", "student-review", "boot-camps", "song", "chords-and-scales", "pack", "podcasts", "workout", "challenge", "challenge-part"],
259
- 'pianote': ["student-review", "student-reviews", "question-and-answer", "course", "play-along", "student-focus", "coach-stream", "learning-path-level", "unit", "quick-tips", "live", "question-and-answer", "student-review", "boot-camps", "song", "chords-and-scales", "pack", "podcasts", "workout", "challenge", "challenge-part"],
260
- 'guitareo': ["student-review", "student-reviews", "question-and-answer", "archives", "recording", "course", "play-along", "student-focus", "coach-stream", "learning-path-level", "unit", "quick-tips", "live", "question-and-answer", "student-review", "boot-camps", "song", "chords-and-scales", "pack", "podcasts", "workout", "challenge", "challenge-part"],
261
- 'singeo': ["student-review", "student-reviews", "question-and-answer", "course", "play-along", "student-focus", "coach-stream", "learning-path-level", "unit", "quick-tips", "live", "question-and-answer", "student-review", "boot-camps", "song", "chords-and-scales", "pack", "podcasts", "workout", "challenge", "challenge-part"],
262
- 'default': ["student-review", "student-reviews", "question-and-answer", "course", "play-along", "student-focus", "coach-stream", "learning-path-level", "unit", "quick-tips", "live", "question-and-answer", "student-review", "boot-camps", "song", "chords-and-scales", "pack", "podcasts", "workout", "challenge", "challenge-part"]
263
- };
264
- const typesString = arrayJoinWithQuotes(newTypes[brand] ?? newTypes['default']);
259
+ const newTypes = getNewReleasesTypes(brand);
260
+ const typesString = arrayJoinWithQuotes(newTypes);
265
261
  const start = (page - 1) * limit;
266
262
  const end = start + limit;
267
263
  const sortOrder = getSortOrder(sort);
@@ -297,15 +293,8 @@ export async function fetchNewReleases(brand, { page = 1, limit = 10, sort="-pub
297
293
  * .catch(error => console.error(error));
298
294
  */
299
295
  export async function fetchUpcomingEvents(brand, { page = 1, limit = 10 } = {}) {
300
- const baseLiveTypes = ["student-review", "student-reviews", "student-focus", "coach-stream", "live", "question-and-answer", "student-review", "boot-camps", "recording", "pack-bundle-lesson"];
301
- const liveTypes = {
302
- 'drumeo': [...baseLiveTypes, "drum-fest-international-2022", "spotlight", "the-history-of-electronic-drums", "backstage-secrets", "quick-tips", "student-collaborations", "live-streams", "podcasts", "solos", "gear-guides", "performances", "in-rhythm", "challenges", "on-the-road", "diy-drum-experiments", "rhythmic-adventures-of-captain-carson", "study-the-greats", "rhythms-from-another-planet", "tama-drums", "paiste-cymbals", "behind-the-scenes", "exploring-beats", "sonor-drums"],
303
- 'pianote': baseLiveTypes,
304
- 'guitareo': [...baseLiveTypes, "archives"],
305
- 'singeo': baseLiveTypes,
306
- 'default': baseLiveTypes
307
- };
308
- const typesString = arrayJoinWithQuotes(liveTypes[brand] ?? liveTypes['default']);
296
+ const liveTypes = getUpcomingEventsTypes(brand);
297
+ const typesString = arrayJoinWithQuotes(liveTypes);
309
298
  const now = getSanityDate(new Date());
310
299
  const start = (page - 1) * limit;
311
300
  const end = start + limit;
@@ -325,6 +314,45 @@ export async function fetchUpcomingEvents(brand, { page = 1, limit = 10 } = {})
325
314
  return fetchSanity(query, true);
326
315
  }
327
316
 
317
+ /**
318
+ * Fetch scheduled releases for a specific brand.
319
+ *
320
+ * @param {string} brand - The brand for which to fetch scheduled releasess.
321
+ * @returns {Promise<Object|null>} - A promise that resolves to an array of scheduled release objects or null if not found.
322
+ *
323
+ * @example
324
+ * fetchScheduledReleases('drumeo', {
325
+ * page: 2,
326
+ * limit: 20,
327
+ * })
328
+ * .then(content => console.log(content))
329
+ * .catch(error => console.error(error));
330
+ */
331
+ export async function fetchScheduledReleases(brand, { page = 1, limit = 10 }) {
332
+ const upcomingTypes = getUpcomingEventsTypes(brand);
333
+ const newTypes = getNewReleasesTypes(brand);
334
+
335
+ const scheduledTypes = merge(upcomingTypes, newTypes)
336
+ const typesString = arrayJoinWithQuotes(scheduledTypes);
337
+ const now = getSanityDate(new Date());
338
+ const start = (page - 1) * limit;
339
+ const end = start + limit;
340
+ const query = `*[_type in [${typesString}] && brand == '${brand}' && status in ['published','scheduled'] && published_on > '${now}']{
341
+ "id": railcontent_id,
342
+ title,
343
+ "image": thumbnail.asset->url,
344
+ "artist_name": instructor[0]->name,
345
+ "artists": instructor[]->name,
346
+ difficulty,
347
+ difficulty_string,
348
+ length_in_seconds,
349
+ published_on,
350
+ "type": _type,
351
+ web_url_path,
352
+ } | order(published_on asc)[${start}...${end}]`;
353
+ return fetchSanity(query, true);
354
+ }
355
+
328
356
  /**
329
357
  * Fetch content by a specific Railcontent ID.
330
358
  *
@@ -415,7 +443,7 @@ export async function fetchAll(brand, type, {
415
443
  const searchFilter = searchTerm
416
444
  ? groupBy !== "" ?
417
445
  `&& (^.name match "${searchTerm}*" || title match "${searchTerm}*")`
418
- : `&& (artist->name match "${searchTerm}*" || instructor[]->name match "${searchTerm}*" || title match "${searchTerm}*")`
446
+ : `&& (artist->name match "${searchTerm}*" || instructor[]->name match "${searchTerm}*" || title match "${searchTerm}*" || name match "${searchTerm}*")`
419
447
  : "";
420
448
 
421
449
  // Construct the included fields filter, replacing 'difficulty' with 'difficulty_string'
@@ -499,8 +527,9 @@ export function getSortOrder(sort= '-published_on', groupBy)
499
527
  case "slug":
500
528
  sortOrder = groupBy ? 'name' : "title";
501
529
  break;
530
+ case "name":
502
531
  case "popularity":
503
- sortOrder = "popularity";
532
+ sortOrder = sort;
504
533
  break;
505
534
  case "published_on":
506
535
  default:
@@ -664,6 +693,7 @@ export async function fetchMethod(brand, slug) {
664
693
  "type": _type,
665
694
  "description": ${descriptionField},
666
695
  "url": web_url_path,
696
+ web_url_path,
667
697
  xp,
668
698
  }
669
699
  } | order(published_on asc)`
@@ -821,7 +851,13 @@ export async function fetchLessonContent(railContentId) {
821
851
  "coach_profile_image":thumbnail_url.asset->url
822
852
  },
823
853
  "instructors":instructor[]->name,
824
- instructor[]->,
854
+ "instructor": instructor[]->{
855
+ "id":_id,
856
+ name,
857
+ short_bio,
858
+ web_url_path,
859
+ "coach_card_image": coach_card_image.asset->url,
860
+ },
825
861
  ${assignmentsField}
826
862
  video,
827
863
  length_in_seconds
@@ -1224,6 +1260,13 @@ function getSanityDate(date) {
1224
1260
  return date.toISOString();
1225
1261
  }
1226
1262
 
1263
+ const merge = (a, b, predicate = (a, b) => a === b) => {
1264
+ const c = [...a]; // copy to avoid side effects
1265
+ // add all items from B to copy C if they're not already present
1266
+ b.forEach((bItem) => (c.some((cItem) => predicate(bItem, cItem)) ? null : c.push(bItem)))
1267
+ return c;
1268
+ }
1269
+
1227
1270
  function checkSanityConfig(config) {
1228
1271
  if (!config.sanityConfig.token) {
1229
1272
  console.warn('fetchSanity: The "token" property is missing in the config object.');
@@ -0,0 +1,105 @@
1
+ import {fetchUserContext, fetchLikeContent, fetchUnlikeContent} from "./railcontent";
2
+
3
+ const StorageKey = "userContext";
4
+ let userContext = null;
5
+ let cache = null;
6
+
7
+ export function init(localCache) {
8
+ cache = localCache;
9
+ }
10
+
11
+ export function version() {
12
+ ensureLocalContextLoaded();
13
+ return userContext.version;
14
+ }
15
+
16
+ async function getUserContext() {
17
+ ensureLocalContextLoaded();
18
+ if (userContext) {
19
+ verifyContextIsValid();
20
+ }
21
+ if (!userContext) {
22
+ await fetchFromServer();
23
+ }
24
+ return userContext;
25
+ }
26
+
27
+ function verifyContextIsValid() {
28
+
29
+ }
30
+
31
+ function ensureLocalContextLoaded() {
32
+ if (userContext) return;
33
+ let localData = cache.getItem(StorageKey);
34
+ if (localData) {
35
+ userContext = JSON.parse(localData);
36
+ }
37
+ }
38
+
39
+ function updateLocalContext(contentId, updateFunction) {
40
+ ensureLocalContextLoaded();
41
+ if (userContext) {
42
+ let contentData = userContext.data[contentId] ?? [];
43
+ updateFunction(contentData);
44
+ userContext.data[contentId] = contentData;
45
+ userContext.version++;
46
+ let data = JSON.stringify(userContext);
47
+ cache.setItem(StorageKey, data);
48
+ }
49
+ }
50
+
51
+ async function fetchFromServer() {
52
+ let data = await fetchUserContext();
53
+ userContext = JSON.parse(data);
54
+ cache.setItem(StorageKey, data);
55
+ }
56
+
57
+
58
+ function transformData(data, contentId) {
59
+ let transformed = [];
60
+ transformed["contentId"] = contentId;
61
+ transformed["liked"] = (data && data.l) ?? 0;
62
+ return transformed;
63
+ }
64
+
65
+ export async function fetchContentData(contentId) {
66
+ let userContext = await getUserContext();
67
+ let data = userContext.data[contentId];
68
+ data = transformData(data, contentId);
69
+ return data;
70
+ }
71
+
72
+ export function clearCache() {
73
+ userContext = null;
74
+ cache.setItem(StorageKey, null);
75
+ }
76
+
77
+ export function testClearLocal() {
78
+ userContext = null;
79
+ }
80
+
81
+ export async function likeContent(contentId) {
82
+ updateLocalContext(contentId,
83
+ function (contentData) {
84
+ contentData.l = 1;
85
+ }
86
+ );
87
+
88
+ let result = await fetchLikeContent(contentId);
89
+ if (result.version !== userContext.version) {
90
+ clearCache();
91
+ }
92
+ }
93
+
94
+ export async function unlikeContent(contentId) {
95
+ updateLocalContext(contentId,
96
+ function (contentData) {
97
+ contentData.l = 0;
98
+ }
99
+ );
100
+
101
+ let result = await fetchUnlikeContent(contentId);
102
+ if (result.version !== userContext.version) {
103
+ clearCache();
104
+ }
105
+ }
@@ -0,0 +1,21 @@
1
+ export class LocalStorageMock {
2
+ constructor() {
3
+ this.store = {};
4
+ }
5
+
6
+ clear() {
7
+ this.store = {};
8
+ }
9
+
10
+ getItem(key) {
11
+ return this.store[key] || null;
12
+ }
13
+
14
+ setItem(key, value) {
15
+ this.store[key] = String(value);
16
+ }
17
+
18
+ removeItem(key) {
19
+ delete this.store[key];
20
+ }
21
+ }
@@ -32,6 +32,7 @@ const {
32
32
  fetchPacksAll,
33
33
  fetchCoachLessons,
34
34
  fetchByReference,
35
+ fetchScheduledReleases
35
36
  } = require('../src/services/sanity.js');
36
37
 
37
38
  describe('Sanity Queries', function () {
@@ -286,4 +287,9 @@ describe('Sanity Queries', function () {
286
287
  const response = await fetchByReference('drumeo', { includedFields: ['is_featured'] });
287
288
  expect(response.entity.length).toBeGreaterThan(0);
288
289
  });
290
+
291
+ test('fetchScheduledReleases', async () => {
292
+ const response = await fetchScheduledReleases('drumeo', {});
293
+ expect(response.length).toBeGreaterThan(0);
294
+ });
289
295
  });
@@ -0,0 +1,86 @@
1
+ const railContentModule = require('../src/services/railcontent.js')
2
+ const userContextModule = require('../src/services/userContext.js');
3
+ import {LocalStorageMock} from "./localStorageMock";
4
+
5
+ describe('userContext', function () {
6
+ let mock = null;
7
+ const testVersion = 1;
8
+ beforeEach(() => {
9
+ userContextModule.init(new LocalStorageMock());
10
+ mock = jest.spyOn(railContentModule, 'fetchUserContext');
11
+ var json = `{"version":${testVersion},"data":{"308516":{"l":1},"308515":{"p":100},"308514":{"p":13},"308518":{"p":100}}}`;
12
+ mock.mockImplementation(() => json);
13
+ });
14
+
15
+ test('contentLiked', async () => {
16
+ let contentData = await userContextModule.fetchContentData(308516);
17
+ expect(contentData.liked).toBe(1);
18
+ });
19
+
20
+ test('contentDoesNotExist', async () => {
21
+ //fetch content that does not exist
22
+ let contentData = await userContextModule.fetchContentData(121111);
23
+ expect(contentData.liked).toBe(0);
24
+ });
25
+
26
+ test('ensureOnlyOneServerFetchRequest', async () => {
27
+ userContextModule.clearCache();
28
+ await userContextModule.fetchContentData(308516);
29
+ await userContextModule.fetchContentData(308514);
30
+ expect(railContentModule.fetchUserContext).toHaveBeenCalledTimes(1);
31
+ });
32
+
33
+ test('ensureDataPulledFromLocalCache', async () => {
34
+ userContextModule.clearCache();
35
+ await userContextModule.fetchContentData(308516);
36
+ userContextModule.testClearLocal();
37
+ await userContextModule.fetchContentData(308514);
38
+ expect(railContentModule.fetchUserContext).toHaveBeenCalledTimes(1);
39
+ });
40
+
41
+ // test('hashExpiration', async () => {
42
+ // userContextModule.clearCache();
43
+ // await userContextModule.fetchContentData(testHash308516);
44
+ // let newHash = "8g9qg5wn3e5s5oi69q6g22et9w6g34t5";
45
+ // await userContextModule.fetchContentData(newHash, 308516);
46
+ // expect(railContentModule.fetchUserContext).toHaveBeenCalledTimes(2);
47
+ // });
48
+
49
+ test('likeContent', async () => {
50
+ mock = jest.spyOn(railContentModule, 'fetchLikeContent');
51
+ var json = JSON.parse(`{"version":${testVersion + 1}}`);
52
+ mock.mockImplementation(() => json);
53
+
54
+ userContextModule.clearCache();
55
+ await userContextModule.fetchContentData(308515);
56
+ await userContextModule.likeContent(308515);
57
+ let contentData = await userContextModule.fetchContentData(308515);
58
+ expect(contentData.liked).toBe(1);
59
+
60
+ userContextModule.testClearLocal();
61
+ contentData = await userContextModule.fetchContentData(308515);
62
+ expect(contentData.liked).toBe(1);
63
+
64
+ expect(userContextModule.version()).toBe(testVersion + 1);
65
+ });
66
+
67
+
68
+ test('unlikeContent', async () => {
69
+ mock = jest.spyOn(railContentModule, 'fetchUnlikeContent');
70
+ var json = JSON.parse(`{"version":${testVersion + 1}}`);
71
+ mock.mockImplementation(() => json);
72
+
73
+ userContextModule.clearCache();
74
+ await userContextModule.fetchContentData(308516);
75
+ await userContextModule.unlikeContent(308516);
76
+ let contentData = await userContextModule.fetchContentData(308516);
77
+ expect(contentData.liked).toBe(0);
78
+
79
+ userContextModule.testClearLocal();
80
+ contentData = await userContextModule.fetchContentData(308516);
81
+ expect(contentData.liked).toBe(0);
82
+
83
+ expect(userContextModule.version()).toBe(testVersion + 1);
84
+ });
85
+
86
+ });