musora-content-services 2.167.1 → 2.167.3

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.
Files changed (136) hide show
  1. package/.agent/decisions/2026-07-17-onboarding-statuses.md +21 -0
  2. package/.agent/decisions/2026-07-20-mu2-1724-songs-lessons-counts.md +33 -0
  3. package/.agent/decisions/2026-07-20-query-builder-tostring.md +37 -0
  4. package/CHANGELOG.md +15 -0
  5. package/package.json +2 -1
  6. package/src/contentTypeConfig.js +2 -0
  7. package/src/index.d.ts +2 -0
  8. package/src/index.js +2 -0
  9. package/src/lib/sanity/query.ts +5 -0
  10. package/src/services/config.js +15 -0
  11. package/src/services/content/artist.ts +1 -3
  12. package/src/services/content/counts.ts +47 -0
  13. package/src/services/content/genre.ts +1 -3
  14. package/src/services/content/instructor.ts +1 -3
  15. package/src/services/contentProgress.js +1 -1
  16. package/src/services/eventsAPI.js +49 -7
  17. package/src/services/user/onboarding.ts +22 -0
  18. package/src/services/userActivity.js +2 -0
  19. package/src/version-info.js +1 -0
  20. package/.claude/settings.local.json +0 -52
  21. package/test/initializeTests.js +0 -60
  22. package/test/integration/awards/award-exclusion-handling.test.ts +0 -275
  23. package/test/integration/awards/internal/image-utils.test.ts +0 -86
  24. package/test/integration/content-progress/__mocks__/mocks.ts +0 -104
  25. package/test/integration/content-progress/contentProgress.test.ts +0 -335
  26. package/test/integration/content-progress/e2eOfflineProgress.test.ts +0 -352
  27. package/test/integration/content-progress/e2eProgress.test.ts +0 -612
  28. package/test/integration/content-progress/getters.test.ts +0 -334
  29. package/test/integration/content-progress/helpers.test.ts +0 -263
  30. package/test/integration/content-progress/offlineContentProgress.test.ts +0 -226
  31. package/test/integration/forums.test.ts +0 -209
  32. package/test/integration/infrastructure/FetchRequestExecutor.test.ts +0 -88
  33. package/test/integration/initializeTestDB.ts +0 -80
  34. package/test/integration/learning-paths.test.ts +0 -854
  35. package/test/integration/notifications.test.ts +0 -178
  36. package/test/integration/progress-row/content-card.test.ts +0 -283
  37. package/test/integration/sync/adapters/idb-errors.test.ts +0 -144
  38. package/test/integration/sync/adapters/sqlite-errors.test.ts +0 -173
  39. package/test/integration/sync/fetch.test.ts +0 -224
  40. package/test/integration/sync/repositories/content-likes.test.ts +0 -99
  41. package/test/integration/sync/repositories/practices.test.ts +0 -179
  42. package/test/integration/sync/repositories/progress.test.ts +0 -245
  43. package/test/integration/sync/repositories/user-award-progress.static.test.ts +0 -68
  44. package/test/integration/sync/repositories/user-award-progress.test.ts +0 -387
  45. package/test/integration/sync/store/cross-user-protection.test.ts +0 -217
  46. package/test/integration/sync/store/store-idb.test.ts +0 -180
  47. package/test/integration/sync/store/store.test.ts +0 -361
  48. package/test/integration/user/permissions.test.js +0 -20
  49. package/test/integration/userActivity.test.ts +0 -102
  50. package/test/live/README.md +0 -29
  51. package/test/live/content.test.js +0 -116
  52. package/test/localStorageMock.js +0 -21
  53. package/test/log.js +0 -5
  54. package/test/mockData/award-definitions.js +0 -171
  55. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -35
  56. package/test/mockData/mockData_progress_content.json +0 -182
  57. package/test/mockData/mockData_sanity_progress_content.json +0 -1451
  58. package/test/mockData/mockData_user_practices.json +0 -9
  59. package/test/setupConsole.js +0 -6
  60. package/test/setupNetworkGuard.js +0 -3
  61. package/test/setupTimers.js +0 -13
  62. package/test/unit/HttpClient.test.ts +0 -267
  63. package/test/unit/awards/award-alacarte-observer.test.ts +0 -199
  64. package/test/unit/awards/award-auto-refresh.test.ts +0 -84
  65. package/test/unit/awards/award-calculations.test.ts +0 -34
  66. package/test/unit/awards/award-callbacks.test.ts +0 -144
  67. package/test/unit/awards/award-certificate-display.test.ts +0 -330
  68. package/test/unit/awards/award-collection-edge-cases.test.ts +0 -212
  69. package/test/unit/awards/award-collection-filtering.test.ts +0 -287
  70. package/test/unit/awards/award-completion-flow.test.ts +0 -306
  71. package/test/unit/awards/award-multi-lesson.test.ts +0 -243
  72. package/test/unit/awards/award-observer-integration.test.ts +0 -327
  73. package/test/unit/awards/award-query-messages.test.ts +0 -448
  74. package/test/unit/awards/award-user-collection.test.ts +0 -416
  75. package/test/unit/awards/duplicate-prevention.test.ts +0 -120
  76. package/test/unit/awards/helpers/completion-mock.ts +0 -56
  77. package/test/unit/awards/helpers/index.ts +0 -3
  78. package/test/unit/awards/helpers/mock-setup.ts +0 -79
  79. package/test/unit/awards/helpers/progress-emitter.ts +0 -45
  80. package/test/unit/awards/message-generator.test.ts +0 -162
  81. package/test/unit/content-progress/bubbleTrickle.test.ts +0 -322
  82. package/test/unit/content-progress/helpers.test.ts +0 -329
  83. package/test/unit/content-progress/navigateTo.test.ts +0 -381
  84. package/test/unit/contentLikes.test.ts +0 -62
  85. package/test/unit/contentMetaData.test.ts +0 -58
  86. package/test/unit/dataContext.test.ts +0 -38
  87. package/test/unit/dateUtils.test.ts +0 -188
  88. package/test/unit/endScreen.test.js +0 -712
  89. package/test/unit/imageSRCBuilder.test.ts +0 -37
  90. package/test/unit/imageSRCVerify.test.ts +0 -155
  91. package/test/unit/infrastructure/DefaultHeaderProvider.test.ts +0 -39
  92. package/test/unit/infrastructure/sanity/ContentClient.test.ts +0 -168
  93. package/test/unit/infrastructure/sanity/DefaultConfigProvider.test.ts +0 -93
  94. package/test/unit/infrastructure/sanity/FetchQueryExecutor.test.ts +0 -174
  95. package/test/unit/infrastructure/sanity/SanityClient.test.ts +0 -140
  96. package/test/unit/lib/__snapshots__/filter.test.ts.snap +0 -5
  97. package/test/unit/lib/filter.test.ts +0 -1154
  98. package/test/unit/lib/lastUpdated.test.ts +0 -21
  99. package/test/unit/lib/query.test.ts +0 -1469
  100. package/test/unit/lib/sanity/decorators/base.test.ts +0 -368
  101. package/test/unit/lib/sanity/decorators/navigate-to.test.ts +0 -361
  102. package/test/unit/lib/sanity/decorators/need-access.test.ts +0 -89
  103. package/test/unit/lib/sanity/decorators/need-lifetime-upgrade.test.ts +0 -85
  104. package/test/unit/lib/sanity/decorators/page-type.test.ts +0 -81
  105. package/test/unit/playerSettings.test.ts +0 -73
  106. package/test/unit/progress-row/content-card.test.ts +0 -314
  107. package/test/unit/progress-row/playlist-card.test.ts +0 -104
  108. package/test/unit/progressRows.test.ts +0 -243
  109. package/test/unit/recommendations.test.js +0 -47
  110. package/test/unit/sanityQueryService.test.ts +0 -200
  111. package/test/unit/search.test.ts +0 -44
  112. package/test/unit/sentry.test.ts +0 -62
  113. package/test/unit/services/progress-internal/bubble.test.ts +0 -279
  114. package/test/unit/services/progress.test.ts +0 -396
  115. package/test/unit/streakMessage.test.ts +0 -251
  116. package/test/unit/sync/context.test.ts +0 -51
  117. package/test/unit/sync/effects/logout-warning.test.ts +0 -158
  118. package/test/unit/sync/effects/push-failure-notification.test.ts +0 -196
  119. package/test/unit/sync/errors/sync-errors.test.ts +0 -106
  120. package/test/unit/sync/errors/validators.test.ts +0 -61
  121. package/test/unit/sync/helpers/TestModel.ts +0 -44
  122. package/test/unit/sync/helpers/index.ts +0 -172
  123. package/test/unit/sync/manager.test.ts +0 -303
  124. package/test/unit/sync/models/user-award-progress.test.ts +0 -82
  125. package/test/unit/sync/resolver.test.ts +0 -229
  126. package/test/unit/sync/retry.test.ts +0 -314
  127. package/test/unit/sync/run-scope.test.ts +0 -23
  128. package/test/unit/sync/stale-record-cleanup.test.ts +0 -107
  129. package/test/unit/sync/store/push-coalescer.test.ts +0 -156
  130. package/test/unit/sync/store-configs.test.ts +0 -37
  131. package/test/unit/sync/telemetry/sync-telemetry.test.ts +0 -118
  132. package/test/unit/sync/utils/event-emitter.test.ts +0 -64
  133. package/test/unit/sync/utils/throttle.test.ts +0 -245
  134. package/test/unit/url-builder.test.ts +0 -72
  135. package/test/unit/user/account.test.ts +0 -66
  136. package/test/unit/user/permissions.test.js +0 -191
@@ -1,854 +0,0 @@
1
- import { initializeTestDB } from './initializeTestDB'
2
- import { COLLECTION_TYPE, STATE } from '../../src/services/sync/models/ContentProgress'
3
- import { LEARNING_PATH_LESSON } from '../../src/contentTypeConfig'
4
- import db from '../../src/services/sync/repository-proxy'
5
- import { contentStatusCompleted, getProgressState } from '../../src/services/contentProgress.js'
6
-
7
- jest.mock('../../src/infrastructure/http/HttpClient.ts', () => ({
8
- __esModule: true,
9
- GET: jest.fn(),
10
- PUT: jest.fn(),
11
- POST: jest.fn(),
12
- PATCH: jest.fn(),
13
- DELETE: jest.fn(),
14
- HttpClient: jest.fn(),
15
- }))
16
-
17
- jest.mock('../../src/services/sanity.js', () => ({
18
- __esModule: true,
19
- fetchByRailContentId: jest.fn(),
20
- fetchByRailContentIds: jest.fn(),
21
- fetchMethodV2Structure: jest.fn(),
22
- fetchParentChildRelationshipsFor: jest.fn(),
23
- hasAnyMethodV2IntroCompleted: jest.fn(),
24
- devFetchAllLearningPathsAndIntroVideoIdsForDelete: jest.fn(),
25
- getHierarchy: jest.fn((contentId: number) => Promise.resolve({
26
- topLevelId: contentId,
27
- parents: {},
28
- children: {},
29
- metadata: { [contentId]: { brand: 'drumeo', type: 'lesson', parent_id: 0 } },
30
- })),
31
- getHierarchies: jest.fn((contentIds: number[] = []) => Promise.resolve(
32
- Object.fromEntries(contentIds.map(id => [id, {
33
- topLevelId: id,
34
- parents: {},
35
- children: {},
36
- metadata: { [id]: { brand: 'drumeo', type: 'lesson', parent_id: 0 } },
37
- }])),
38
- )),
39
- getSanityDate: jest.fn((date: Date) => date.toISOString()),
40
- }))
41
-
42
- jest.mock('../../src/services/railcontent.js', () => ({
43
- __esModule: true,
44
- fetchLikeCount: jest.fn().mockResolvedValue(0),
45
- fetchUserPermissionsData: jest.fn().mockResolvedValue({ permissions: [], isAdmin: false }),
46
- }))
47
-
48
- jest.mock('../../src/services/awards/award-query.js', () => ({
49
- __esModule: true,
50
- getContentAwardsByIds: jest.fn((ids: number[] = []) => Promise.resolve(
51
- Object.fromEntries(ids.map(id => [id, { awards: [] }])),
52
- )),
53
- }))
54
-
55
- jest.mock('../../src/services/awards/internal/content-progress-observer', () => ({
56
- contentProgressObserver: {
57
- start: jest.fn().mockResolvedValue(undefined),
58
- stop: jest.fn(),
59
- },
60
- }))
61
-
62
- jest.mock('../../src/services/progress-events', () => ({
63
- emitProgressSaved: jest.fn(),
64
- }))
65
-
66
- jest.mock('../../src/services/userActivity', () => ({
67
- trackUserPractice: jest.fn().mockResolvedValue(undefined),
68
- }))
69
-
70
- const HttpClient = require('../../src/infrastructure/http/HttpClient.ts')
71
- const sanity = require('../../src/services/sanity.js')
72
-
73
- const {
74
- mapContentToParent,
75
- isNextLessonLocked,
76
- getDailySession,
77
- updateDailySession,
78
- getActivePath,
79
- startLearningPath,
80
- getEnrichedLearningPath,
81
- getEnrichedLearningPaths,
82
- getLearningPathLessonsByIds,
83
- fetchLearningPathProgressCheckLessons,
84
- fetchLearningPathLessons,
85
- resetAllLearningPaths,
86
- completeMethodIntroVideo,
87
- completeLearningPathIntroVideo,
88
- onLearningPathCompletedActions,
89
- mapLearningPathParentsTo,
90
- mapContentsThatWereLastProgressedFromMethod,
91
- } = require('../../src/services/content-org/learning-paths.ts')
92
-
93
- const ctx = initializeTestDB()
94
-
95
- const lpType = COLLECTION_TYPE.LEARNING_PATH
96
-
97
- type ApiResponses = {
98
- activePath?: any
99
- dailySession?: any
100
- }
101
-
102
- function setApiResponses(r: ApiResponses) {
103
- HttpClient.GET.mockImplementation((url: string) => {
104
- if (url.includes('/active-path/get')) return Promise.resolve(r.activePath ?? null)
105
- if (url.includes('/daily-session')) return Promise.resolve(r.dailySession ?? null)
106
- return Promise.resolve(null)
107
- })
108
- }
109
-
110
- beforeEach(() => {
111
- HttpClient.GET.mockReset()
112
- HttpClient.POST.mockReset()
113
- sanity.fetchByRailContentId.mockReset()
114
- sanity.fetchByRailContentIds.mockReset()
115
- sanity.fetchMethodV2Structure.mockReset()
116
- sanity.fetchParentChildRelationshipsFor.mockReset()
117
- sanity.hasAnyMethodV2IntroCompleted.mockReset()
118
- sanity.devFetchAllLearningPathsAndIntroVideoIdsForDelete.mockReset()
119
-
120
- HttpClient.POST.mockResolvedValue(null)
121
- setApiResponses({
122
- activePath: { active_learning_path_id: 0 },
123
- dailySession: { active_learning_path_id: 0, daily_session: [] },
124
- })
125
- sanity.fetchByRailContentId.mockResolvedValue(false)
126
- sanity.fetchByRailContentIds.mockResolvedValue([])
127
- sanity.fetchMethodV2Structure.mockResolvedValue({ learning_paths: [] })
128
- sanity.fetchParentChildRelationshipsFor.mockResolvedValue([])
129
- sanity.hasAnyMethodV2IntroCompleted.mockResolvedValue(false)
130
- })
131
-
132
- function makeLp(id: number, children: Array<{ id: number; type?: string }> = [], extras: any = {}) {
133
- return {
134
- id,
135
- type: lpType,
136
- brand: 'drumeo',
137
- children: children.map(c => ({ id: c.id, type: c.type ?? 'lesson' })),
138
- ...extras,
139
- }
140
- }
141
-
142
- describe('mapContentToParent', () => {
143
- test('returns null when null', () => {
144
- expect(mapContentToParent(null)).toBeNull()
145
- })
146
-
147
- test('returns empty array unchanged', () => {
148
- expect(mapContentToParent([])).toEqual([])
149
- })
150
-
151
- test('maps single object', () => {
152
- const result = mapContentToParent(
153
- { id: 1, type: 'foo' },
154
- { lessonType: 'bar', parentContentId: 99 },
155
- )
156
- expect(result).toEqual({ id: 1, type: 'bar', parent_id: 99 })
157
- })
158
-
159
- test('maps array', () => {
160
- const result = mapContentToParent(
161
- [{ id: 1 }, { id: 2 }],
162
- { lessonType: 'x', parentContentId: 7 },
163
- )
164
- expect(result).toEqual([
165
- { id: 1, type: 'x', parent_id: 7 },
166
- { id: 2, type: 'x', parent_id: 7 },
167
- ])
168
- })
169
- })
170
-
171
- describe('isNextLessonLocked', () => {
172
- test('false when no dailies', () => {
173
- expect(isNextLessonLocked({} as any)).toBe(false)
174
- })
175
-
176
- test('true when all dailies completed and next lesson needs access', () => {
177
- expect(isNextLessonLocked({
178
- learning_path_dailies: [{ progressStatus: 'completed', need_access: true }],
179
- upcoming_lessons: [{ need_access: true }],
180
- } as any)).toBe(true)
181
- })
182
-
183
- test('false when all dailies completed and next lesson accessible', () => {
184
- expect(isNextLessonLocked({
185
- learning_path_dailies: [{ progressStatus: 'completed', need_access: false }],
186
- upcoming_lessons: [{ need_access: false }],
187
- } as any)).toBe(false)
188
- })
189
-
190
- test('false when all dailies accessible', () => {
191
- expect(isNextLessonLocked({
192
- learning_path_dailies: [
193
- { progressStatus: 'started', need_access: false },
194
- { progressStatus: 'started', need_access: false },
195
- { progressStatus: 'started', need_access: false },
196
- ],
197
- } as any)).toBe(false)
198
- })
199
-
200
- test('false when a locked daily exists but its not next', () => {
201
- expect(isNextLessonLocked({
202
- learning_path_dailies: [
203
- { progressStatus: 'completed', need_access: false },
204
- { progressStatus: 'started', need_access: false },
205
- { progressStatus: 'started', need_access: true },
206
- ],
207
- } as any)).toBe(false)
208
- })
209
-
210
- test('true when all remaining dailies are locked', () => {
211
- expect(isNextLessonLocked({
212
- learning_path_dailies: [
213
- { progressStatus: 'completed', need_access: false },
214
- { progressStatus: 'completed', need_access: false },
215
- { progressStatus: 'started', need_access: true },
216
- ],
217
- } as any)).toBe(true)
218
- })
219
- })
220
-
221
- describe('getDailySession', () => {
222
- test('returns response when present', async () => {
223
- const resp = { active_learning_path_id: 5, daily_session: [] }
224
- setApiResponses({ dailySession: resp })
225
- const result = await getDailySession('drumeo', new Date('2026-01-01T10:00:00Z'), true)
226
- expect(result).toEqual(resp)
227
- expect(HttpClient.GET.mock.calls[0][0]).toContain('/daily-session/get?brand=drumeo')
228
- })
229
-
230
- test('falls back to updateDailySession when empty', async () => {
231
- setApiResponses({ dailySession: '' })
232
- const created = { active_learning_path_id: 9, daily_session: [] }
233
- HttpClient.POST.mockResolvedValueOnce(created)
234
- const result = await getDailySession('pianote', new Date('2026-01-01T10:00:00Z'), true)
235
- expect(result).toEqual(created)
236
- expect(HttpClient.POST).toHaveBeenCalledTimes(1)
237
- })
238
-
239
- test('concurrent calls share single GET (cached promise reuse)', async () => {
240
- const resp = { active_learning_path_id: 5, daily_session: [] }
241
- setApiResponses({ dailySession: resp })
242
- const [a, b] = await Promise.all([
243
- getDailySession('drumeo', new Date('2026-01-01T10:00:00Z')),
244
- getDailySession('drumeo', new Date('2026-01-01T10:00:00Z')),
245
- ])
246
- expect(a).toEqual(resp)
247
- expect(b).toEqual(resp)
248
- expect(HttpClient.GET).toHaveBeenCalledTimes(1)
249
- })
250
-
251
- test('returns null and logs when GET throws', async () => {
252
- const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
253
- HttpClient.GET.mockImplementationOnce(() => Promise.reject(new Error('boom')))
254
- const result = await getDailySession('drumeo', new Date('2026-01-01T10:00:00Z'), true)
255
- expect(result).toBeNull()
256
- expect(errSpy).toHaveBeenCalled()
257
- errSpy.mockRestore()
258
- })
259
- })
260
-
261
- describe('updateDailySession', () => {
262
- test('posts and returns response', async () => {
263
- const resp = { active_learning_path_id: 7, daily_session: [] }
264
- HttpClient.POST.mockResolvedValueOnce(resp)
265
- setApiResponses({ dailySession: resp })
266
- const result = await updateDailySession('drumeo', new Date('2026-01-01T10:00:00Z'), true)
267
- expect(result).toEqual(resp)
268
- const [url, body] = HttpClient.POST.mock.calls[0]
269
- expect(url).toContain('/daily-session/create')
270
- expect(body.brand).toBe('drumeo')
271
- expect(body.keepFirstLearningPath).toBe(true)
272
- })
273
-
274
- test('returns null on empty-string response', async () => {
275
- HttpClient.POST.mockResolvedValueOnce('')
276
- setApiResponses({ dailySession: '' })
277
- const result = await updateDailySession('drumeo', new Date('2026-01-01T10:00:00Z'))
278
- expect(result).toBeNull()
279
- })
280
-
281
- test('returns null on POST error', async () => {
282
- HttpClient.POST.mockRejectedValueOnce(new Error('boom'))
283
- const result = await updateDailySession('drumeo', new Date('2026-01-01T10:00:00Z'))
284
- expect(result).toBeNull()
285
- })
286
-
287
- test('defaults keepFirstLearningPath to false', async () => {
288
- HttpClient.POST.mockResolvedValueOnce({ active_learning_path_id: 1, daily_session: [] })
289
- await updateDailySession('drumeo', new Date('2026-01-01T10:00:00Z'))
290
- expect(HttpClient.POST.mock.calls[0][1].keepFirstLearningPath).toBe(false)
291
- })
292
- })
293
-
294
- describe('getActivePath', () => {
295
- test('returns response', async () => {
296
- const resp = { user_id: 1, brand: 'drumeo', active_learning_path_id: 42 }
297
- setApiResponses({ activePath: resp })
298
- const result = await getActivePath('drumeo', true)
299
- expect(result).toEqual(resp)
300
- expect(HttpClient.GET.mock.calls[0][0]).toContain('/active-path/get?brand=drumeo')
301
- })
302
-
303
- test('concurrent calls share single GET (cached promise reuse)', async () => {
304
- const resp = { user_id: 1, brand: 'drumeo', active_learning_path_id: 7 }
305
- setApiResponses({ activePath: resp })
306
- const [a, b] = await Promise.all([getActivePath('drumeo'), getActivePath('drumeo')])
307
- expect(a).toEqual(resp)
308
- expect(b).toEqual(resp)
309
- expect(HttpClient.GET).toHaveBeenCalledTimes(1)
310
- })
311
- })
312
-
313
- describe('startLearningPath', () => {
314
- test('posts and triggers GET refresh', async () => {
315
- const resp = { user_id: 1, brand: 'drumeo', active_learning_path_id: 11 }
316
- HttpClient.POST.mockResolvedValueOnce(resp)
317
- setApiResponses({ activePath: resp })
318
- const result = await startLearningPath('drumeo', 11)
319
- expect(result).toEqual(resp)
320
- const [url, body] = HttpClient.POST.mock.calls[0]
321
- expect(url).toContain('/active-path/set')
322
- expect(body).toEqual({ brand: 'drumeo', learning_path_id: 11 })
323
- })
324
-
325
- test('does not trigger GET refresh when POST returns falsy', async () => {
326
- HttpClient.POST.mockResolvedValueOnce(null)
327
- await startLearningPath('drumeo', 11)
328
- const getCalls = HttpClient.GET.mock.calls.filter((c: any[]) => c[0].includes('/active-path/get'))
329
- expect(getCalls).toHaveLength(0)
330
- })
331
- })
332
-
333
- describe('resetAllLearningPaths', () => {
334
- test('erases progress in db and posts reset', async () => {
335
- sanity.fetchByRailContentId.mockResolvedValue(makeLp(200))
336
- setApiResponses({ activePath: { active_learning_path_id: 999 } })
337
- await contentStatusCompleted(100)
338
- await contentStatusCompleted(200, { type: lpType, id: 200 })
339
- expect(await getProgressState(100)).toBe('completed')
340
- expect(await getProgressState(200, { type: lpType, id: 200 })).toBe('completed')
341
-
342
- sanity.devFetchAllLearningPathsAndIntroVideoIdsForDelete.mockResolvedValueOnce({
343
- intros: [100],
344
- learning_paths: [200],
345
- })
346
- HttpClient.POST.mockResolvedValueOnce({})
347
-
348
- await resetAllLearningPaths()
349
-
350
- expect(await getProgressState(100)).toBe('')
351
- expect(await getProgressState(200, { type: lpType, id: 200 })).toBe('')
352
- expect(HttpClient.POST.mock.calls.some((c: any[]) => c[0].endsWith('/reset'))).toBe(true)
353
- })
354
- })
355
-
356
- describe('getEnrichedLearningPath', () => {
357
- test('returns null when fetched lp is falsy', async () => {
358
- sanity.fetchByRailContentId.mockResolvedValueOnce(null)
359
- const result = await getEnrichedLearningPath(1)
360
- expect(result).toBeFalsy()
361
- })
362
-
363
- test('maps children to LEARNING_PATH_LESSON with parent_id', async () => {
364
- const lp = makeLp(10, [{ id: 100 }, { id: 101 }])
365
- sanity.fetchByRailContentId.mockResolvedValueOnce(lp)
366
-
367
- const result = await getEnrichedLearningPath(10)
368
- expect(result.children).toHaveLength(2)
369
- expect(result.children[0]).toEqual(expect.objectContaining({
370
- id: 100,
371
- type: LEARNING_PATH_LESSON,
372
- parent_id: 10,
373
- }))
374
- })
375
-
376
- test('reflects real progress state from db', async () => {
377
- const lp = makeLp(20, [{ id: 200 }, { id: 201 }])
378
- sanity.fetchByRailContentId.mockResolvedValueOnce(lp)
379
- await contentStatusCompleted(200, { type: lpType, id: 20 })
380
-
381
- const result = await getEnrichedLearningPath(20)
382
- const lesson200 = result.children.find((c: any) => c.id === 200)
383
- const lesson201 = result.children.find((c: any) => c.id === 201)
384
- expect(lesson200.progressStatus).toBe(STATE.COMPLETED)
385
- expect(lesson201.progressStatus).toBeFalsy()
386
- })
387
-
388
- test('adds awards to LP parent (second addContextToLearningPaths call)', async () => {
389
- const lp = makeLp(30, [{ id: 300 }])
390
- sanity.fetchByRailContentId.mockResolvedValueOnce(lp)
391
- const result = await getEnrichedLearningPath(30)
392
- expect(result.awards).toEqual([])
393
- })
394
- })
395
-
396
- describe('getEnrichedLearningPaths', () => {
397
- test('maps each lp children', async () => {
398
- const paths = [makeLp(1, [{ id: 10 }]), makeLp(2, [{ id: 20 }])]
399
- sanity.fetchByRailContentIds.mockResolvedValueOnce(paths)
400
- const result = await getEnrichedLearningPaths([1, 2])
401
- expect(result[0].children[0].parent_id).toBe(1)
402
- expect(result[1].children[0].parent_id).toBe(2)
403
- })
404
-
405
- test('returns empty array when fetched empty', async () => {
406
- sanity.fetchByRailContentIds.mockResolvedValueOnce([])
407
- const result = await getEnrichedLearningPaths([])
408
- expect(result).toEqual([])
409
- })
410
- })
411
-
412
- describe('getLearningPathLessonsByIds', () => {
413
- test('filters lessons by ids', async () => {
414
- const lp = makeLp(1, [{ id: 1 }, { id: 2 }, { id: 3 }])
415
- sanity.fetchByRailContentId.mockResolvedValueOnce(lp)
416
- const result = await getLearningPathLessonsByIds([1, 3], 1)
417
- expect(result.map((l: any) => l.id)).toEqual([1, 3])
418
- })
419
- })
420
-
421
- describe('fetchLearningPathProgressCheckLessons', () => {
422
- test('returns ids that have completed progress in db', async () => {
423
- await contentStatusCompleted(1)
424
- await contentStatusCompleted(3)
425
- const result = await fetchLearningPathProgressCheckLessons([1, 2, 3])
426
- expect(result.sort()).toEqual([1, 3])
427
- })
428
-
429
- test('returns [] when none completed', async () => {
430
- const result = await fetchLearningPathProgressCheckLessons([1, 2])
431
- expect(result).toEqual([])
432
- })
433
- })
434
-
435
- describe('fetchLearningPathLessons', () => {
436
- test('returns null when learning path has no children', async () => {
437
- sanity.fetchByRailContentId.mockResolvedValueOnce(makeLp(1, []))
438
- const result = await fetchLearningPathLessons(1, 'drumeo', new Date())
439
- expect(result).toBeNull()
440
- })
441
-
442
- test('returns is_active_learning_path false when not active', async () => {
443
- sanity.fetchByRailContentId.mockResolvedValueOnce(makeLp(1, [{ id: 100 }]))
444
- setApiResponses({ dailySession: { active_learning_path_id: 999, daily_session: [] } })
445
- const result = await fetchLearningPathLessons(1, 'drumeo', new Date())
446
- expect(result.is_active_learning_path).toBe(false)
447
- })
448
-
449
- test('categorizes dailies/completed/upcoming when active', async () => {
450
- const lp = makeLp(5, [{ id: 100 }, { id: 101 }, { id: 102 }])
451
- sanity.fetchByRailContentId.mockResolvedValue(lp)
452
- await contentStatusCompleted(101, { type: lpType, id: 5 })
453
- setApiResponses({
454
- dailySession: {
455
- active_learning_path_id: 5,
456
- active_learning_path_created_at: '2026-01-01',
457
- daily_session: [{ learning_path_id: 5, content_ids: [100] }],
458
- },
459
- })
460
-
461
- const result = await fetchLearningPathLessons(5, 'drumeo', new Date())
462
- expect(result.is_active_learning_path).toBe(true)
463
- expect(result.learning_path_dailies.map((l: any) => l.id)).toEqual([100])
464
- expect(result.completed_lessons.map((l: any) => l.id)).toEqual([101])
465
- expect(result.upcoming_lessons.map((l: any) => l.id)).toEqual([102])
466
- })
467
-
468
- test('returns null when learningPath fetch resolves falsy', async () => {
469
- sanity.fetchByRailContentId.mockResolvedValueOnce(null)
470
- const result = await fetchLearningPathLessons(1, 'drumeo', new Date())
471
- expect(result).toBeNull()
472
- })
473
-
474
- test('resolves previous and next learning path dailies', async () => {
475
- const lpCurrent = makeLp(5, [{ id: 100 }])
476
- const lpPrev = makeLp(4, [{ id: 40 }])
477
- const lpNext = makeLp(6, [{ id: 60 }])
478
- sanity.fetchByRailContentId.mockImplementation((id: number) => {
479
- if (id === 5) return Promise.resolve(lpCurrent)
480
- if (id === 4) return Promise.resolve(lpPrev)
481
- if (id === 6) return Promise.resolve(lpNext)
482
- return Promise.resolve(false)
483
- })
484
- setApiResponses({
485
- dailySession: {
486
- active_learning_path_id: 5,
487
- daily_session: [
488
- { learning_path_id: 4, content_ids: [40] },
489
- { learning_path_id: 5, content_ids: [100] },
490
- { learning_path_id: 6, content_ids: [60] },
491
- ],
492
- },
493
- })
494
-
495
- const result = await fetchLearningPathLessons(5, 'drumeo', new Date())
496
- expect(result.previous_learning_path_id).toBe(4)
497
- expect(result.previous_learning_path_dailies.map((l: any) => l.id)).toEqual([40])
498
- expect(result.next_learning_path_id).toBe(6)
499
- expect(result.next_learning_path_dailies.map((l: any) => l.id)).toEqual([60])
500
- expect(result.next_learning_path_dailies[0].in_next_learning_path).toBe(false)
501
- })
502
-
503
- test('in_next_learning_path true when current LP is completed', async () => {
504
- const lpCurrent = makeLp(5, [{ id: 100 }])
505
- const lpNext = makeLp(6, [{ id: 60 }])
506
- sanity.fetchByRailContentId.mockImplementation((id: number) => {
507
- if (id === 5) return Promise.resolve(lpCurrent)
508
- if (id === 6) return Promise.resolve(lpNext)
509
- return Promise.resolve(false)
510
- })
511
- setApiResponses({ activePath: { active_learning_path_id: 999 } })
512
- await contentStatusCompleted(5, { type: lpType, id: 5 })
513
- await contentStatusCompleted(100, { type: lpType, id: 5 })
514
- setApiResponses({
515
- dailySession: {
516
- active_learning_path_id: 5,
517
- daily_session: [
518
- { learning_path_id: 5, content_ids: [100] },
519
- { learning_path_id: 6, content_ids: [60] },
520
- ],
521
- },
522
- })
523
-
524
- const result = await fetchLearningPathLessons(5, 'drumeo', new Date())
525
- expect(result.progressStatus).toBe(STATE.COMPLETED)
526
- expect(result.next_learning_path_dailies[0].in_next_learning_path).toBe(true)
527
- })
528
-
529
- test('treats missing session.content_ids as empty array', async () => {
530
- const lp = makeLp(5, [{ id: 100 }])
531
- sanity.fetchByRailContentId.mockResolvedValueOnce(lp)
532
- setApiResponses({
533
- dailySession: {
534
- active_learning_path_id: 5,
535
- daily_session: [{ learning_path_id: 5 }],
536
- },
537
- })
538
- const result = await fetchLearningPathLessons(5, 'drumeo', new Date())
539
- expect(result.learning_path_dailies).toEqual([])
540
- expect(result.upcoming_lessons.map((l: any) => l.id)).toEqual([100])
541
- })
542
- })
543
-
544
- describe('completeMethodIntroVideo', () => {
545
- test('completes intro video in db and posts active-path actions', async () => {
546
- sanity.fetchMethodV2Structure.mockResolvedValueOnce({
547
- learning_paths: [{ id: 50 }, { id: 51 }],
548
- })
549
- HttpClient.POST.mockResolvedValueOnce({ active_learning_path_id: 50 })
550
-
551
- const result = await completeMethodIntroVideo(700, 'drumeo')
552
-
553
- expect(result.intro_video_response).toBeTruthy()
554
- expect(await getProgressState(700)).toBe('completed')
555
- expect(result.active_path_response).toEqual({ active_learning_path_id: 50 })
556
- const methodCall = HttpClient.POST.mock.calls.find(
557
- (c: any[]) => c[0].includes('/method-intro-video-complete-actions'),
558
- )
559
- expect(methodCall).toBeDefined()
560
- expect(methodCall[1].brand).toBe('drumeo')
561
- expect(methodCall[1].learningPathId).toBe(50)
562
- expect(methodCall[1].userDate).toMatch(/^\d{4}-\d{2}-\d{2}/)
563
- })
564
-
565
- test('skips intro completion when already completed', async () => {
566
- await contentStatusCompleted(701)
567
- sanity.fetchMethodV2Structure.mockResolvedValueOnce({
568
- learning_paths: [{ id: 50 }],
569
- })
570
- HttpClient.POST.mockResolvedValueOnce({ active_learning_path_id: 50 })
571
-
572
- const result = await completeMethodIntroVideo(701, 'drumeo')
573
- expect(result.intro_video_response).toBeNull()
574
- })
575
-
576
- test('null intro video id skips completion', async () => {
577
- sanity.fetchMethodV2Structure.mockResolvedValueOnce({
578
- learning_paths: [{ id: 50 }],
579
- })
580
- HttpClient.POST.mockResolvedValueOnce({ active_learning_path_id: 50 })
581
- const result = await completeMethodIntroVideo(null, 'drumeo')
582
- expect(result.intro_video_response).toBeNull()
583
- })
584
- })
585
-
586
- describe('completeLearningPathIntroVideo', () => {
587
- test('resets LP progress when no lessons to import', async () => {
588
- const collection = { type: lpType, id: 10 }
589
- sanity.fetchByRailContentId.mockResolvedValue(makeLp(10))
590
- setApiResponses({ activePath: { active_learning_path_id: 999 } })
591
- await contentStatusCompleted(10, collection)
592
-
593
- sanity.hasAnyMethodV2IntroCompleted.mockResolvedValueOnce(true)
594
- setApiResponses({ activePath: { active_learning_path_id: 10 } })
595
-
596
- const result = await completeLearningPathIntroVideo(800, 10, null, 'drumeo')
597
-
598
- expect(result.learning_path_reset_response).toBeTruthy()
599
- expect(await getProgressState(10, collection)).toBe('')
600
- expect(result.intro_video_response).toBeTruthy()
601
- expect(await getProgressState(800)).toBe('completed')
602
- })
603
-
604
- test('imports lessons and updates dailies when active', async () => {
605
- sanity.hasAnyMethodV2IntroCompleted.mockResolvedValueOnce(true)
606
- setApiResponses({
607
- activePath: { active_learning_path_id: 10 },
608
- dailySession: { active_learning_path_id: 10, daily_session: [] },
609
- })
610
- HttpClient.POST.mockResolvedValueOnce({ active_learning_path_id: 10, daily_session: [] })
611
-
612
- const result = await completeLearningPathIntroVideo(801, 10, [301, 302], 'drumeo')
613
-
614
- expect(result.lesson_import_response).toBeTruthy()
615
- expect(await getProgressState(301, { type: lpType, id: 10 })).toBe('completed')
616
- expect(await getProgressState(302, { type: lpType, id: 10 })).toBe('completed')
617
- expect(result.update_dailies_response).toBeTruthy()
618
- expect(await getProgressState(801)).toBe('completed')
619
- })
620
-
621
- test('lateMethodSetup: triggers completeMethodIntroVideo when anyIntroComplete and no activePath', async () => {
622
- sanity.hasAnyMethodV2IntroCompleted.mockResolvedValueOnce(true)
623
- setApiResponses({ activePath: null })
624
- sanity.fetchMethodV2Structure.mockResolvedValue({ learning_paths: [{ id: 10 }] })
625
- HttpClient.POST.mockResolvedValue({ active_learning_path_id: 10 })
626
-
627
- await completeLearningPathIntroVideo(802, 10, null, 'drumeo')
628
-
629
- const methodCalls = HttpClient.POST.mock.calls.filter(
630
- (c: any[]) => c[0].includes('/method-intro-video-complete-actions'),
631
- )
632
- expect(methodCalls.length).toBeGreaterThanOrEqual(1)
633
- })
634
-
635
- test('skips late method setup when anyIntroComplete is false', async () => {
636
- sanity.hasAnyMethodV2IntroCompleted.mockResolvedValueOnce(false)
637
- setApiResponses({ activePath: null })
638
-
639
- await completeLearningPathIntroVideo(803, 10, null, 'drumeo')
640
-
641
- const methodCalls = HttpClient.POST.mock.calls.filter(
642
- (c: any[]) => c[0].includes('/method-intro-video-complete-actions'),
643
- )
644
- expect(methodCalls).toHaveLength(0)
645
- })
646
-
647
- test('imports lessons but does not update dailies when LP is not active', async () => {
648
- sanity.hasAnyMethodV2IntroCompleted.mockResolvedValueOnce(true)
649
- setApiResponses({ activePath: { active_learning_path_id: 999 } })
650
-
651
- const result = await completeLearningPathIntroVideo(804, 10, [304], 'drumeo')
652
-
653
- expect(result.lesson_import_response).toBeTruthy()
654
- expect(result.update_dailies_response).toBeUndefined()
655
- const dailyPosts = HttpClient.POST.mock.calls.filter(
656
- (c: any[]) => c[0].includes('/daily-session/create'),
657
- )
658
- expect(dailyPosts).toHaveLength(0)
659
- })
660
-
661
- test('skips updateDailySession when lateMethodSetup already set dailies', async () => {
662
- sanity.hasAnyMethodV2IntroCompleted.mockResolvedValueOnce(true)
663
- setApiResponses({ activePath: null })
664
- sanity.fetchMethodV2Structure.mockResolvedValue({ learning_paths: [{ id: 10 }] })
665
- HttpClient.POST.mockImplementation((url: string) => {
666
- if (url.includes('/method-intro-video-complete-actions')) {
667
- setApiResponses({ activePath: { active_learning_path_id: 10 } })
668
- }
669
- return Promise.resolve({ active_learning_path_id: 10, daily_session: [] })
670
- })
671
-
672
- const result = await completeLearningPathIntroVideo(805, 10, [305], 'drumeo')
673
-
674
- expect(result.update_dailies_response).toBeUndefined()
675
- const dailyPosts = HttpClient.POST.mock.calls.filter(
676
- (c: any[]) => c[0].includes('/daily-session/create'),
677
- )
678
- expect(dailyPosts).toHaveLength(0)
679
- })
680
-
681
- test('skips intro video completion when already completed', async () => {
682
- await contentStatusCompleted(806)
683
- sanity.hasAnyMethodV2IntroCompleted.mockResolvedValueOnce(true)
684
- setApiResponses({ activePath: { active_learning_path_id: 10 } })
685
-
686
- const result = await completeLearningPathIntroVideo(806, 10, null, 'drumeo')
687
- expect(result.intro_video_response).toBeNull()
688
- })
689
- })
690
-
691
- describe('onLearningPathCompletedActions', () => {
692
- test('returns early when not the active path', async () => {
693
- sanity.fetchByRailContentId.mockResolvedValue(makeLp(5))
694
- setApiResponses({ activePath: { active_learning_path_id: 99 } })
695
- await onLearningPathCompletedActions(5)
696
- const setPathCalls = HttpClient.POST.mock.calls.filter((c: any[]) => c[0].includes('/active-path/set'))
697
- expect(setPathCalls).toHaveLength(0)
698
- })
699
-
700
- test('starts next published learning path and resets its intro video', async () => {
701
- const lp = makeLp(5)
702
- const nextLp = { ...makeLp(6), intro_video: { id: 600 } }
703
- sanity.fetchByRailContentId
704
- .mockResolvedValueOnce(lp)
705
- .mockResolvedValueOnce(nextLp)
706
- setApiResponses({ activePath: { active_learning_path_id: 5 } })
707
- sanity.fetchMethodV2Structure.mockResolvedValueOnce({
708
- learning_paths: [
709
- { id: 5, published_on: '2025-01-01' },
710
- { id: 6, published_on: '2025-01-02' },
711
- { id: 7, published_on: null },
712
- ],
713
- })
714
- HttpClient.POST.mockResolvedValueOnce({ active_learning_path_id: 6 })
715
- await contentStatusCompleted(600)
716
-
717
- await onLearningPathCompletedActions(5)
718
-
719
- const setPathCalls = HttpClient.POST.mock.calls.filter((c: any[]) => c[0].includes('/active-path/set'))
720
- expect(setPathCalls).toHaveLength(1)
721
- expect(setPathCalls[0][1]).toEqual({ brand: 'drumeo', learning_path_id: 6 })
722
- expect(await getProgressState(600)).toBe('')
723
- })
724
-
725
- test('returns when no next learning path exists', async () => {
726
- sanity.fetchByRailContentId.mockResolvedValueOnce(makeLp(9))
727
- setApiResponses({ activePath: { active_learning_path_id: 9 } })
728
- sanity.fetchMethodV2Structure.mockResolvedValueOnce({
729
- learning_paths: [{ id: 9, published_on: '2025-01-01' }],
730
- })
731
- await onLearningPathCompletedActions(9)
732
- const setPathCalls = HttpClient.POST.mock.calls.filter((c: any[]) => c[0].includes('/active-path/set'))
733
- expect(setPathCalls).toHaveLength(0)
734
- })
735
-
736
- test('returns when active LP is not in published list', async () => {
737
- sanity.fetchByRailContentId.mockResolvedValueOnce(makeLp(8))
738
- setApiResponses({ activePath: { active_learning_path_id: 8 } })
739
- sanity.fetchMethodV2Structure.mockResolvedValueOnce({
740
- learning_paths: [
741
- { id: 8, published_on: null },
742
- { id: 9, published_on: '2025-01-01' },
743
- ],
744
- })
745
- await onLearningPathCompletedActions(8)
746
- const setPathCalls = HttpClient.POST.mock.calls.filter((c: any[]) => c[0].includes('/active-path/set'))
747
- expect(setPathCalls).toHaveLength(0)
748
- })
749
-
750
- test('skips next LP that is not yet published', async () => {
751
- sanity.fetchByRailContentId.mockResolvedValueOnce(makeLp(5))
752
- setApiResponses({ activePath: { active_learning_path_id: 5 } })
753
- sanity.fetchMethodV2Structure.mockResolvedValueOnce({
754
- learning_paths: [
755
- { id: 5, published_on: '2025-01-01' },
756
- { id: 6, published_on: '2099-01-01' },
757
- ],
758
- })
759
- await onLearningPathCompletedActions(5)
760
- const setPathCalls = HttpClient.POST.mock.calls.filter((c: any[]) => c[0].includes('/active-path/set'))
761
- expect(setPathCalls).toHaveLength(0)
762
- })
763
- })
764
-
765
- describe('mapLearningPathParentsTo', () => {
766
- test('maps parent_id from hierarchy', async () => {
767
- sanity.fetchParentChildRelationshipsFor.mockResolvedValueOnce([
768
- { railcontent_id: '100', children: [1, 2] },
769
- { railcontent_id: '200', children: [3] },
770
- ])
771
- const result = await mapLearningPathParentsTo(
772
- [{ id: 1 }, { id: 2 }, { id: 3 }],
773
- { type: true, parent_id: true },
774
- )
775
- expect(result).toEqual([
776
- { id: 1, type: LEARNING_PATH_LESSON, parent_id: 100 },
777
- { id: 2, type: LEARNING_PATH_LESSON, parent_id: 100 },
778
- { id: 3, type: LEARNING_PATH_LESSON, parent_id: 200 },
779
- ])
780
- })
781
-
782
- test('id without parent in hierarchy gets parent_id undefined', async () => {
783
- sanity.fetchParentChildRelationshipsFor.mockResolvedValueOnce([
784
- { railcontent_id: '100', children: [1] },
785
- ])
786
- const result = await mapLearningPathParentsTo(
787
- [{ id: 1 }, { id: 99 }],
788
- { type: true, parent_id: true },
789
- )
790
- expect(result[0].parent_id).toBe(100)
791
- expect(result[1].parent_id).toBeUndefined()
792
- })
793
-
794
- test('only maps type when parent_id flag is false', async () => {
795
- sanity.fetchParentChildRelationshipsFor.mockResolvedValueOnce([
796
- { railcontent_id: '100', children: [1] },
797
- ])
798
- const result = await mapLearningPathParentsTo(
799
- [{ id: 1, type: 'orig' }],
800
- { type: true },
801
- )
802
- expect(result[0]).toEqual({ id: 1, type: LEARNING_PATH_LESSON })
803
- })
804
-
805
- test('only maps parent_id when type flag is false', async () => {
806
- sanity.fetchParentChildRelationshipsFor.mockResolvedValueOnce([
807
- { railcontent_id: '100', children: [1] },
808
- ])
809
- const result = await mapLearningPathParentsTo(
810
- [{ id: 1, type: 'orig' }],
811
- { parent_id: true },
812
- )
813
- expect(result[0]).toEqual({ id: 1, type: 'orig', parent_id: 100 })
814
- })
815
- })
816
-
817
- describe('mapContentsThatWereLastProgressedFromMethod', () => {
818
- test('returns input when empty', async () => {
819
- expect(await mapContentsThatWereLastProgressedFromMethod([])).toEqual([])
820
- expect(await mapContentsThatWereLastProgressedFromMethod(null)).toBeNull()
821
- })
822
-
823
- test('returns input unchanged when no eligible types', async () => {
824
- const input = [{ id: 1, type: 'song' }]
825
- expect(await mapContentsThatWereLastProgressedFromMethod(input)).toEqual(input)
826
- })
827
-
828
- test('maps eligible ids with parent data when last accessed from method', async () => {
829
- const collection = { type: lpType, id: 50 }
830
- sanity.fetchByRailContentId.mockResolvedValue(makeLp(50))
831
- setApiResponses({ activePath: { active_learning_path_id: 999 } })
832
- await contentStatusCompleted(1, collection)
833
- sanity.fetchParentChildRelationshipsFor.mockResolvedValueOnce([
834
- { railcontent_id: '50', children: [1] },
835
- ])
836
-
837
- const input = [
838
- { id: 1, type: 'skill-pack-lesson' },
839
- { id: 2, type: 'song' },
840
- ]
841
- const result = await mapContentsThatWereLastProgressedFromMethod(input)
842
- expect(result[0]).toEqual({ id: 1, type: LEARNING_PATH_LESSON, parent_id: 50 })
843
- expect(result[1]).toEqual({ id: 2, type: 'song' })
844
- })
845
-
846
- test('returns objects unchanged when none were last accessed from method', async () => {
847
- const input = [
848
- { id: 1, type: 'skill-pack-lesson' },
849
- { id: 2, type: 'song-tutorial-lesson' },
850
- ]
851
- const result = await mapContentsThatWereLastProgressedFromMethod(input)
852
- expect(result).toEqual(input)
853
- })
854
- })