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