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,178 +0,0 @@
1
- import { initializeTestService } from '../initializeTests.js'
2
- import * as UserNotifications from "../../src/services/user/notifications.js";
3
-
4
- jest.mock('../../src/infrastructure/http/HttpClient.ts', () => ({
5
- GET: jest.fn(),
6
- POST: jest.fn(),
7
- PUT: jest.fn(),
8
- PATCH: jest.fn(),
9
- DELETE: jest.fn(),
10
- HttpClient: jest.fn(),
11
- }))
12
-
13
- jest.mock('../../src/services/railcontent.js', () => ({
14
- fetchUserPermissionsData: jest.fn(() => ({ permissions: [78, 91, 92], isAdmin: false })),
15
- fetchHandler: jest.fn(),
16
- }))
17
-
18
- jest.mock('../../src/services/eventsAPI.js', () => ({
19
- __esModule: true,
20
- default: {
21
- pauseLiveEventCheck: jest.fn().mockResolvedValue(undefined),
22
- }
23
- }))
24
-
25
- import { GET, PUT, DELETE } from '../../src/infrastructure/http/HttpClient'
26
-
27
- const baseUrl = `/api/notifications`
28
-
29
- describe('UserNotifications module', function () {
30
- beforeEach(() => {
31
- initializeTestService()
32
- })
33
-
34
- describe('fetchNotifications', () => {
35
- it('calls GET with correct url', async () => {
36
- GET.mockResolvedValueOnce([{ id: 1 }])
37
-
38
- const result = await UserNotifications.fetchNotifications({
39
- limit: 5,
40
- onlyUnread: true,
41
- page: 2,
42
- })
43
-
44
- expect(GET).toHaveBeenCalledWith(`${baseUrl}/v1?limit=5&page=2&unread=1`)
45
- expect(result).toEqual([{ id: 1 }])
46
- })
47
- })
48
-
49
- describe('markNotificationAsRead', () => {
50
- it('throws if notificationId not provided', async () => {
51
- await expect(UserNotifications.markNotificationAsRead()).rejects.toThrow('notificationId is required')
52
- })
53
-
54
- it('calls PUT with correct url', async () => {
55
- PUT.mockResolvedValueOnce({ success: true })
56
-
57
- const result = await UserNotifications.markNotificationAsRead(123)
58
- expect(PUT).toHaveBeenCalledWith(`${baseUrl}/v1/read?id=123`, null)
59
- expect(result).toEqual({ success: true })
60
- })
61
- })
62
-
63
- describe('markAllNotificationsAsRead', () => {
64
- it('calls PUT with correct url', async () => {
65
- PUT.mockResolvedValueOnce({ success: true })
66
-
67
- const result = await UserNotifications.markAllNotificationsAsRead()
68
- expect(PUT).toHaveBeenCalledWith(`${baseUrl}/v1/read`, null)
69
- expect(result).toEqual({ success: true })
70
- })
71
- })
72
-
73
- describe('markNotificationAsUnread', () => {
74
- it('throws if notificationId not provided', async () => {
75
- await expect(UserNotifications.markNotificationAsUnread()).rejects.toThrow('notificationId is required')
76
- })
77
-
78
- it('calls PUT with correct url', async () => {
79
- PUT.mockResolvedValueOnce({ success: true })
80
-
81
- const result = await UserNotifications.markNotificationAsUnread(456)
82
- expect(PUT).toHaveBeenCalledWith(`${baseUrl}/v1/unread?id=456`, null)
83
- expect(result).toEqual({ success: true })
84
- })
85
- })
86
-
87
- describe('deleteNotification', () => {
88
- it('throws if notificationId not provided', async () => {
89
- await expect(UserNotifications.deleteNotification()).rejects.toThrow('notificationId is required')
90
- })
91
-
92
- it('calls DELETE with correct url', async () => {
93
- DELETE.mockResolvedValueOnce({ success: true })
94
-
95
- const result = await UserNotifications.deleteNotification(789)
96
- expect(DELETE).toHaveBeenCalledWith(`${baseUrl}/v1/789`)
97
- expect(result).toEqual({ success: true })
98
- })
99
- })
100
-
101
- describe('fetchUnreadCount', () => {
102
- it('returns unread count when data is greater than 0', async () => {
103
- GET.mockResolvedValueOnce({ data: 42 })
104
-
105
- const result = await UserNotifications.fetchUnreadCount()
106
- expect(GET).toHaveBeenCalledWith(`${baseUrl}/v1/unread-count`)
107
- expect(result).toEqual({ data: 42 })
108
- })
109
- })
110
-
111
- describe('fetchNotificationSettings', () => {
112
- it('returns empty object if settings is falsy or not object', async () => {
113
- GET.mockResolvedValueOnce(null)
114
- expect(await UserNotifications.fetchNotificationSettings()).toEqual({})
115
-
116
- GET.mockResolvedValueOnce('string')
117
- expect(await UserNotifications.fetchNotificationSettings()).toEqual({})
118
- })
119
-
120
- it('returns transformed settings grouped by brand', async () => {
121
- GET.mockResolvedValueOnce({
122
- drumeo: {
123
- new_lessons_and_features: { channel: 'email', value: true, brand: 'drumeo' },
124
- membership_perks_promotions: { channel: 'push', value: false, brand: 'drumeo' },
125
- },
126
- pianote: {
127
- membership_perks_promotions: { channel: 'email', value: true, brand: 'pianote' },
128
- },
129
- })
130
-
131
- const result = await UserNotifications.fetchNotificationSettings()
132
-
133
- expect(result).toEqual({
134
- drumeo: [
135
- { name: 'new_lessons_and_features', channel: 'email', value: true, brand: 'drumeo' },
136
- { name: 'membership_perks_promotions', channel: 'push', value: false, brand: 'drumeo' },
137
- ],
138
- pianote: [{ name: 'membership_perks_promotions', channel: 'email', value: true, brand: 'pianote' }],
139
- })
140
- })
141
- })
142
-
143
- describe('updateNotificationSetting', () => {
144
- it('throws if settingName not provided', async () => {
145
- await expect(UserNotifications.updateNotificationSetting({ brand: 'drumeo' })).rejects.toThrow(
146
- 'The "settingName" parameter is required.'
147
- )
148
- })
149
-
150
- it('throws if no channels provided', async () => {
151
- await expect(
152
- UserNotifications.updateNotificationSetting({ brand: 'drumeo', settingName: 'new_lesson' })
153
- ).rejects.toThrow('At least one channel (email, push, or bell) must be provided.')
154
- })
155
-
156
- it('calls PUT with correct payload and url', async () => {
157
- PUT.mockResolvedValueOnce({ success: true })
158
-
159
- const result = await UserNotifications.updateNotificationSetting({
160
- brand: 'drumeo',
161
- settingName: 'membership_perks_promotions',
162
- email: true,
163
- push: false,
164
- })
165
-
166
- expect(PUT).toHaveBeenCalledWith(
167
- '/api/notifications/v1/settings',
168
- {
169
- settings: [
170
- { name: 'membership_perks_promotions', channel: 'email', value: true, brand: 'drumeo' },
171
- { name: 'membership_perks_promotions', channel: 'push', value: false, brand: 'drumeo' },
172
- ],
173
- }
174
- )
175
- expect(result).toEqual({ success: true })
176
- })
177
- })
178
- })
@@ -1,283 +0,0 @@
1
- import { initializeTestDB } from '../initializeTestDB'
2
- import db from '../../../src/services/sync/repository-proxy'
3
-
4
- jest.mock('../../../src/services/sanity.js', () => ({
5
- ...jest.requireActual('../../../src/services/sanity.js'),
6
- fetchShows: jest.fn(),
7
- fetchByRailContentIds: jest.fn(),
8
- }))
9
- jest.mock('../../../src/services/contentAggregator.js', () => ({
10
- addContextToContent: jest.fn(),
11
- }))
12
- jest.mock('../../../src/services/content-org/learning-paths.ts', () =>
13
- require('../content-progress/__mocks__/mocks').mockLearningPaths(),
14
- )
15
- jest.mock('../../../src/services/awards/internal/content-progress-observer', () =>
16
- require('../content-progress/__mocks__/mocks').mockContentProgressObserver(),
17
- )
18
- jest.mock('../../../src/services/progress-events', () =>
19
- require('../content-progress/__mocks__/mocks').mockProgressEvents(),
20
- )
21
-
22
- import {
23
- processContentItem,
24
- getCompletedChildren,
25
- } from '../../../src/services/progress-row/rows/content-card.js'
26
- import { addContextToContent } from '../../../src/services/contentAggregator.js'
27
-
28
- const lessonMeta = { brand: 'drumeo', type: 'lesson', parent_id: 0 }
29
-
30
- initializeTestDB()
31
-
32
- describe('getCompletedChildren', () => {
33
- describe('show contentType', () => {
34
- test('counts completed shows from addContextToContent result', async () => {
35
- ;(addContextToContent as jest.Mock).mockResolvedValue([
36
- { id: 1, progressStatus: 'completed' },
37
- { id: 2, progressStatus: 'started' },
38
- { id: 3, progressStatus: 'completed' },
39
- { id: 4, progressStatus: 'not-started' },
40
- ])
41
- const content = { type: 'boot-camp', brand: 'drumeo' }
42
- const result = await getCompletedChildren(content, 'show')
43
- expect(result).toEqual({ completedChildren: 2, allChildren: 4 })
44
- })
45
-
46
- test('returns zero counts when no shows returned', async () => {
47
- ;(addContextToContent as jest.Mock).mockResolvedValue([])
48
- const content = { type: 'boot-camp', brand: 'drumeo' }
49
- const result = await getCompletedChildren(content, 'show')
50
- expect(result).toEqual({ completedChildren: 0, allChildren: 0 })
51
- })
52
-
53
- test('returns zero completed when no shows have completed status', async () => {
54
- ;(addContextToContent as jest.Mock).mockResolvedValue([
55
- { id: 1, progressStatus: 'started' },
56
- { id: 2, progressStatus: 'not-started' },
57
- ])
58
- const content = { type: 'boot-camp', brand: 'drumeo' }
59
- const result = await getCompletedChildren(content, 'show')
60
- expect(result).toEqual({ completedChildren: 0, allChildren: 2 })
61
- })
62
- })
63
-
64
- describe('non-show with children', () => {
65
- test('counts completed children from real DB progress', async () => {
66
- await db.contentProgress.recordProgress(101, null, 100, lessonMeta, undefined, { skipPush: true })
67
- await db.contentProgress.recordProgress(102, null, 30, lessonMeta, undefined, { skipPush: true })
68
- await db.contentProgress.recordProgress(103, null, 100, lessonMeta, undefined, { skipPush: true })
69
-
70
- const content = {
71
- type: 'course',
72
- children: [{ id: 101 }, { id: 102 }, { id: 103 }],
73
- }
74
- const result = await getCompletedChildren(content, 'course')
75
- expect(result).toEqual({ completedChildren: 2, allChildren: 3 })
76
- })
77
-
78
- test('recursively flattens nested children to leaf ids', async () => {
79
- await db.contentProgress.recordProgress(201, null, 100, lessonMeta, undefined, { skipPush: true })
80
- await db.contentProgress.recordProgress(202, null, 100, lessonMeta, undefined, { skipPush: true })
81
- await db.contentProgress.recordProgress(203, null, 50, lessonMeta, undefined, { skipPush: true })
82
-
83
- const content = {
84
- type: 'course',
85
- children: [
86
- { id: 201 },
87
- {
88
- id: 999,
89
- children: [{ id: 202 }, { id: 203 }],
90
- },
91
- ],
92
- }
93
- const result = await getCompletedChildren(content, 'course')
94
- expect(result).toEqual({ completedChildren: 2, allChildren: 3 })
95
- })
96
-
97
- test('returns zero when no children have progress recorded', async () => {
98
- const content = {
99
- type: 'course',
100
- children: [{ id: 901 }, { id: 902 }],
101
- }
102
- const result = await getCompletedChildren(content, 'course')
103
- expect(result).toEqual({ completedChildren: 0, allChildren: 2 })
104
- })
105
- })
106
-
107
- describe('non-show without children', () => {
108
- test('returns zero counts when children is undefined', async () => {
109
- const content = { type: 'song' }
110
- const result = await getCompletedChildren(content, 'transcription')
111
- expect(result).toEqual({ completedChildren: 0, allChildren: 0 })
112
- })
113
-
114
- test('returns zero counts when children is empty array', async () => {
115
- const content = { type: 'song', children: [] }
116
- const result = await getCompletedChildren(content, 'transcription')
117
- expect(result).toEqual({ completedChildren: 0, allChildren: 0 })
118
- })
119
- })
120
- })
121
-
122
- describe('processContentItem', () => {
123
- test('returns correctly shaped object for a song with no children', async () => {
124
- const content = {
125
- id: 500,
126
- type: 'song',
127
- brand: 'drumeo',
128
- title: 'Test Song',
129
- thumbnail: 'thumb.jpg',
130
- progressPercentage: 40,
131
- progressStatus: 'started',
132
- progressTimestamp: 1700000000,
133
- difficulty_string: 'Beginner',
134
- artist_name: 'The Band',
135
- slug: 'test-song',
136
- navigateTo: 'child-slug',
137
- }
138
- const result = await processContentItem(content)
139
- expect(result).toMatchObject({
140
- id: 500,
141
- progressType: 'content',
142
- header: 'transcription',
143
- pinned: false,
144
- progressTimestamp: 1700000000,
145
- body: {
146
- progressPercent: 40,
147
- thumbnail: 'thumb.jpg',
148
- title: 'Test Song',
149
- isLive: false,
150
- brand: 'drumeo',
151
- badge: null,
152
- badge_rear: null,
153
- badge_logo: null,
154
- badge_template: null,
155
- badge_template_rear: null,
156
- isLocked: false,
157
- subtitle: 'Beginner • The Band',
158
- },
159
- cta: {
160
- text: 'Continue',
161
- action: {
162
- type: 'song',
163
- brand: 'drumeo',
164
- id: 500,
165
- slug: 'test-song',
166
- child: 'child-slug',
167
- },
168
- },
169
- })
170
- })
171
-
172
- test('sets progressPercent to undefined when isLive=true', async () => {
173
- const content = {
174
- id: 1,
175
- type: 'song',
176
- brand: 'drumeo',
177
- progressPercentage: 80,
178
- isLive: true,
179
- }
180
- const result = await processContentItem(content)
181
- expect(result.body.progressPercent).toBeUndefined()
182
- expect(result.body.isLive).toBe(true)
183
- })
184
-
185
- test('isLive defaults to false when missing', async () => {
186
- const content = { id: 2, type: 'song', brand: 'drumeo', progressPercentage: 50 }
187
- const result = await processContentItem(content)
188
- expect(result.body.isLive).toBe(false)
189
- expect(result.body.progressPercent).toBe(50)
190
- })
191
-
192
- test('pinned defaults to false when missing', async () => {
193
- const content = { id: 3, type: 'song', brand: 'drumeo' }
194
- const result = await processContentItem(content)
195
- expect(result.pinned).toBe(false)
196
- })
197
-
198
- test('pinned reflects content.pinned when set', async () => {
199
- const content = { id: 4, type: 'song', brand: 'drumeo', pinned: true }
200
- const result = await processContentItem(content)
201
- expect(result.pinned).toBe(true)
202
- })
203
-
204
- test('isLocked is false when need_access is missing', async () => {
205
- const content = { id: 5, type: 'song', brand: 'drumeo' }
206
- const result = await processContentItem(content)
207
- expect(result.body.isLocked).toBe(false)
208
- })
209
-
210
- test('isLocked is true when need_access is true', async () => {
211
- const content = { id: 6, type: 'song', brand: 'drumeo', need_access: true }
212
- const result = await processContentItem(content)
213
- expect(result.body.isLocked).toBe(true)
214
- })
215
-
216
- test('passes through badge fields when set', async () => {
217
- const content = {
218
- id: 7,
219
- type: 'song',
220
- brand: 'drumeo',
221
- badge: 'badge.png',
222
- badge_rear: 'badge-rear.png',
223
- badge_logo: 'logo.png',
224
- badge_template: 'tpl',
225
- badge_template_rear: 'tpl-rear',
226
- }
227
- const result = await processContentItem(content)
228
- expect(result.body).toMatchObject({
229
- badge: 'badge.png',
230
- badge_rear: 'badge-rear.png',
231
- badge_logo: 'logo.png',
232
- badge_template: 'tpl',
233
- badge_template_rear: 'tpl-rear',
234
- })
235
- })
236
-
237
- test('populates completed_children/all_children for course with children', async () => {
238
- await db.contentProgress.recordProgress(701, null, 100, lessonMeta, undefined, { skipPush: true })
239
- await db.contentProgress.recordProgress(702, null, 100, lessonMeta, undefined, { skipPush: true })
240
- await db.contentProgress.recordProgress(703, null, 25, lessonMeta, undefined, { skipPush: true })
241
-
242
- const content: any = {
243
- id: 700,
244
- type: 'course',
245
- brand: 'drumeo',
246
- children: [{ id: 701 }, { id: 702 }, { id: 703 }],
247
- }
248
- const result = await processContentItem(content)
249
- expect(content.completed_children).toBe(2)
250
- expect(content.all_children).toBe(3)
251
- expect(result.body.subtitle).toBe('2 of 3 Lessons Complete')
252
- })
253
-
254
- test('uses contentType from getFormattedType in header and cta resolution', async () => {
255
- const content = {
256
- id: 8,
257
- type: 'guided-course',
258
- brand: 'drumeo',
259
- progressStatus: 'not-started',
260
- }
261
- const result = await processContentItem(content)
262
- expect(result.header).toBe('guided course')
263
- expect(result.cta.text).toBe('Start Course')
264
- })
265
-
266
- test('cta action mirrors content identity fields', async () => {
267
- const content = {
268
- id: 9,
269
- type: 'song',
270
- brand: 'pianote',
271
- slug: 'my-slug',
272
- navigateTo: 'next-child',
273
- }
274
- const result = await processContentItem(content)
275
- expect(result.cta.action).toEqual({
276
- type: 'song',
277
- brand: 'pianote',
278
- id: 9,
279
- slug: 'my-slug',
280
- child: 'next-child',
281
- })
282
- })
283
- })
@@ -1,144 +0,0 @@
1
- /**
2
- * @jest-environment jsdom
3
- */
4
-
5
- // Replace jsdom's incomplete IndexedDB with a spec-compliant fake
6
- import 'fake-indexeddb/auto'
7
-
8
- import {
9
- patchIndexedDBOpenErrors,
10
- patchIndexedDBDeleteDatabaseErrors,
11
- simulateIndexedDBDelay,
12
- simulateIndexedDBUnavailable,
13
- simulateIndexedDBFailure,
14
- simulateIndexedDBQuotaExceeded,
15
- } from '@/services/sync/adapters/lokijs'
16
-
17
- // Each test gets a fresh indexedDB instance so patches don't bleed between tests
18
- let originalOpen: typeof indexedDB.open
19
- let originalDeleteDatabase: typeof indexedDB.deleteDatabase
20
-
21
- beforeEach(() => {
22
- originalOpen = indexedDB.open.bind(indexedDB)
23
- originalDeleteDatabase = indexedDB.deleteDatabase.bind(indexedDB)
24
- })
25
-
26
- afterEach(() => {
27
- // Restore original methods after each test
28
- ;(indexedDB as any).open = originalOpen
29
- ;(indexedDB as any).deleteDatabase = originalDeleteDatabase
30
- })
31
-
32
- // ---
33
-
34
- describe('patchIndexedDBOpenErrors', () => {
35
- test('fires callback when open request errors', done => {
36
- patchIndexedDBOpenErrors((error, _event) => {
37
- expect(error).not.toBeNull()
38
- done()
39
- })
40
-
41
- // Force an error by opening with a downgrade attempt
42
- const req = indexedDB.open('test-db', 1)
43
- req.onsuccess = () => {
44
- // Now try to open with lower version to trigger an error via another mechanism
45
- // Actually trigger via a direct error simulation instead
46
- }
47
-
48
- // Simplest: patch is in place, manually fire an open error by opening the same db
49
- // at a lower version after it exists at version 1
50
- req.onsuccess = () => {
51
- req.result.close()
52
- // Open at lower version → versionchange error in strict IDB
53
- // In fakeIndexedDB this won't error the same way; test by calling stored onerror
54
- // The patch intercepts onerror — trigger it via the proxy
55
- ;(indexedDB as any).open = originalOpen // restore so next open works normally
56
-
57
- // Verify patch was applied by checking it wraps the error handler
58
- done()
59
- }
60
-
61
- req.onerror = () => done()
62
- })
63
-
64
- test('still calls original onerror handler', done => {
65
- const originalHandler = jest.fn()
66
-
67
- patchIndexedDBOpenErrors((_error, _event) => {
68
- // patch callback called
69
- })
70
-
71
- const req = indexedDB.open(`test-onerror-${Date.now()}`)
72
- const proxiedReq = req as any
73
-
74
- // Simulate an error event through the proxy
75
- proxiedReq.onerror = originalHandler
76
-
77
- // Synthesize an error event on the real request
78
- const errorEvent = new Event('error')
79
- Object.defineProperty(errorEvent, 'target', {
80
- value: { error: new DOMException('Test error', 'UnknownError') },
81
- configurable: true,
82
- })
83
- ;(req as any).onerror?.(errorEvent)
84
-
85
- expect(originalHandler).toHaveBeenCalled()
86
- done()
87
- })
88
- })
89
-
90
- describe('simulateIndexedDBUnavailable', () => {
91
- test('causes indexedDB.open to throw synchronously', () => {
92
- simulateIndexedDBUnavailable()
93
-
94
- expect(() => {
95
- indexedDB.open('test-unavailable')
96
- }).toThrow(DOMException)
97
- })
98
- })
99
-
100
- describe('simulateIndexedDBQuotaExceeded', () => {
101
- test('triggers QuotaExceededError after open call', done => {
102
- simulateIndexedDBQuotaExceeded()
103
-
104
- const req = indexedDB.open(`test-quota-${Date.now()}`)
105
- req.onerror = event => {
106
- const error = (event.target as any)?.error
107
- expect(error?.name).toBe('QuotaExceededError')
108
- done()
109
- }
110
-
111
- // Timeout safety
112
- setTimeout(() => done(new Error('QuotaExceeded event not fired in time')), 1000)
113
- })
114
- })
115
-
116
- describe('simulateIndexedDBFailure', () => {
117
- test('triggers an error after the specified delay', done => {
118
- simulateIndexedDBFailure(50) // 50ms delay for test speed
119
-
120
- const req = indexedDB.open(`test-failure-${Date.now()}`)
121
- req.onerror = event => {
122
- const error = (event.target as any)?.error
123
- expect(error).toBeInstanceOf(DOMException)
124
- done()
125
- }
126
-
127
- setTimeout(() => done(new Error('Failure event not fired in time')), 2000)
128
- })
129
- })
130
-
131
- describe('patchIndexedDBDeleteDatabaseErrors', () => {
132
- test('returns a proxy request immediately, real delete fires after 1ms', done => {
133
- patchIndexedDBDeleteDatabaseErrors()
134
-
135
- // The patched deleteDatabase should return immediately (a proxy)
136
- const req = indexedDB.deleteDatabase(`test-delete-${Date.now()}`)
137
- expect(req).toBeDefined()
138
-
139
- // Set handler on proxy — it should fire after the real request completes
140
- req.onsuccess = () => done()
141
-
142
- setTimeout(() => done(new Error('Delete did not complete')), 1000)
143
- })
144
- })