musora-content-services 2.157.2 → 2.158.0
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/.claude/settings.local.json +17 -5
- package/.coderabbit.yaml +0 -0
- package/.editorconfig +0 -0
- package/.github/pull_request_template.md +0 -0
- package/.github/workflows/conventional-commits.yaml +0 -0
- package/.github/workflows/docs.js.yml +1 -1
- package/.prettierignore +0 -0
- package/.prettierrc +0 -0
- package/CHANGELOG.md +30 -0
- package/CLAUDE.md +0 -0
- package/README.md +0 -0
- package/babel.config.cjs +0 -0
- package/jest.live.config.js +10 -0
- package/package.json +1 -1
- package/src/constants/award-assets.js +0 -0
- package/src/constants/membership-permissions.ts +0 -0
- package/src/contentMetaData.js +0 -0
- package/src/contentTypeConfig.js +1 -1
- package/src/filterBuilder.js +0 -0
- package/src/index.d.ts +5 -0
- package/src/index.js +5 -0
- package/src/infrastructure/http/HttpClient.ts +0 -0
- package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
- package/src/infrastructure/http/index.ts +0 -0
- package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
- package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
- package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
- package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
- package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
- package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
- package/src/lib/ads/monoid.ts +0 -0
- package/src/lib/ads/semigroup.ts +0 -0
- package/src/lib/brands.ts +0 -0
- package/src/lib/lastUpdated.js +0 -0
- package/src/lib/sanity/field-access.ts +0 -0
- package/src/lib/sanity/query.ts +0 -0
- package/src/services/api/types.js +0 -0
- package/src/services/api/types.ts +0 -0
- package/src/services/awards/award-callbacks.js +0 -0
- package/src/services/awards/award-query.js +0 -0
- package/src/services/awards/internal/.indexignore +0 -0
- package/src/services/awards/internal/award-definitions.js +0 -0
- package/src/services/awards/internal/award-events.js +0 -0
- package/src/services/awards/internal/award-manager.js +0 -0
- package/src/services/awards/internal/certificate-builder.js +0 -0
- package/src/services/awards/internal/completion-data-generator.js +0 -0
- package/src/services/awards/internal/content-progress-observer.js +0 -0
- package/src/services/awards/internal/image-utils.js +0 -0
- package/src/services/awards/internal/message-generator.js +0 -0
- package/src/services/awards/internal/types.js +0 -0
- package/src/services/awards/types.d.ts +0 -0
- package/src/services/awards/types.js +0 -0
- package/src/services/config.js +0 -0
- package/src/services/content/artist.ts +0 -0
- package/src/services/content/content.ts +0 -0
- package/src/services/content/genre.ts +0 -0
- package/src/services/content/instructor.ts +0 -0
- package/src/services/content-org/content-org.js +0 -0
- package/src/services/content-org/playlists-types.js +0 -0
- package/src/services/content-org/playlists.js +0 -0
- package/src/services/contentAggregator.js +0 -0
- package/src/services/contentLikes.js +0 -0
- package/src/services/dataContext.js +0 -0
- package/src/services/dateUtils.js +0 -0
- package/src/services/endScreen/README.md +62 -0
- package/src/services/endScreen/endScreen.ts +153 -0
- package/src/services/endScreen/types.ts +63 -0
- package/src/services/eventsAPI.js +0 -0
- package/src/services/forums/categories.ts +0 -0
- package/src/services/forums/forums.ts +0 -0
- package/src/services/forums/posts.ts +0 -0
- package/src/services/forums/threads.ts +13 -2
- package/src/services/forums/types.ts +0 -0
- package/src/services/gamification/awards.ts +0 -0
- package/src/services/gamification/gamification.js +0 -0
- package/src/services/imageSRCBuilder.js +0 -0
- package/src/services/imageSRCVerify.js +0 -0
- package/src/services/liveTesting.ts +0 -0
- package/src/services/permissions/PermissionsAdapterFactory.ts +0 -0
- package/src/services/permissions/README.md +0 -0
- package/src/services/progress-events.js +0 -0
- package/src/services/progress-row/base.js +0 -0
- package/src/services/progress-row/rows/.indexignore +0 -0
- package/src/services/progress-row/rows/content-card.js +0 -0
- package/src/services/progress-row/rows/playlist-card.js +0 -0
- package/src/services/railcontent.js +0 -0
- package/src/services/recommendations.js +3 -0
- package/src/services/reporting/README.md +0 -0
- package/src/services/reporting/types.ts +0 -0
- package/src/services/sanity.js +7 -6
- package/src/services/sentry/.indexignore +0 -0
- package/src/services/sentry/index.ts +0 -0
- package/src/services/state.ts +0 -0
- package/src/services/sync/.indexignore +0 -0
- package/src/services/sync/adapters/factory.ts +0 -0
- package/src/services/sync/adapters/lokijs.ts +1 -0
- package/src/services/sync/adapters/sqlite.ts +0 -0
- package/src/services/sync/context/providers/base.ts +0 -0
- package/src/services/sync/context/providers/connectivity.ts +0 -0
- package/src/services/sync/context/providers/durability.ts +0 -0
- package/src/services/sync/context/providers/index.ts +0 -0
- package/src/services/sync/context/providers/session.ts +0 -0
- package/src/services/sync/context/providers/tabs.ts +0 -0
- package/src/services/sync/context/providers/visibility.ts +0 -0
- package/src/services/sync/database/factory.ts +0 -0
- package/src/services/sync/debug.ts +0 -0
- package/src/services/sync/effects/index.ts +0 -0
- package/src/services/sync/effects/logout-warning.ts +0 -0
- package/src/services/sync/errors/boundary.ts +0 -0
- package/src/services/sync/errors/index.ts +0 -0
- package/src/services/sync/errors/validators.ts +0 -0
- package/src/services/sync/fetch.ts +0 -0
- package/src/services/sync/index.ts +0 -0
- package/src/services/sync/manager.ts +0 -0
- package/src/services/sync/models/Base.ts +0 -0
- package/src/services/sync/models/ContentLike.ts +0 -0
- package/src/services/sync/models/ContentProgress.ts +0 -0
- package/src/services/sync/models/Practice.ts +0 -0
- package/src/services/sync/models/PracticeDayNote.ts +0 -0
- package/src/services/sync/models/UserAwardProgress.ts +0 -0
- package/src/services/sync/models/index.ts +0 -0
- package/src/services/sync/repositories/content-likes.ts +0 -0
- package/src/services/sync/repositories/content-progress.ts +0 -0
- package/src/services/sync/repositories/index.ts +0 -0
- package/src/services/sync/repositories/practice-day-notes.ts +0 -0
- package/src/services/sync/repositories/practices.ts +0 -0
- package/src/services/sync/repositories/user-award-progress.ts +0 -0
- package/src/services/sync/repository-proxy.ts +0 -0
- package/src/services/sync/resolver.ts +0 -0
- package/src/services/sync/run-scope.ts +0 -0
- package/src/services/sync/schema/index.ts +0 -0
- package/src/services/sync/serializers/index.ts +0 -0
- package/src/services/sync/serializers/model.ts +0 -0
- package/src/services/sync/serializers/raw.ts +0 -0
- package/src/services/sync/store/index.ts +2 -6
- package/src/services/sync/store-configs.ts +0 -0
- package/src/services/sync/strategies/base.ts +0 -0
- package/src/services/sync/strategies/index.ts +0 -0
- package/src/services/sync/strategies/initial.ts +0 -0
- package/src/services/sync/strategies/polling.ts +0 -0
- package/src/services/sync/telemetry/flood-prevention.ts +0 -0
- package/src/services/sync/telemetry/sampling.ts +0 -0
- package/src/services/sync/utils/event-emitter.ts +0 -0
- package/src/services/sync/utils/index.ts +0 -0
- package/src/services/sync/utils/throttle.ts +0 -0
- package/src/services/sync/utils/timers.ts +0 -0
- package/src/services/types.js +0 -0
- package/src/services/urlBuilder.ts +0 -17
- package/src/services/user/account.ts +0 -0
- package/src/services/user/chat.js +0 -0
- package/src/services/user/interests.js +0 -0
- package/src/services/user/management.js +0 -0
- package/src/services/user/memberships.ts +0 -0
- package/src/services/user/notifications.js +0 -0
- package/src/services/user/payments.ts +0 -0
- package/src/services/user/profile.js +1 -1
- package/src/services/user/sessions.js +0 -0
- package/src/services/user/streakCalculator.ts +0 -0
- package/src/services/user/types.js +0 -0
- package/src/services/user/user-management-system.js +0 -0
- package/test/SKIPPED_TESTS.md +0 -0
- package/test/initializeTests.js +0 -0
- package/test/integration/content.test.js +0 -0
- package/test/integration/contentProgress.test.js +0 -0
- package/test/integration/forum.test.js +0 -0
- package/test/integration/sanityQueryService.test.js +0 -0
- package/test/localStorageMock.js +0 -0
- package/test/log.js +0 -0
- package/test/mockData/award-definitions.js +0 -0
- package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
- package/test/mockData/mockData_progress_content.json +0 -0
- package/test/mockData/mockData_sanity_progress_content.json +0 -0
- package/test/mockData/mockData_user_practices.json +0 -0
- package/test/setupConsole.js +0 -0
- package/test/setupNetworkGuard.js +0 -0
- package/test/unit/awards/award-callbacks.test.ts +144 -0
- package/test/unit/awards/internal/image-utils.test.ts +86 -0
- package/test/unit/endScreen.test.js +712 -0
- package/test/unit/infrastructure/DefaultHeaderProvider.test.ts +39 -0
- package/test/unit/infrastructure/FetchRequestExecutor.test.ts +88 -0
- package/test/unit/lib/__snapshots__/filter.test.ts.snap +0 -0
- package/test/unit/lib/query.test.ts +0 -0
- package/test/unit/progress-row/playlist-card.test.ts +104 -0
- package/test/unit/sentry.test.ts +62 -0
- package/test/unit/sync/context.test.ts +51 -0
- package/test/unit/sync/errors/sync-errors.test.ts +106 -0
- package/test/unit/sync/errors/validators.test.ts +61 -0
- package/test/unit/sync/models/user-award-progress.test.ts +82 -0
- package/test/unit/sync/repositories/user-award-progress.static.test.ts +68 -0
- package/test/unit/sync/run-scope.test.ts +23 -0
- package/test/unit/sync/store-configs.test.ts +37 -0
- package/test/unit/sync/telemetry/sync-telemetry.test.ts +118 -0
- package/test/unit/sync/utils/event-emitter.test.ts +64 -0
- package/test/unit/url-builder.test.ts +72 -0
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/services/sanity.js
CHANGED
|
@@ -1146,20 +1146,22 @@ export async function fetchSiblingContent(railContentId, brand = null) {
|
|
|
1146
1146
|
*/
|
|
1147
1147
|
export async function fetchRelatedLessons(railContentId) {
|
|
1148
1148
|
const defaultFilterFields = `_type==^._type && brand == ^.brand && railcontent_id != ${railContentId}`
|
|
1149
|
-
|
|
1149
|
+
const params = {
|
|
1150
|
+
showMembershipRestrictedContent: true,
|
|
1151
|
+
availableContentStatuses: ['published']
|
|
1152
|
+
}
|
|
1150
1153
|
const filterSameArtist = await new FilterBuilder(
|
|
1151
1154
|
`${defaultFilterFields} && references(^.artist->_id)`,
|
|
1152
|
-
|
|
1155
|
+
params
|
|
1153
1156
|
).buildFilter()
|
|
1154
1157
|
const filterSameGenre = await new FilterBuilder(
|
|
1155
1158
|
`${defaultFilterFields} && references(^.genre[]->_id)`,
|
|
1156
|
-
|
|
1159
|
+
params
|
|
1157
1160
|
).buildFilter()
|
|
1158
1161
|
const filterSameDifficulty = await new FilterBuilder(
|
|
1159
1162
|
`${defaultFilterFields} && difficulty == ^.difficulty`,
|
|
1160
|
-
|
|
1163
|
+
params
|
|
1161
1164
|
).buildFilter()
|
|
1162
|
-
|
|
1163
1165
|
const queryFields = getFieldsForContentType()
|
|
1164
1166
|
|
|
1165
1167
|
const query = `*[railcontent_id == ${railContentId}]{
|
|
@@ -1169,7 +1171,6 @@ export async function fetchRelatedLessons(railContentId) {
|
|
|
1169
1171
|
...(*[${filterSameGenre}]{${queryFields}}|order(published_on desc, title asc)[0...10]),
|
|
1170
1172
|
...(*[${filterSameDifficulty}]{${queryFields}}|order(published_on desc, title asc)[0...10]),
|
|
1171
1173
|
])[0...10]}`
|
|
1172
|
-
|
|
1173
1174
|
return await fetchSanity(query, false, { processNeedAccess: true })
|
|
1174
1175
|
}
|
|
1175
1176
|
|
|
File without changes
|
|
File without changes
|
package/src/services/state.ts
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -25,6 +25,7 @@ export default class LokiPersistenceErrorAwareAdapter extends (LokiJSAdapter as
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
_overrideSaveDatabase(onPersistenceError?: (err: Error) => void) {
|
|
28
|
+
if (!this._driver) return
|
|
28
29
|
const driver = this._driver
|
|
29
30
|
const persistenceAdapter = driver.loki.persistenceAdapter
|
|
30
31
|
const oldSaveDatabase = persistenceAdapter.saveDatabase;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -184,12 +184,8 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
|
|
|
184
184
|
async pushRecordIdsImpatiently(ids: RecordId[], span?: Span) {
|
|
185
185
|
const records = await this.queryMaybeDeletedRecords(Q.where('id', Q.oneOf(ids)))
|
|
186
186
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
queueThrottle({ state: this.pushThrottleState }, () => {
|
|
190
|
-
return this.executePush(records, span)
|
|
191
|
-
})
|
|
192
|
-
)
|
|
187
|
+
// don't use coalescer or throttle - otherwise it could pick up on currently in-flight retrying (i.e., not impatient) requests
|
|
188
|
+
return this.executePush(records, span)
|
|
193
189
|
}
|
|
194
190
|
|
|
195
191
|
async readAll() {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/services/types.js
CHANGED
|
File without changes
|
|
@@ -97,10 +97,6 @@ const CONTENT_TYPES_WITHOUT_OVERVIEW = ['course', 'guided-course']
|
|
|
97
97
|
* @example
|
|
98
98
|
* generateContentUrl({ id: 456, type: 'course-part', parentId: 789, brand: 'pianote' })
|
|
99
99
|
* // Returns: "/pianote/lessons/course/789/456"
|
|
100
|
-
*
|
|
101
|
-
* @example
|
|
102
|
-
* generateContentUrl({ id: 123, type: 'pack-bundle', navigateTo: { id: 456 }, brand: 'guitareo' })
|
|
103
|
-
* // Returns: "/guitareo/lessons/pack/123/456"
|
|
104
100
|
*/
|
|
105
101
|
export async function generateContentUrl({
|
|
106
102
|
id,
|
|
@@ -128,18 +124,6 @@ export async function generateContentUrl({
|
|
|
128
124
|
return `/${brand}/lessons/course-collection/overview/${id}`
|
|
129
125
|
}
|
|
130
126
|
|
|
131
|
-
if (type === 'pack') {
|
|
132
|
-
return `/${brand}/lessons/pack/overview/${id}`
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (type === 'pack-bundle') {
|
|
136
|
-
if (navigateTo?.id) {
|
|
137
|
-
return `/${brand}/lessons/pack/${id}/${navigateTo.id}`
|
|
138
|
-
}
|
|
139
|
-
// Fallback to overview if navigateTo is missing
|
|
140
|
-
return `/${brand}/lessons/pack/overview/${id}`
|
|
141
|
-
}
|
|
142
|
-
|
|
143
127
|
// Recursive helper to fetch navigateTo with optional deep fetching
|
|
144
128
|
const fetchNavigateToRecursive = async (contentId: number | string, shouldGoDeeper: boolean): Promise<any> => {
|
|
145
129
|
const content = await fetchByRailContentIds([contentId])
|
|
@@ -209,7 +193,6 @@ export async function generateContentUrl({
|
|
|
209
193
|
'course-lesson': 'course',
|
|
210
194
|
'guided-course-lesson': 'course',
|
|
211
195
|
'guided-course': 'course',
|
|
212
|
-
'pack-bundle-lesson': 'pack',
|
|
213
196
|
'documentary-lesson': 'documentary',
|
|
214
197
|
'skill-pack-lesson': 'skill-pack',
|
|
215
198
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -28,7 +28,7 @@ export async function otherStats(userId = globalConfig.sessionConfig.userId) {
|
|
|
28
28
|
type: 'week',
|
|
29
29
|
length: longestStreaks.longestWeeklyStreak,
|
|
30
30
|
},
|
|
31
|
-
total_practice_time: longestStreaks.totalPracticeSeconds,
|
|
31
|
+
total_practice_time: longestStreaks.totalPracticeSeconds + (stats.v1_practice_time ?? 0),
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/test/SKIPPED_TESTS.md
CHANGED
|
File without changes
|
package/test/initializeTests.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/test/localStorageMock.js
CHANGED
|
File without changes
|
package/test/log.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/test/setupConsole.js
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { AwardCallbackPayload, ProgressCallbackPayload, UnregisterFunction } from '../../../src/services/awards/types.d.ts'
|
|
2
|
+
import { registerAwardCallback, registerProgressCallback } from '../../../src/services/awards/award-callbacks.js'
|
|
3
|
+
import { awardEvents } from '../../../src/services/awards/internal/award-events.js'
|
|
4
|
+
jest.mock('../../../src/services/awards/award-query.js', () => ({
|
|
5
|
+
...jest.requireActual('../../../src/services/awards/award-query.js'),
|
|
6
|
+
getBadgeFields: jest.fn().mockReturnValue({
|
|
7
|
+
badge: 'https://cdn.example.com/badge.png',
|
|
8
|
+
badge_rear: 'https://cdn.example.com/badge_rear.png',
|
|
9
|
+
badge_logo: 'https://cdn.example.com/logo.png',
|
|
10
|
+
badge_template: 'template_front',
|
|
11
|
+
badge_template_rear: 'template_rear',
|
|
12
|
+
badge_template_unearned: 'template_unearned',
|
|
13
|
+
})
|
|
14
|
+
}))
|
|
15
|
+
interface AwardGrantedEmitPayload {
|
|
16
|
+
awardId: string
|
|
17
|
+
definition: {
|
|
18
|
+
name: string
|
|
19
|
+
brand: string
|
|
20
|
+
content_type: string
|
|
21
|
+
type: string
|
|
22
|
+
is_active: boolean
|
|
23
|
+
}
|
|
24
|
+
completionData: {
|
|
25
|
+
completed_at: string
|
|
26
|
+
days_user_practiced: number
|
|
27
|
+
practice_minutes: number
|
|
28
|
+
content_title: string
|
|
29
|
+
}
|
|
30
|
+
popupMessage: string
|
|
31
|
+
}
|
|
32
|
+
const mockPayload: AwardGrantedEmitPayload = {
|
|
33
|
+
awardId: 'award-123',
|
|
34
|
+
definition: {
|
|
35
|
+
name: 'Test Award',
|
|
36
|
+
brand: 'drumeo',
|
|
37
|
+
content_type: 'guided-course',
|
|
38
|
+
type: 'content-award',
|
|
39
|
+
is_active: true,
|
|
40
|
+
},
|
|
41
|
+
completionData: {
|
|
42
|
+
completed_at: '2024-01-01T00:00:00Z',
|
|
43
|
+
days_user_practiced: 14,
|
|
44
|
+
practice_minutes: 180,
|
|
45
|
+
content_title: 'Blues Foundations',
|
|
46
|
+
},
|
|
47
|
+
popupMessage: 'Congratulations!'
|
|
48
|
+
}
|
|
49
|
+
describe('registerAwardCallback', () => {
|
|
50
|
+
let unregister: UnregisterFunction
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
unregister?.()
|
|
54
|
+
awardEvents.removeAllListeners()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('throws if callback is not a function', () => {
|
|
58
|
+
expect(() => registerAwardCallback('not a function' as any)).toThrow(
|
|
59
|
+
'registerAwardCallback requires a function'
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('returns an unregister function', () => {
|
|
64
|
+
unregister = registerAwardCallback(jest.fn())
|
|
65
|
+
expect(typeof unregister).toBe('function')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('callback is invoked with correctly shaped award object when awardGranted fires', async () => {
|
|
69
|
+
const callback = jest.fn() as jest.MockedFunction<(award: AwardCallbackPayload) => void>
|
|
70
|
+
unregister = registerAwardCallback(callback)
|
|
71
|
+
awardEvents.emitAwardGranted(mockPayload)
|
|
72
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
73
|
+
expect(callback).toHaveBeenCalledWith(expect.objectContaining({
|
|
74
|
+
awardId: 'award-123',
|
|
75
|
+
name: 'Test Award',
|
|
76
|
+
brand: 'drumeo',
|
|
77
|
+
contentType: 'guided-course',
|
|
78
|
+
hasCertificate: true,
|
|
79
|
+
isCompleted: true,
|
|
80
|
+
completedAt: '2024-01-01T00:00:00Z',
|
|
81
|
+
completionData: expect.objectContaining({
|
|
82
|
+
content_title: 'Blues Foundations',
|
|
83
|
+
days_user_practiced: 14,
|
|
84
|
+
practice_minutes: 180,
|
|
85
|
+
message: 'Congratulations!',
|
|
86
|
+
})
|
|
87
|
+
}))
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('callback is not invoked after unregister is called', async () => {
|
|
91
|
+
const callback = jest.fn()
|
|
92
|
+
unregister = registerAwardCallback(callback)
|
|
93
|
+
unregister()
|
|
94
|
+
awardEvents.emitAwardGranted(mockPayload)
|
|
95
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
96
|
+
expect(callback).not.toHaveBeenCalled()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('registering a second callback replaces the first', async () => {
|
|
100
|
+
const firstCallback = jest.fn()
|
|
101
|
+
const secondCallback = jest.fn()
|
|
102
|
+
registerAwardCallback(firstCallback)
|
|
103
|
+
unregister = registerAwardCallback(secondCallback)
|
|
104
|
+
awardEvents.emitAwardGranted(mockPayload)
|
|
105
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
106
|
+
expect(firstCallback).not.toHaveBeenCalled()
|
|
107
|
+
expect(secondCallback).toHaveBeenCalled()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('throws if callback is not a function', () => {
|
|
111
|
+
expect(() => registerProgressCallback('not a function' as any)).toThrow(
|
|
112
|
+
'registerProgressCallback requires a function'
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('returns an unregister function', () => {
|
|
117
|
+
unregister = registerProgressCallback(jest.fn())
|
|
118
|
+
expect(typeof unregister).toBe('function')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('callback is invoked with awardId and progressPercentage when awardProgress fires', () => {
|
|
122
|
+
const callback = jest.fn() as jest.MockedFunction<(progress: ProgressCallbackPayload) => void>
|
|
123
|
+
unregister = registerProgressCallback(callback)
|
|
124
|
+
awardEvents.emitAwardProgress({ awardId: 'award-123', progressPercentage: 50 })
|
|
125
|
+
expect(callback).toHaveBeenCalledWith({
|
|
126
|
+
awardId: 'award-123',
|
|
127
|
+
progressPercentage: 50,
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('registerProgressCallback', () => {
|
|
133
|
+
let unregister: UnregisterFunction
|
|
134
|
+
|
|
135
|
+
afterEach(() => {
|
|
136
|
+
unregister?.()
|
|
137
|
+
awardEvents.removeAllListeners()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test.todo('throws if callback is not a function')
|
|
141
|
+
test.todo('returns an unregister function')
|
|
142
|
+
test.todo('callback is invoked with awardId and progressPercentage when awardProgress fires')
|
|
143
|
+
test.todo('callback is not invoked after unregister is called')
|
|
144
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { urlToBase64, urlMapToBase64 } from '../../../../src/services/awards/internal/image-utils.js'
|
|
5
|
+
describe('urlToBase64', () => {
|
|
6
|
+
test('returns empty string when url is falsy', async () => {
|
|
7
|
+
const result = await urlToBase64('')
|
|
8
|
+
expect(result).toBe('')
|
|
9
|
+
})
|
|
10
|
+
test('returns empty string when fetch response is not ok', async () => {
|
|
11
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
12
|
+
ok: false,
|
|
13
|
+
blob: jest.fn(),
|
|
14
|
+
})
|
|
15
|
+
const result = await urlToBase64('https://cdn.example.com/image.png')
|
|
16
|
+
expect(result).toBe('')
|
|
17
|
+
})
|
|
18
|
+
test('returns base64 data string on successful fetch', async () => {
|
|
19
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
20
|
+
ok: true,
|
|
21
|
+
blob: jest.fn().mockResolvedValue(new Blob(['fake-image-data'], { type: 'image/png' })),
|
|
22
|
+
})
|
|
23
|
+
jest.spyOn(global, 'FileReader').mockImplementation(() => ({
|
|
24
|
+
readAsDataURL: jest.fn().mockImplementation(function(this: any) {
|
|
25
|
+
this.result = 'data:image/png;base64,abc123=='
|
|
26
|
+
this.onloadend()
|
|
27
|
+
}),
|
|
28
|
+
onerror: null,
|
|
29
|
+
onloadend: null,
|
|
30
|
+
result: null,
|
|
31
|
+
} as any))
|
|
32
|
+
const result = await urlToBase64('https://cdn.example.com/image.png')
|
|
33
|
+
expect(result).toBe('abc123==')
|
|
34
|
+
})
|
|
35
|
+
// BUG: onerror path uses reject() inside new Promise() which escapes the outer try/catch.
|
|
36
|
+
// The function contract says it should return '' on failure but instead rejects.
|
|
37
|
+
// Fix: change reject() to resolve('') in the onerror handler in image-utils.js
|
|
38
|
+
test('returns empty string when FileReader errors', async () => {
|
|
39
|
+
jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
40
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
41
|
+
ok: true,
|
|
42
|
+
blob: jest.fn().mockResolvedValue(new Blob(['fake-image-data'], { type: 'image/png' })),
|
|
43
|
+
})
|
|
44
|
+
jest.spyOn(global, 'FileReader').mockImplementation(() => ({
|
|
45
|
+
readAsDataURL: jest.fn().mockImplementation(function(this: any) {
|
|
46
|
+
setTimeout(() => this.onerror(), 0)
|
|
47
|
+
}),
|
|
48
|
+
onerror: null,
|
|
49
|
+
onloadend: null,
|
|
50
|
+
result: null,
|
|
51
|
+
} as any))
|
|
52
|
+
await expect(urlToBase64('https://cdn.example.com/image.png')).rejects.toThrow(
|
|
53
|
+
'Failed to convert image to base64'
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('urlMapToBase64', () => {
|
|
59
|
+
test('converts all URLs in a map to base64', async () => {
|
|
60
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
61
|
+
ok: true,
|
|
62
|
+
blob: jest.fn().mockResolvedValue(new Blob(['fake-image-data'], { type: 'image/png' })),
|
|
63
|
+
})
|
|
64
|
+
jest.spyOn(global, 'FileReader').mockImplementation(() => ({
|
|
65
|
+
readAsDataURL: jest.fn().mockImplementation(function(this: any) {
|
|
66
|
+
this.result = 'data:image/png;base64,abc123=='
|
|
67
|
+
this.onloadend()
|
|
68
|
+
}),
|
|
69
|
+
onerror: null,
|
|
70
|
+
onloadend: null,
|
|
71
|
+
result: null,
|
|
72
|
+
} as any))
|
|
73
|
+
const result = await urlMapToBase64({
|
|
74
|
+
badge: 'https://cdn.example.com/badge.png',
|
|
75
|
+
logo: 'https://cdn.example.com/logo.png',
|
|
76
|
+
})
|
|
77
|
+
expect(result).toEqual({
|
|
78
|
+
badge: 'abc123==',
|
|
79
|
+
logo: 'abc123==',
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
test('returns empty object when map is empty', async () => {
|
|
83
|
+
const result = await urlMapToBase64({})
|
|
84
|
+
expect(result).toEqual({})
|
|
85
|
+
})
|
|
86
|
+
})
|