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,51 +0,0 @@
1
- jest.mock('../../../src/services/sync/manager', () => ({ default: class SyncManager {} }))
2
- jest.mock('../../../src/services/sync/repository-proxy', () => ({ db: {} }))
3
- import { makeContext } from './helpers/index'
4
- describe('SyncContext', () => {
5
- test('start calls start on all providers', () => {
6
- const context = makeContext() as any
7
- const providers = [
8
- context.session,
9
- context.connectivity,
10
- context.visibility,
11
- context.tabs,
12
- context.durability,
13
- ]
14
- providers.forEach(p => jest.spyOn(p, 'start'))
15
- context.start()
16
- providers.forEach(p => expect(p.start).toHaveBeenCalled())
17
- })
18
- test('stop calls stop on all providers', () => {
19
- const context = makeContext() as any
20
- const providers = [
21
- context.session,
22
- context.connectivity,
23
- context.visibility,
24
- context.tabs,
25
- context.durability,
26
- ]
27
- providers.forEach(p => jest.spyOn(p, 'stop'))
28
- context.stop()
29
- providers.forEach(p => expect(p.stop).toHaveBeenCalled())
30
- })
31
- test('session getter returns session provider', () => {
32
- const context = makeContext()
33
- expect(context.session).toBeDefined()
34
- })
35
- test('connectivity getter returns connectivity provider', () => {
36
- const context = makeContext()
37
- expect(context.connectivity).toBeDefined()
38
- })
39
- test('visibility getter returns visibility provider', () => {
40
- const context = makeContext()
41
- expect(context.visibility).toBeDefined()
42
- })
43
- test('tabs getter returns tabs provider', () => {
44
- const context = makeContext()
45
- expect(context.tabs).toBeDefined()
46
- })
47
- test('durability getter returns durability provider', () => {
48
- const context = makeContext()
49
- expect(context.durability).toBeDefined()
50
- })
51
- })
@@ -1,158 +0,0 @@
1
- import createLogoutWarningEffect from '@/services/sync/effects/logout-warning'
2
- import type SyncStore from '@/services/sync/store/index'
3
- import { makeContext } from '../helpers/index'
4
-
5
- // ---
6
-
7
- function makeMockStore(model: any = { table: 'test' }) {
8
- let handler: ((records: any[]) => void) | null = null
9
-
10
- const store = {
11
- model,
12
- collection: {
13
- query: jest.fn().mockReturnValue({
14
- observe: jest.fn().mockReturnValue({
15
- subscribe: (h: (records: any[]) => void) => {
16
- handler = h
17
- return { unsubscribe: () => { handler = null } }
18
- },
19
- }),
20
- }),
21
- },
22
- } as unknown as SyncStore
23
-
24
- const push = (records: any[]) => handler?.(records)
25
-
26
- return { store, push }
27
- }
28
-
29
- // ---
30
-
31
- describe('notification on unsynced records', () => {
32
- test('notifyCallback called when unsynced records appear', () => {
33
- const notify = jest.fn()
34
- const { store, push } = makeMockStore()
35
-
36
- createLogoutWarningEffect(notify)(makeContext(), [store])
37
- push([{ id: 'rec-1' }])
38
-
39
- expect(notify).toHaveBeenCalledTimes(1)
40
- })
41
-
42
- test('notifyCallback called with the model class of the store', () => {
43
- const notify = jest.fn()
44
- const model = { table: 'content_progress' }
45
- const { store, push } = makeMockStore(model)
46
-
47
- createLogoutWarningEffect(notify)(makeContext(), [store])
48
- push([{ id: 'rec-1' }])
49
-
50
- expect(notify.mock.calls[0][0]).toContain(model)
51
- })
52
-
53
- test('notifyCallback called with empty array when records go back to zero', () => {
54
- const notify = jest.fn()
55
- const { store, push } = makeMockStore()
56
-
57
- createLogoutWarningEffect(notify)(makeContext(), [store])
58
- push([{ id: 'rec-1' }])
59
- push([])
60
-
61
- expect(notify).toHaveBeenCalledTimes(2)
62
- expect(notify.mock.calls[1][0]).toHaveLength(0)
63
- })
64
-
65
- test('model removed from unsyncedModels when records cleared', () => {
66
- const notify = jest.fn()
67
- const model = { table: 'content_progress' }
68
- const { store, push } = makeMockStore(model)
69
-
70
- createLogoutWarningEffect(notify)(makeContext(), [store])
71
- push([{ id: 'rec-1' }])
72
- push([])
73
-
74
- expect(notify.mock.calls[1][0]).not.toContain(model)
75
- })
76
- })
77
-
78
- // ---
79
-
80
- describe('multiple stores', () => {
81
- test('tracks unsynced models across stores independently', () => {
82
- const notify = jest.fn()
83
- const modelA = { table: 'content_progress' }
84
- const modelB = { table: 'content_likes' }
85
- const storeA = makeMockStore(modelA)
86
- const storeB = makeMockStore(modelB)
87
-
88
- createLogoutWarningEffect(notify)(makeContext(), [storeA.store, storeB.store])
89
-
90
- storeA.push([{ id: 'rec-1' }])
91
- storeB.push([{ id: 'rec-2' }])
92
-
93
- const lastCall = notify.mock.calls[notify.mock.calls.length - 1][0]
94
- expect(lastCall).toContain(modelA)
95
- expect(lastCall).toContain(modelB)
96
- })
97
-
98
- test('clears only the model whose records were resolved', () => {
99
- const notify = jest.fn()
100
- const modelA = { table: 'content_progress' }
101
- const modelB = { table: 'content_likes' }
102
- const storeA = makeMockStore(modelA)
103
- const storeB = makeMockStore(modelB)
104
-
105
- createLogoutWarningEffect(notify)(makeContext(), [storeA.store, storeB.store])
106
-
107
- storeA.push([{ id: 'rec-1' }])
108
- storeB.push([{ id: 'rec-2' }])
109
- storeA.push([]) // storeA resolved
110
-
111
- const lastCall = notify.mock.calls[notify.mock.calls.length - 1][0]
112
- expect(lastCall).not.toContain(modelA)
113
- expect(lastCall).toContain(modelB)
114
- })
115
-
116
- test('notifies once per store event regardless of other stores', () => {
117
- const notify = jest.fn()
118
- const storeA = makeMockStore({ table: 'a' })
119
- const storeB = makeMockStore({ table: 'b' })
120
-
121
- createLogoutWarningEffect(notify)(makeContext(), [storeA.store, storeB.store])
122
-
123
- storeA.push([{ id: '1' }])
124
- storeA.push([{ id: '1' }])
125
- storeB.push([{ id: '2' }])
126
-
127
- expect(notify).toHaveBeenCalledTimes(3)
128
- })
129
- })
130
-
131
- // ---
132
-
133
- describe('teardown', () => {
134
- test('teardown unsubscribes all store subscriptions', () => {
135
- const notify = jest.fn()
136
- const { store, push } = makeMockStore()
137
-
138
- const teardown = createLogoutWarningEffect(notify)(makeContext(), [store])
139
- teardown()
140
- push([{ id: 'rec-1' }])
141
-
142
- expect(notify).not.toHaveBeenCalled()
143
- })
144
-
145
- test('teardown unsubscribes all stores', () => {
146
- const notify = jest.fn()
147
- const storeA = makeMockStore({ table: 'a' })
148
- const storeB = makeMockStore({ table: 'b' })
149
-
150
- const teardown = createLogoutWarningEffect(notify)(makeContext(), [storeA.store, storeB.store])
151
- teardown()
152
-
153
- storeA.push([{ id: '1' }])
154
- storeB.push([{ id: '2' }])
155
-
156
- expect(notify).not.toHaveBeenCalled()
157
- })
158
- })
@@ -1,196 +0,0 @@
1
- import createPushFailureNotificationEffect, { NOTIFICATION_COOLDOWN, MUTE_PERIOD } from '@/services/sync/effects/push-failure-notification'
2
- import type SyncStore from '@/services/sync/store/index'
3
- import { makeContext } from '../helpers/index'
4
-
5
- // ---
6
-
7
- beforeEach(() => {
8
- jest.useFakeTimers()
9
- })
10
-
11
- afterEach(() => {
12
- jest.useRealTimers()
13
- })
14
-
15
- // ---
16
-
17
- function makeMockStore() {
18
- const listeners = new Map<string, (() => void)[]>()
19
-
20
- const store = {
21
- on: jest.fn().mockImplementation((event: string, handler: () => void) => {
22
- const list = listeners.get(event) ?? []
23
- list.push(handler)
24
- listeners.set(event, list)
25
- return () => {
26
- const idx = list.indexOf(handler)
27
- if (idx >= 0) list.splice(idx, 1)
28
- }
29
- }),
30
- } as unknown as SyncStore
31
-
32
- const emit = (event: string) => listeners.get(event)?.forEach(h => h())
33
-
34
- return { store, emit }
35
- }
36
-
37
- function makeEffect(notify = jest.fn()) {
38
- const effect = createPushFailureNotificationEffect(notify)
39
- return { effect, notify }
40
- }
41
-
42
- // ---
43
-
44
- describe('notification triggering', () => {
45
- test('notifyCallback called when failedPush fires', () => {
46
- const { store, emit } = makeMockStore()
47
- const { effect, notify } = makeEffect()
48
-
49
- effect(makeContext(), [store])
50
- emit('failedPush')
51
-
52
- expect(notify).toHaveBeenCalledTimes(1)
53
- })
54
-
55
- test('notifyCallback called with mute function', () => {
56
- const { store, emit } = makeMockStore()
57
- const { effect, notify } = makeEffect()
58
-
59
- effect(makeContext(), [store])
60
- emit('failedPush')
61
-
62
- expect(notify.mock.calls[0][0]).toHaveProperty('mute')
63
- expect(typeof notify.mock.calls[0][0].mute).toBe('function')
64
- })
65
-
66
- test('failedPush on any store triggers notification', () => {
67
- const s1 = makeMockStore()
68
- const s2 = makeMockStore()
69
- const { effect, notify } = makeEffect()
70
-
71
- effect(makeContext(), [s1.store, s2.store])
72
-
73
- s2.emit('failedPush')
74
-
75
- expect(notify).toHaveBeenCalledTimes(1)
76
- })
77
- })
78
-
79
- // ---
80
-
81
- describe('cooldown', () => {
82
- test('second failedPush within cooldown does not notify again', () => {
83
- const { store, emit } = makeMockStore()
84
- const { effect, notify } = makeEffect()
85
-
86
- effect(makeContext(), [store])
87
- emit('failedPush')
88
- emit('failedPush')
89
-
90
- expect(notify).toHaveBeenCalledTimes(1)
91
- })
92
-
93
- test('notifies again after cooldown expires', () => {
94
- const { store, emit } = makeMockStore()
95
- const { effect, notify } = makeEffect()
96
-
97
- effect(makeContext(), [store])
98
- emit('failedPush')
99
-
100
- jest.advanceTimersByTime(NOTIFICATION_COOLDOWN)
101
- emit('failedPush')
102
-
103
- expect(notify).toHaveBeenCalledTimes(2)
104
- })
105
-
106
- test('does not notify just before cooldown expires', () => {
107
- const { store, emit } = makeMockStore()
108
- const { effect, notify } = makeEffect()
109
-
110
- effect(makeContext(), [store])
111
- emit('failedPush')
112
-
113
- jest.advanceTimersByTime(NOTIFICATION_COOLDOWN - 1)
114
- emit('failedPush')
115
-
116
- expect(notify).toHaveBeenCalledTimes(1)
117
- })
118
- })
119
-
120
- // ---
121
-
122
- describe('mute', () => {
123
- test('mute() prevents further notifications', () => {
124
- const { store, emit } = makeMockStore()
125
- const { effect, notify } = makeEffect()
126
-
127
- effect(makeContext(), [store])
128
- emit('failedPush')
129
-
130
- const { mute } = notify.mock.calls[0][0]
131
- mute()
132
-
133
- jest.advanceTimersByTime(NOTIFICATION_COOLDOWN)
134
- emit('failedPush')
135
-
136
- expect(notify).toHaveBeenCalledTimes(1)
137
- })
138
-
139
- test('notifications resume after mute period expires', () => {
140
- const { store, emit } = makeMockStore()
141
- const { effect, notify } = makeEffect()
142
-
143
- effect(makeContext(), [store])
144
- emit('failedPush')
145
-
146
- const { mute } = notify.mock.calls[0][0]
147
- mute()
148
-
149
- jest.advanceTimersByTime(MUTE_PERIOD + NOTIFICATION_COOLDOWN)
150
- emit('failedPush')
151
-
152
- expect(notify).toHaveBeenCalledTimes(2)
153
- })
154
-
155
- test('mute does not suppress notification already in progress', () => {
156
- const { store, emit } = makeMockStore()
157
- const { effect, notify } = makeEffect()
158
-
159
- effect(makeContext(), [store])
160
- emit('failedPush')
161
-
162
- expect(notify).toHaveBeenCalledTimes(1)
163
- })
164
- })
165
-
166
- // ---
167
-
168
- describe('teardown', () => {
169
- test('teardown prevents further notifications', () => {
170
- const { store, emit } = makeMockStore()
171
- const { effect, notify } = makeEffect()
172
-
173
- const teardown = effect(makeContext(), [store])
174
- teardown()
175
-
176
- jest.advanceTimersByTime(NOTIFICATION_COOLDOWN)
177
- emit('failedPush')
178
-
179
- expect(notify).not.toHaveBeenCalled()
180
- })
181
-
182
- test('teardown unsubscribes all stores', () => {
183
- const s1 = makeMockStore()
184
- const s2 = makeMockStore()
185
- const { effect, notify } = makeEffect()
186
-
187
- const teardown = effect(makeContext(), [s1.store, s2.store])
188
- teardown()
189
-
190
- jest.advanceTimersByTime(NOTIFICATION_COOLDOWN)
191
- s1.emit('failedPush')
192
- s2.emit('failedPush')
193
-
194
- expect(notify).not.toHaveBeenCalled()
195
- })
196
- })
@@ -1,106 +0,0 @@
1
- import { SyncError, SyncValidationError, SyncAbortError, SyncInitError, SyncSetupError, SyncUnexpectedError } from '../../../../src/services/sync/errors/index'
2
- import { inBoundary } from '../../../../src/services/sync/errors/boundary'
3
- import { SyncTelemetry } from '../../../../src/services/sync/telemetry/index'
4
-
5
- jest.spyOn(SyncTelemetry, 'getInstance').mockReturnValue({
6
- capture: jest.fn()
7
- } as any)
8
-
9
- describe('SyncError', () => {
10
- test('has correct name property', () => {
11
- const error = new SyncError('test message')
12
- expect(error.name).toBe('SyncError')
13
- })
14
- test('isReported() returns false by default', () => {
15
- const error = new SyncError('test message')
16
- expect(error.isReported()).toBe(false)
17
- })
18
- test('markReported() sets isReported() to true', () => {
19
- const error = new SyncError('test message')
20
- error.markReported()
21
- expect(error.isReported()).toBe(true)
22
- })
23
- test('getDetails() returns details passed to constructor', () => {
24
- const details = { table: 'content_progress', userId: 1 }
25
- const error = new SyncError('test message', details)
26
- expect(error.getDetails()).toEqual(details)
27
- })
28
- })
29
- describe('SyncValidationError', () => {
30
- test('has correct name property', () => {
31
- const error = new SyncValidationError('invalid value', {} as any)
32
- expect(error.name).toBe('SyncValidationError')
33
- })
34
- test('is instanceof SyncError', () => {
35
- const error = new SyncValidationError('invalid value', {} as any)
36
- expect(error).toBeInstanceOf(SyncError)
37
- })
38
- })
39
- describe('SyncAbortError', () => {
40
- test('has default message when none provided', () => {
41
- const error = new SyncAbortError()
42
- expect(error.message).toBe('Sync operation was aborted')
43
- })
44
- test('accepts custom message', () => {
45
- const error = new SyncAbortError('custom abort message')
46
- expect(error.message).toBe('custom abort message')
47
- })
48
- })
49
- describe('SyncInitError', () => {
50
- test('has correct name and stores cause in details', () => {
51
- const cause = new Error('original error')
52
- const error = new SyncInitError(cause)
53
- expect(error.name).toBe('SyncInitError')
54
- expect(error.getDetails()).toEqual({ cause })
55
- })
56
- })
57
- describe('SyncSetupError', () => {
58
- test('is instanceof SyncError', () => {
59
- const error = new SyncSetupError(new Error('setup failed'))
60
- expect(error).toBeInstanceOf(SyncError)
61
- })
62
- })
63
- describe('SyncUnexpectedError', () => {
64
- test('is instanceof SyncError', () => {
65
- const error = new SyncUnexpectedError('unexpected')
66
- expect(error).toBeInstanceOf(SyncError)
67
- })
68
- })
69
-
70
- describe('inBoundary', () => {
71
- let mockCapture: jest.Mock
72
-
73
- beforeEach(() => {
74
- mockCapture = jest.fn()
75
- jest.spyOn(SyncTelemetry, 'getInstance').mockReturnValue({
76
- capture: mockCapture
77
- } as any)
78
- })
79
- test('returns result of sync function when no error', () => {
80
- const result = inBoundary(() => 'hello')
81
- expect(result).toBe('hello')
82
- expect(mockCapture).not.toHaveBeenCalled()
83
- })
84
- test('throws and calls capture when sync function throws', () => {
85
- const error = new Error('sync error')
86
- expect(() => inBoundary(() => { throw error })).toThrow('sync error')
87
- expect(mockCapture).toHaveBeenCalledWith(error, undefined)
88
- })
89
- test('returns resolved value of async function when no error', async () => {
90
- const result = await inBoundary(async () => 'async hello')
91
- expect(result).toBe('async hello')
92
- expect(mockCapture).not.toHaveBeenCalled()
93
- })
94
- test('re-throws and calls capture when async function rejects', async () => {
95
- const error = new Error('async error')
96
- await expect(inBoundary(async () => { throw error })).rejects.toThrow('async error')
97
- expect(mockCapture).toHaveBeenCalledWith(error, undefined)
98
- })
99
- test('passes context to fn and to capture on error', () => {
100
- const context = { table: 'content_progress' }
101
- const error = new Error('context error')
102
- expect(() => inBoundary((ctx) => { throw error }, context)).toThrow('context error')
103
- expect(mockCapture).toHaveBeenCalledWith(error, context)
104
- })
105
- })
106
-
@@ -1,61 +0,0 @@
1
- import { percent, nullableString, string, number, varchar, numberInRange, enumValue } from '../../../../src/services/sync/errors/validators'
2
- import { SyncValidationError } from '../../../../src/services/sync/errors/index'
3
- describe('validators', () => {
4
- describe('percent', () => {
5
- test('returns value when within 0-100', () => {
6
- expect(percent(50)).toBe(50)
7
- })
8
- })
9
- test('returns 0 at minimum boundary', () => {
10
- expect(percent(0)).toBe(0)
11
- })
12
- test('returns 100 at maximum boundary', () => {
13
- expect(percent(100)).toBe(100)
14
- })
15
- test('throws SyncValidationError when value exceeds 100', () => {
16
- expect(() => percent(101)).toThrow(SyncValidationError)
17
- })
18
- test('throws SyncValidationError when value is negative', () => {
19
- expect(() => percent(-1)).toThrow(SyncValidationError)
20
- })
21
- })
22
- describe('nullableString', () => {
23
- test('returns null when value is null', () => {
24
- expect(nullableString(null)).toBeNull()
25
- })
26
- test('returns string when value is a string', () => {
27
- expect(nullableString('hello')).toBe('hello')
28
- })
29
- })
30
- describe('string', () => {
31
- test('throws SyncValidationError when value is not a string', () => {
32
- expect(() => string(123)).toThrow(SyncValidationError)
33
- })
34
- })
35
- describe('number', () => {
36
- test('throws SyncValidationError when value is not a number', () => {
37
- expect(() => number('abc')).toThrow(SyncValidationError)
38
- })
39
- })
40
- describe('varchar', () => {
41
- test('returns string when within max length', () => {
42
- expect(varchar(10)('hello')).toBe('hello')
43
- })
44
- test('throws SyncValidationError when string exceeds max length', () => {
45
- expect(() => varchar(5)('toolong')).toThrow(SyncValidationError)
46
- })
47
- })
48
- describe('numberInRange', () => {
49
- test('returns value when within range', () => {
50
- expect(numberInRange(0, 10)(5)).toBe(5)
51
- })
52
- test('throws SyncValidationError when value exceeds max', () => {
53
- expect(() => numberInRange(0, 10)(11)).toThrow(SyncValidationError)
54
- })
55
- })
56
- describe('enumValue', () => {
57
- test('throws SyncValidationError when value is not in enum', () => {
58
- enum TestEnum { A = 'a', B = 'b' }
59
- expect(() => enumValue(TestEnum)('c')).toThrow(SyncValidationError)
60
- })
61
- })
@@ -1,44 +0,0 @@
1
- import { appSchema, tableSchema } from '@nozbe/watermelondb'
2
- import { Database } from '@nozbe/watermelondb'
3
- import LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs'
4
- import BaseModel from '@/services/sync/models/Base'
5
-
6
- export const TEST_TABLE = 'test_items'
7
-
8
- export const testSchema = appSchema({
9
- version: 1,
10
- tables: [
11
- tableSchema({
12
- name: TEST_TABLE,
13
- columns: [
14
- { name: 'server_record_id', type: 'number' },
15
- { name: 'value', type: 'string' },
16
- { name: 'score', type: 'number' },
17
- { name: 'created_at', type: 'number' },
18
- { name: 'updated_at', type: 'number' },
19
- ],
20
- }),
21
- ],
22
- })
23
-
24
- export default class TestModel extends BaseModel {
25
- static table = TEST_TABLE
26
-
27
- get value() { return this._getRaw('value') as string }
28
- get score() { return this._getRaw('score') as number }
29
-
30
- set value(v: string) { this._setRaw('value', v) }
31
- set score(v: number) { this._setRaw('score', v) }
32
- }
33
-
34
- export function makeTestDatabase(opts: { useIncrementalIndexedDB?: boolean; dbName?: string } = {}) {
35
- const adapter = new LokiJSAdapter({
36
- schema: testSchema,
37
- useWebWorker: false,
38
- useIncrementalIndexedDB: opts.useIncrementalIndexedDB ?? false,
39
- dbName: opts.dbName ?? `test_store_${Date.now()}_${Math.random()}`,
40
- extraLokiOptions: { autosave: false },
41
- })
42
-
43
- return new Database({ adapter, modelClasses: [TestModel] })
44
- }