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.
- package/.agent/decisions/2026-07-20-mu2-1724-songs-lessons-counts.md +33 -0
- package/.agent/decisions/2026-07-20-query-builder-tostring.md +37 -0
- package/CHANGELOG.md +8 -0
- package/package.json +2 -1
- package/src/contentTypeConfig.js +1 -0
- package/src/lib/sanity/query.ts +5 -0
- package/src/services/config.js +15 -0
- package/src/services/content/artist.ts +1 -3
- package/src/services/content/counts.ts +47 -0
- package/src/services/content/genre.ts +1 -3
- package/src/services/content/instructor.ts +1 -3
- package/src/services/userActivity.js +2 -0
- package/src/version-info.js +1 -0
- package/.claude/settings.local.json +0 -53
- package/test/initializeTests.js +0 -60
- package/test/integration/awards/award-exclusion-handling.test.ts +0 -275
- package/test/integration/awards/internal/image-utils.test.ts +0 -86
- package/test/integration/content-progress/__mocks__/mocks.ts +0 -104
- package/test/integration/content-progress/contentProgress.test.ts +0 -335
- package/test/integration/content-progress/e2eOfflineProgress.test.ts +0 -352
- package/test/integration/content-progress/e2eProgress.test.ts +0 -612
- package/test/integration/content-progress/getters.test.ts +0 -334
- package/test/integration/content-progress/helpers.test.ts +0 -263
- package/test/integration/content-progress/offlineContentProgress.test.ts +0 -226
- package/test/integration/forums.test.ts +0 -209
- package/test/integration/infrastructure/FetchRequestExecutor.test.ts +0 -88
- package/test/integration/initializeTestDB.ts +0 -80
- package/test/integration/learning-paths.test.ts +0 -854
- package/test/integration/notifications.test.ts +0 -178
- package/test/integration/progress-row/content-card.test.ts +0 -283
- package/test/integration/sync/adapters/idb-errors.test.ts +0 -144
- package/test/integration/sync/adapters/sqlite-errors.test.ts +0 -173
- package/test/integration/sync/fetch.test.ts +0 -224
- package/test/integration/sync/repositories/content-likes.test.ts +0 -99
- package/test/integration/sync/repositories/practices.test.ts +0 -179
- package/test/integration/sync/repositories/progress.test.ts +0 -245
- package/test/integration/sync/repositories/user-award-progress.static.test.ts +0 -68
- package/test/integration/sync/repositories/user-award-progress.test.ts +0 -387
- package/test/integration/sync/store/cross-user-protection.test.ts +0 -217
- package/test/integration/sync/store/store-idb.test.ts +0 -180
- package/test/integration/sync/store/store.test.ts +0 -361
- package/test/integration/user/permissions.test.js +0 -20
- package/test/integration/userActivity.test.ts +0 -102
- package/test/live/README.md +0 -29
- package/test/live/content.test.js +0 -116
- package/test/localStorageMock.js +0 -21
- package/test/log.js +0 -5
- package/test/mockData/award-definitions.js +0 -171
- package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -35
- package/test/mockData/mockData_progress_content.json +0 -182
- package/test/mockData/mockData_sanity_progress_content.json +0 -1451
- package/test/mockData/mockData_user_practices.json +0 -9
- package/test/setupConsole.js +0 -6
- package/test/setupNetworkGuard.js +0 -3
- package/test/setupTimers.js +0 -13
- package/test/unit/HttpClient.test.ts +0 -267
- package/test/unit/awards/award-alacarte-observer.test.ts +0 -199
- package/test/unit/awards/award-auto-refresh.test.ts +0 -84
- package/test/unit/awards/award-calculations.test.ts +0 -34
- package/test/unit/awards/award-callbacks.test.ts +0 -144
- package/test/unit/awards/award-certificate-display.test.ts +0 -330
- package/test/unit/awards/award-collection-edge-cases.test.ts +0 -212
- package/test/unit/awards/award-collection-filtering.test.ts +0 -287
- package/test/unit/awards/award-completion-flow.test.ts +0 -306
- package/test/unit/awards/award-multi-lesson.test.ts +0 -243
- package/test/unit/awards/award-observer-integration.test.ts +0 -327
- package/test/unit/awards/award-query-messages.test.ts +0 -448
- package/test/unit/awards/award-user-collection.test.ts +0 -416
- package/test/unit/awards/duplicate-prevention.test.ts +0 -120
- package/test/unit/awards/helpers/completion-mock.ts +0 -56
- package/test/unit/awards/helpers/index.ts +0 -3
- package/test/unit/awards/helpers/mock-setup.ts +0 -79
- package/test/unit/awards/helpers/progress-emitter.ts +0 -45
- package/test/unit/awards/message-generator.test.ts +0 -162
- package/test/unit/content-progress/bubbleTrickle.test.ts +0 -322
- package/test/unit/content-progress/helpers.test.ts +0 -329
- package/test/unit/content-progress/navigateTo.test.ts +0 -392
- package/test/unit/contentLikes.test.ts +0 -62
- package/test/unit/contentMetaData.test.ts +0 -58
- package/test/unit/dataContext.test.ts +0 -38
- package/test/unit/dateUtils.test.ts +0 -188
- package/test/unit/endScreen.test.js +0 -712
- package/test/unit/eventsAPI.test.ts +0 -159
- package/test/unit/imageSRCBuilder.test.ts +0 -37
- package/test/unit/imageSRCVerify.test.ts +0 -155
- package/test/unit/infrastructure/DefaultHeaderProvider.test.ts +0 -39
- package/test/unit/infrastructure/sanity/ContentClient.test.ts +0 -168
- package/test/unit/infrastructure/sanity/DefaultConfigProvider.test.ts +0 -93
- package/test/unit/infrastructure/sanity/FetchQueryExecutor.test.ts +0 -174
- package/test/unit/infrastructure/sanity/SanityClient.test.ts +0 -140
- package/test/unit/lib/__snapshots__/filter.test.ts.snap +0 -5
- package/test/unit/lib/filter.test.ts +0 -1154
- package/test/unit/lib/lastUpdated.test.ts +0 -21
- package/test/unit/lib/query.test.ts +0 -1469
- package/test/unit/lib/sanity/decorators/base.test.ts +0 -368
- package/test/unit/lib/sanity/decorators/navigate-to.test.ts +0 -361
- package/test/unit/lib/sanity/decorators/need-access.test.ts +0 -89
- package/test/unit/lib/sanity/decorators/need-lifetime-upgrade.test.ts +0 -85
- package/test/unit/lib/sanity/decorators/page-type.test.ts +0 -81
- package/test/unit/playerSettings.test.ts +0 -73
- package/test/unit/progress-row/content-card.test.ts +0 -314
- package/test/unit/progress-row/playlist-card.test.ts +0 -104
- package/test/unit/progressRows.test.ts +0 -243
- package/test/unit/recommendations.test.js +0 -47
- package/test/unit/sanityQueryService.test.ts +0 -200
- package/test/unit/search.test.ts +0 -44
- package/test/unit/sentry.test.ts +0 -62
- package/test/unit/services/progress-internal/bubble.test.ts +0 -279
- package/test/unit/services/progress.test.ts +0 -396
- package/test/unit/streakMessage.test.ts +0 -251
- package/test/unit/sync/context.test.ts +0 -51
- package/test/unit/sync/effects/logout-warning.test.ts +0 -158
- package/test/unit/sync/effects/push-failure-notification.test.ts +0 -196
- package/test/unit/sync/errors/sync-errors.test.ts +0 -106
- package/test/unit/sync/errors/validators.test.ts +0 -61
- package/test/unit/sync/helpers/TestModel.ts +0 -44
- package/test/unit/sync/helpers/index.ts +0 -172
- package/test/unit/sync/manager.test.ts +0 -303
- package/test/unit/sync/models/user-award-progress.test.ts +0 -82
- package/test/unit/sync/resolver.test.ts +0 -229
- package/test/unit/sync/retry.test.ts +0 -314
- package/test/unit/sync/run-scope.test.ts +0 -23
- package/test/unit/sync/stale-record-cleanup.test.ts +0 -107
- package/test/unit/sync/store/push-coalescer.test.ts +0 -156
- package/test/unit/sync/store-configs.test.ts +0 -37
- package/test/unit/sync/telemetry/sync-telemetry.test.ts +0 -118
- package/test/unit/sync/utils/event-emitter.test.ts +0 -64
- package/test/unit/sync/utils/throttle.test.ts +0 -245
- package/test/unit/url-builder.test.ts +0 -72
- package/test/unit/user/account.test.ts +0 -66
- 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
|
-
}
|