musora-content-services 2.167.2 → 2.167.4

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 (140) hide show
  1. package/.agent/decisions/2026-07-20-mu2-1724-songs-lessons-counts.md +33 -0
  2. package/.agent/decisions/2026-07-20-query-builder-tostring.md +37 -0
  3. package/.idea/codeStyles/Project.xml +61 -0
  4. package/.idea/codeStyles/codeStyleConfig.xml +5 -0
  5. package/.idea/modules.xml +8 -0
  6. package/.idea/musora-content-services.iml +8 -0
  7. package/.idea/php.xml +19 -0
  8. package/.idea/prettier.xml +6 -0
  9. package/.idea/vcs.xml +7 -0
  10. package/.yarn/install-state.gz +0 -0
  11. package/CHANGELOG.md +10 -0
  12. package/package.json +2 -1
  13. package/src/contentTypeConfig.js +1 -0
  14. package/src/lib/sanity/query.ts +5 -0
  15. package/src/services/config.js +15 -0
  16. package/src/services/content/artist.ts +1 -3
  17. package/src/services/content/counts.ts +47 -0
  18. package/src/services/content/genre.ts +1 -3
  19. package/src/services/content/instructor.ts +1 -3
  20. package/src/services/multi-user-accounts/multi-user-accounts.ts +1 -0
  21. package/src/services/userActivity.js +2 -0
  22. package/src/version-info.js +1 -0
  23. package/.claude/settings.local.json +0 -53
  24. package/test/initializeTests.js +0 -60
  25. package/test/integration/awards/award-exclusion-handling.test.ts +0 -275
  26. package/test/integration/awards/internal/image-utils.test.ts +0 -86
  27. package/test/integration/content-progress/__mocks__/mocks.ts +0 -104
  28. package/test/integration/content-progress/contentProgress.test.ts +0 -335
  29. package/test/integration/content-progress/e2eOfflineProgress.test.ts +0 -352
  30. package/test/integration/content-progress/e2eProgress.test.ts +0 -612
  31. package/test/integration/content-progress/getters.test.ts +0 -334
  32. package/test/integration/content-progress/helpers.test.ts +0 -263
  33. package/test/integration/content-progress/offlineContentProgress.test.ts +0 -226
  34. package/test/integration/forums.test.ts +0 -209
  35. package/test/integration/infrastructure/FetchRequestExecutor.test.ts +0 -88
  36. package/test/integration/initializeTestDB.ts +0 -80
  37. package/test/integration/learning-paths.test.ts +0 -854
  38. package/test/integration/notifications.test.ts +0 -178
  39. package/test/integration/progress-row/content-card.test.ts +0 -283
  40. package/test/integration/sync/adapters/idb-errors.test.ts +0 -144
  41. package/test/integration/sync/adapters/sqlite-errors.test.ts +0 -173
  42. package/test/integration/sync/fetch.test.ts +0 -224
  43. package/test/integration/sync/repositories/content-likes.test.ts +0 -99
  44. package/test/integration/sync/repositories/practices.test.ts +0 -179
  45. package/test/integration/sync/repositories/progress.test.ts +0 -245
  46. package/test/integration/sync/repositories/user-award-progress.static.test.ts +0 -68
  47. package/test/integration/sync/repositories/user-award-progress.test.ts +0 -387
  48. package/test/integration/sync/store/cross-user-protection.test.ts +0 -217
  49. package/test/integration/sync/store/store-idb.test.ts +0 -180
  50. package/test/integration/sync/store/store.test.ts +0 -361
  51. package/test/integration/user/permissions.test.js +0 -20
  52. package/test/integration/userActivity.test.ts +0 -102
  53. package/test/live/README.md +0 -29
  54. package/test/live/content.test.js +0 -116
  55. package/test/localStorageMock.js +0 -21
  56. package/test/log.js +0 -5
  57. package/test/mockData/award-definitions.js +0 -171
  58. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -35
  59. package/test/mockData/mockData_progress_content.json +0 -182
  60. package/test/mockData/mockData_sanity_progress_content.json +0 -1451
  61. package/test/mockData/mockData_user_practices.json +0 -9
  62. package/test/setupConsole.js +0 -6
  63. package/test/setupNetworkGuard.js +0 -3
  64. package/test/setupTimers.js +0 -13
  65. package/test/unit/HttpClient.test.ts +0 -267
  66. package/test/unit/awards/award-alacarte-observer.test.ts +0 -199
  67. package/test/unit/awards/award-auto-refresh.test.ts +0 -84
  68. package/test/unit/awards/award-calculations.test.ts +0 -34
  69. package/test/unit/awards/award-callbacks.test.ts +0 -144
  70. package/test/unit/awards/award-certificate-display.test.ts +0 -330
  71. package/test/unit/awards/award-collection-edge-cases.test.ts +0 -212
  72. package/test/unit/awards/award-collection-filtering.test.ts +0 -287
  73. package/test/unit/awards/award-completion-flow.test.ts +0 -306
  74. package/test/unit/awards/award-multi-lesson.test.ts +0 -243
  75. package/test/unit/awards/award-observer-integration.test.ts +0 -327
  76. package/test/unit/awards/award-query-messages.test.ts +0 -448
  77. package/test/unit/awards/award-user-collection.test.ts +0 -416
  78. package/test/unit/awards/duplicate-prevention.test.ts +0 -120
  79. package/test/unit/awards/helpers/completion-mock.ts +0 -56
  80. package/test/unit/awards/helpers/index.ts +0 -3
  81. package/test/unit/awards/helpers/mock-setup.ts +0 -79
  82. package/test/unit/awards/helpers/progress-emitter.ts +0 -45
  83. package/test/unit/awards/message-generator.test.ts +0 -162
  84. package/test/unit/content-progress/bubbleTrickle.test.ts +0 -322
  85. package/test/unit/content-progress/helpers.test.ts +0 -329
  86. package/test/unit/content-progress/navigateTo.test.ts +0 -392
  87. package/test/unit/contentLikes.test.ts +0 -62
  88. package/test/unit/contentMetaData.test.ts +0 -58
  89. package/test/unit/dataContext.test.ts +0 -38
  90. package/test/unit/dateUtils.test.ts +0 -188
  91. package/test/unit/endScreen.test.js +0 -712
  92. package/test/unit/eventsAPI.test.ts +0 -159
  93. package/test/unit/imageSRCBuilder.test.ts +0 -37
  94. package/test/unit/imageSRCVerify.test.ts +0 -155
  95. package/test/unit/infrastructure/DefaultHeaderProvider.test.ts +0 -39
  96. package/test/unit/infrastructure/sanity/ContentClient.test.ts +0 -168
  97. package/test/unit/infrastructure/sanity/DefaultConfigProvider.test.ts +0 -93
  98. package/test/unit/infrastructure/sanity/FetchQueryExecutor.test.ts +0 -174
  99. package/test/unit/infrastructure/sanity/SanityClient.test.ts +0 -140
  100. package/test/unit/lib/__snapshots__/filter.test.ts.snap +0 -5
  101. package/test/unit/lib/filter.test.ts +0 -1154
  102. package/test/unit/lib/lastUpdated.test.ts +0 -21
  103. package/test/unit/lib/query.test.ts +0 -1469
  104. package/test/unit/lib/sanity/decorators/base.test.ts +0 -368
  105. package/test/unit/lib/sanity/decorators/navigate-to.test.ts +0 -361
  106. package/test/unit/lib/sanity/decorators/need-access.test.ts +0 -89
  107. package/test/unit/lib/sanity/decorators/need-lifetime-upgrade.test.ts +0 -85
  108. package/test/unit/lib/sanity/decorators/page-type.test.ts +0 -81
  109. package/test/unit/playerSettings.test.ts +0 -73
  110. package/test/unit/progress-row/content-card.test.ts +0 -314
  111. package/test/unit/progress-row/playlist-card.test.ts +0 -104
  112. package/test/unit/progressRows.test.ts +0 -243
  113. package/test/unit/recommendations.test.js +0 -47
  114. package/test/unit/sanityQueryService.test.ts +0 -200
  115. package/test/unit/search.test.ts +0 -44
  116. package/test/unit/sentry.test.ts +0 -62
  117. package/test/unit/services/progress-internal/bubble.test.ts +0 -279
  118. package/test/unit/services/progress.test.ts +0 -396
  119. package/test/unit/streakMessage.test.ts +0 -251
  120. package/test/unit/sync/context.test.ts +0 -51
  121. package/test/unit/sync/effects/logout-warning.test.ts +0 -158
  122. package/test/unit/sync/effects/push-failure-notification.test.ts +0 -196
  123. package/test/unit/sync/errors/sync-errors.test.ts +0 -106
  124. package/test/unit/sync/errors/validators.test.ts +0 -61
  125. package/test/unit/sync/helpers/TestModel.ts +0 -44
  126. package/test/unit/sync/helpers/index.ts +0 -172
  127. package/test/unit/sync/manager.test.ts +0 -303
  128. package/test/unit/sync/models/user-award-progress.test.ts +0 -82
  129. package/test/unit/sync/resolver.test.ts +0 -229
  130. package/test/unit/sync/retry.test.ts +0 -314
  131. package/test/unit/sync/run-scope.test.ts +0 -23
  132. package/test/unit/sync/stale-record-cleanup.test.ts +0 -107
  133. package/test/unit/sync/store/push-coalescer.test.ts +0 -156
  134. package/test/unit/sync/store-configs.test.ts +0 -37
  135. package/test/unit/sync/telemetry/sync-telemetry.test.ts +0 -118
  136. package/test/unit/sync/utils/event-emitter.test.ts +0 -64
  137. package/test/unit/sync/utils/throttle.test.ts +0 -245
  138. package/test/unit/url-builder.test.ts +0 -72
  139. package/test/unit/user/account.test.ts +0 -66
  140. 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
- })