@tolgee/core 4.7.0 → 4.7.2

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 (97) hide show
  1. package/dist/tolgee.cjs.js +2 -4
  2. package/dist/tolgee.cjs.js.map +1 -1
  3. package/dist/tolgee.cjs.min.js +1 -1
  4. package/dist/tolgee.cjs.min.js.map +1 -1
  5. package/dist/{tolgee.esm.js → tolgee.esm.min.mjs} +2 -2
  6. package/dist/tolgee.esm.min.mjs.map +1 -0
  7. package/dist/tolgee.esm.mjs +5690 -0
  8. package/dist/tolgee.esm.mjs.map +1 -0
  9. package/dist/tolgee.umd.js +2 -4
  10. package/dist/tolgee.umd.js.map +1 -1
  11. package/dist/tolgee.umd.min.js +1 -1
  12. package/dist/tolgee.umd.min.js.map +1 -1
  13. package/package.json +10 -9
  14. package/src/Constants/Global.ts +9 -0
  15. package/src/Constants/ModifierKey.ts +6 -0
  16. package/src/Errors/ApiHttpError.ts +8 -0
  17. package/src/Observer.test.ts +119 -0
  18. package/src/Observer.ts +68 -0
  19. package/src/Properties.test.ts +150 -0
  20. package/src/Properties.ts +112 -0
  21. package/src/Tolgee.test.ts +473 -0
  22. package/src/Tolgee.ts +335 -0
  23. package/src/TolgeeConfig.test.ts +21 -0
  24. package/src/TolgeeConfig.ts +134 -0
  25. package/src/__integration/FormatterIcu.test.ts +80 -0
  26. package/src/__integration/FormatterMissing.ts +54 -0
  27. package/src/__integration/Tolgee.test.ts +90 -0
  28. package/src/__integration/TolgeeInvisible.test.ts +145 -0
  29. package/src/__integration/mockTranslations.ts +6 -0
  30. package/src/__integration/testConfig.ts +16 -0
  31. package/src/__testFixtures/classMock.ts +11 -0
  32. package/src/__testFixtures/createElement.ts +43 -0
  33. package/src/__testFixtures/createTestDom.ts +25 -0
  34. package/src/__testFixtures/mocked.ts +25 -0
  35. package/src/__testFixtures/setupAfterEnv.ts +34 -0
  36. package/src/helpers/NodeHelper.ts +90 -0
  37. package/src/helpers/TextHelper.test.ts +62 -0
  38. package/src/helpers/TextHelper.ts +58 -0
  39. package/src/helpers/commonTypes.ts +8 -0
  40. package/src/helpers/encoderPolyfill.ts +96 -0
  41. package/src/helpers/secret.test.ts +61 -0
  42. package/src/helpers/secret.ts +68 -0
  43. package/src/helpers/sleep.ts +2 -0
  44. package/src/highlighter/HighlightFunctionsInitializer.test.ts +40 -0
  45. package/src/highlighter/HighlightFunctionsInitializer.ts +61 -0
  46. package/src/highlighter/MouseEventHandler.test.ts +151 -0
  47. package/src/highlighter/MouseEventHandler.ts +191 -0
  48. package/src/highlighter/TranslationHighlighter.test.ts +177 -0
  49. package/src/highlighter/TranslationHighlighter.ts +113 -0
  50. package/src/index.ts +10 -0
  51. package/src/internal.ts +2 -0
  52. package/src/modules/IcuFormatter.ts +17 -0
  53. package/src/modules/index.ts +1 -0
  54. package/src/services/ApiHttpService.ts +85 -0
  55. package/src/services/CoreService.test.ts +142 -0
  56. package/src/services/CoreService.ts +76 -0
  57. package/src/services/DependencyService.test.ts +51 -0
  58. package/src/services/DependencyService.ts +116 -0
  59. package/src/services/ElementRegistrar.test.ts +131 -0
  60. package/src/services/ElementRegistrar.ts +108 -0
  61. package/src/services/EventEmitter.ts +52 -0
  62. package/src/services/EventService.ts +14 -0
  63. package/src/services/ModuleService.ts +14 -0
  64. package/src/services/ScreenshotService.ts +31 -0
  65. package/src/services/Subscription.ts +7 -0
  66. package/src/services/TextService.test.ts +88 -0
  67. package/src/services/TextService.ts +82 -0
  68. package/src/services/TranslationService.test.ts +358 -0
  69. package/src/services/TranslationService.ts +417 -0
  70. package/src/services/__mocks__/CoreService.ts +17 -0
  71. package/src/toolsManager/Messages.test.ts +79 -0
  72. package/src/toolsManager/Messages.ts +60 -0
  73. package/src/toolsManager/PluginManager.test.ts +108 -0
  74. package/src/toolsManager/PluginManager.ts +129 -0
  75. package/src/types/DTOs.ts +25 -0
  76. package/src/types/apiSchema.generated.ts +6208 -0
  77. package/src/types.ts +146 -0
  78. package/src/wrappers/AbstractWrapper.ts +14 -0
  79. package/src/wrappers/NodeHandler.ts +143 -0
  80. package/src/wrappers/WrappedHandler.ts +28 -0
  81. package/src/wrappers/invisible/AttributeHandler.ts +23 -0
  82. package/src/wrappers/invisible/Coder.ts +65 -0
  83. package/src/wrappers/invisible/ContentHandler.ts +15 -0
  84. package/src/wrappers/invisible/CoreHandler.ts +17 -0
  85. package/src/wrappers/invisible/InvisibleWrapper.ts +59 -0
  86. package/src/wrappers/invisible/ValueMemory.test.ts +25 -0
  87. package/src/wrappers/invisible/ValueMemory.ts +16 -0
  88. package/src/wrappers/text/AttributeHandler.test.ts +117 -0
  89. package/src/wrappers/text/AttributeHandler.ts +25 -0
  90. package/src/wrappers/text/Coder.test.ts +298 -0
  91. package/src/wrappers/text/Coder.ts +202 -0
  92. package/src/wrappers/text/ContentHandler.test.ts +185 -0
  93. package/src/wrappers/text/ContentHandler.ts +21 -0
  94. package/src/wrappers/text/CoreHandler.test.ts +106 -0
  95. package/src/wrappers/text/CoreHandler.ts +45 -0
  96. package/src/wrappers/text/TextWrapper.ts +69 -0
  97. package/dist/tolgee.esm.js.map +0 -1
@@ -0,0 +1,417 @@
1
+ import { Translations, TreeTranslationsData } from '../types';
2
+ import { Properties } from '../Properties';
3
+ import { CoreService } from './CoreService';
4
+ import { ApiHttpService } from './ApiHttpService';
5
+ import { ApiHttpError } from '../Errors/ApiHttpError';
6
+ import { EventService } from './EventService';
7
+ import { EventEmitterImpl } from './EventEmitter';
8
+ import {
9
+ ComplexEditKeyDto,
10
+ CreateKeyDto,
11
+ KeyWithDataModel,
12
+ KeyWithTranslationsModel,
13
+ SetTranslationsResponseModel,
14
+ SetTranslationsWithKeyDto,
15
+ TranslationData,
16
+ } from '../types/DTOs';
17
+
18
+ interface TranslationInterface {
19
+ text?: string;
20
+ }
21
+
22
+ interface Key {
23
+ id?: number;
24
+ name: string;
25
+ translations: Record<string, TranslationInterface>;
26
+ }
27
+
28
+ export class TranslationService {
29
+ private translationsCache: Map<string, Translations> = new Map<
30
+ string,
31
+ Translations
32
+ >();
33
+ private fetchPromises: { [key: string]: Promise<any> } = {};
34
+
35
+ // we need to distinguish which languages are in cache initially
36
+ // because we need to refetch them in dev mode
37
+ private fetchedDev: { [key: string]: boolean } = {};
38
+
39
+ constructor(
40
+ private properties: Properties,
41
+ private coreService: CoreService,
42
+ private apiHttpService: ApiHttpService,
43
+ private eventService: EventService
44
+ ) {}
45
+
46
+ private static translationByValue(message: string, defaultValue?: string) {
47
+ if (message) {
48
+ return message;
49
+ }
50
+
51
+ if (defaultValue) {
52
+ return defaultValue;
53
+ }
54
+
55
+ return undefined;
56
+ }
57
+
58
+ initStatic() {
59
+ if (typeof this.properties.config?.staticData === 'object') {
60
+ Object.entries(this.properties.config.staticData).forEach(
61
+ ([language, data]) => {
62
+ //if not provider or promise then it is raw data
63
+ if (typeof data !== 'function') {
64
+ this.setLanguageData(language, data);
65
+ }
66
+ }
67
+ );
68
+ }
69
+ }
70
+
71
+ getCachedTranslations() {
72
+ return this.translationsCache;
73
+ }
74
+
75
+ updateTranslationInCache = async (data: Key) => {
76
+ const result: Record<string, string> = {};
77
+ Object.entries(data.translations).forEach(([lang, translation]) => {
78
+ const cachedData = this.translationsCache.get(lang);
79
+ if (cachedData) {
80
+ cachedData[data.name] = translation.text;
81
+ }
82
+ result[lang] = translation.text;
83
+ });
84
+ await (
85
+ this.eventService.TRANSLATION_CHANGED as EventEmitterImpl<TranslationData>
86
+ ).emit(new TranslationData(data.name, result, data.id));
87
+ };
88
+
89
+ async loadTranslations(lang: string = this.properties.currentLanguage) {
90
+ if (this.isFetchNeeded(lang)) {
91
+ if (!(this.fetchPromises[lang] instanceof Promise)) {
92
+ this.fetchPromises[lang] = this.fetchTranslations(lang);
93
+ }
94
+ await this.fetchPromises[lang];
95
+ (this.eventService.LANGUAGE_LOADED as EventEmitterImpl<string>).emit(
96
+ lang
97
+ );
98
+ }
99
+ this.fetchPromises[lang] = undefined;
100
+ return this.translationsCache.get(lang);
101
+ }
102
+
103
+ async getTranslation(
104
+ key: string,
105
+ lang: string = this.properties.currentLanguage,
106
+ defaultValue?: string
107
+ ): Promise<string> {
108
+ if (this.isFetchNeeded(lang)) {
109
+ await this.loadTranslations(lang);
110
+ }
111
+ let message = this.getFromCache(key, lang);
112
+
113
+ if (!message) {
114
+ // try to get translation from fallback language
115
+ const fallbackLang = this.properties.config.fallbackLanguage;
116
+ if (this.isFetchNeeded(fallbackLang)) {
117
+ await this.loadTranslations(this.properties.config.fallbackLanguage);
118
+ }
119
+ message = this.getFromCache(key, this.properties.config.fallbackLanguage);
120
+ }
121
+
122
+ return TranslationService.translationByValue(message, defaultValue);
123
+ }
124
+
125
+ async updateKeyComplex(
126
+ id: number,
127
+ data: ComplexEditKeyDto
128
+ ): Promise<KeyWithDataModel> {
129
+ this.coreService.checkScope('translations.edit');
130
+ const result = (await this.apiHttpService.postJson(
131
+ `v2/projects/keys/${id}/complex-update`,
132
+ {
133
+ ...data,
134
+ screenshotUploadedImageIds: data.screenshotUploadedImageIds?.length
135
+ ? data.screenshotUploadedImageIds
136
+ : undefined,
137
+ screenshotIdsToDelete: data.screenshotIdsToDelete?.length
138
+ ? data.screenshotIdsToDelete
139
+ : undefined,
140
+ },
141
+ { method: 'put' }
142
+ )) as KeyWithDataModel;
143
+
144
+ await this.updateTranslationInCache(result);
145
+
146
+ return result;
147
+ }
148
+
149
+ async createKey(data: CreateKeyDto): Promise<KeyWithDataModel> {
150
+ this.coreService.checkScope('keys.edit');
151
+ const result = (await this.apiHttpService.postJson(
152
+ `v2/projects/keys/create`,
153
+ {
154
+ ...data,
155
+ screenshotUploadedImageIds: data.screenshotUploadedImageIds?.length
156
+ ? data.screenshotUploadedImageIds
157
+ : undefined,
158
+ }
159
+ )) as KeyWithDataModel;
160
+
161
+ await this.updateTranslationInCache(result);
162
+
163
+ return result;
164
+ }
165
+
166
+ async setTranslations(
167
+ translationData: SetTranslationsWithKeyDto
168
+ ): Promise<SetTranslationsResponseModel> {
169
+ this.coreService.checkScope('translations.edit');
170
+ const result = (await this.apiHttpService.postJson(
171
+ 'v2/projects/translations',
172
+ translationData
173
+ )) as SetTranslationsResponseModel;
174
+
175
+ await this.updateTranslationInCache({
176
+ id: result.keyId,
177
+ name: result.keyName,
178
+ translations: result.translations,
179
+ });
180
+
181
+ return result;
182
+ }
183
+
184
+ /**
185
+ * Change translations of some keys to some value temporarily.
186
+ * For screenshot taking with provided values, before actually saving
187
+ * the values
188
+ *
189
+ * @return Returns callback changing affected translations back
190
+ */
191
+ async changeTranslations({
192
+ key,
193
+ translations,
194
+ }: TranslationData): Promise<() => void> {
195
+ const old: Record<string, string> = {};
196
+
197
+ Object.entries(translations).forEach(([language, value]) => {
198
+ const data = this.translationsCache.get(language);
199
+ if (data) {
200
+ old[language] = data[key];
201
+ data[key] = value;
202
+ }
203
+ });
204
+
205
+ await (
206
+ this.eventService.TRANSLATION_CHANGED as EventEmitterImpl<TranslationData>
207
+ ).emit({
208
+ key,
209
+ translations,
210
+ });
211
+
212
+ // callback to revert the operation
213
+ return async () => {
214
+ Object.entries(old).forEach(([language, value]) => {
215
+ const data = this.translationsCache.get(language);
216
+ if (data) {
217
+ data[key] = value;
218
+ }
219
+ });
220
+ await (
221
+ this.eventService
222
+ .TRANSLATION_CHANGED as EventEmitterImpl<TranslationData>
223
+ ).emit({
224
+ key,
225
+ translations: old,
226
+ });
227
+ };
228
+ }
229
+
230
+ getFromCacheOrFallback(
231
+ key: string,
232
+ lang: string = this.properties.currentLanguage,
233
+ defaultValue?: string
234
+ ): string {
235
+ const fallbackLang = this.properties.config.fallbackLanguage;
236
+ const message =
237
+ this.getFromCache(key, lang) || this.getFromCache(key, fallbackLang);
238
+
239
+ if (!message && (!this.isLoaded(lang) || !this.isLoaded(fallbackLang))) {
240
+ return undefined;
241
+ }
242
+ return TranslationService.translationByValue(message, defaultValue);
243
+ }
244
+
245
+ getTranslationsOfKey = async (
246
+ key: string,
247
+ languages: Set<string> = new Set([this.properties.currentLanguage])
248
+ ) => {
249
+ this.coreService.checkScope('translations.view');
250
+ try {
251
+ const languagesArray = [...languages];
252
+ const languagesQuery = languagesArray
253
+ .map((l) => `languages=${l}`)
254
+ .join('&');
255
+ const data = await this.apiHttpService.fetchJson(
256
+ `v2/projects/translations?${languagesQuery}&filterKeyName=${encodeURIComponent(
257
+ key
258
+ )}`
259
+ );
260
+
261
+ const translationData = languagesArray.reduce(
262
+ (acc, curr) => ({ ...acc, [curr]: '' }),
263
+ {}
264
+ );
265
+
266
+ const firstItem = data._embedded?.keys?.[0] as
267
+ | KeyWithTranslationsModel
268
+ | undefined;
269
+
270
+ if (firstItem?.translations) {
271
+ Object.entries(firstItem.translations).forEach(
272
+ ([language, translation]) =>
273
+ (translationData[language] = (translation as any).text)
274
+ );
275
+ }
276
+
277
+ const langs = data.selectedLanguages?.map((l) => l.tag) as string[];
278
+
279
+ return [firstItem, langs] as const;
280
+ } catch (e) {
281
+ if (
282
+ e instanceof ApiHttpError &&
283
+ e.response.status === 404 &&
284
+ e.code === 'language_not_found'
285
+ ) {
286
+ // only possible reason for this error is, that languages definition
287
+ // is changed, but the old value is stored in preferred languages
288
+ this.properties.preferredLanguages =
289
+ await this.coreService.getLanguages();
290
+ // eslint-disable-next-line no-console
291
+ console.error('Requested language not found, refreshing the page!');
292
+ location.reload();
293
+ } else {
294
+ throw e;
295
+ }
296
+ }
297
+ };
298
+
299
+ private isFetchNeeded(lang: string) {
300
+ const isDevMode = this.properties.mode === 'development';
301
+ const dataPresent = this.translationsCache.get(lang) !== undefined;
302
+ const devFetched = Boolean(this.fetchedDev[lang]);
303
+ return (isDevMode && !devFetched) || !dataPresent;
304
+ }
305
+
306
+ private isLoaded(lang: string) {
307
+ return this.translationsCache.get(lang) !== undefined;
308
+ }
309
+
310
+ private async fetchTranslations(lang: string) {
311
+ const isDevMode = this.properties.mode === 'development';
312
+ if (isDevMode) {
313
+ return await this.fetchTranslationsDevelopment(lang);
314
+ } else {
315
+ return await this.fetchTranslationsProduction(lang);
316
+ }
317
+ }
318
+
319
+ private async fetchTranslationsProduction(language: string) {
320
+ const langStaticData = this.properties.config?.staticData?.[language];
321
+
322
+ if (typeof langStaticData === 'function') {
323
+ const data = await langStaticData();
324
+ this.setLanguageData(language, data);
325
+ return;
326
+ } else if (langStaticData !== undefined) {
327
+ this.setLanguageData(language, langStaticData);
328
+ return;
329
+ }
330
+
331
+ const url = `${
332
+ this.properties.config.filesUrlPrefix || '/'
333
+ }${language}.json`;
334
+ try {
335
+ const result = await fetch(url);
336
+ if (result.status >= 400) {
337
+ //on error set language data as empty object to not break the flow
338
+ // eslint-disable-next-line no-console
339
+ console.error(
340
+ 'Server responded with error status while loading localization data.'
341
+ );
342
+ this.setLanguageData(language, {});
343
+ return;
344
+ }
345
+ try {
346
+ const data = await result.json();
347
+ this.setLanguageData(language, data);
348
+ } catch (e) {
349
+ // eslint-disable-next-line no-console
350
+ console.error(`Error parsing json retrieved from ${url}.`);
351
+ this.setEmptyLanguageData(language);
352
+ }
353
+ } catch (e) {
354
+ // eslint-disable-next-line no-console
355
+ console.error(`Error fetching localization data from ${url}.`);
356
+ this.setEmptyLanguageData(language);
357
+ }
358
+ }
359
+
360
+ private async fetchTranslationsDevelopment(language: string) {
361
+ await this.coreService.loadApiKeyDetails();
362
+ this.coreService.checkScope('translations.view');
363
+ try {
364
+ const data = await this.apiHttpService.fetchJson(
365
+ `v2/projects/translations/${language}`
366
+ );
367
+ this.fetchedDev[language] = true;
368
+ this.setLanguageData(language, data[language] || {});
369
+ } catch (e) {
370
+ // eslint-disable-next-line no-console
371
+ console.error('Error while fetching localization data from API.', e);
372
+ this.setEmptyLanguageData(language);
373
+ return;
374
+ }
375
+ }
376
+
377
+ private setEmptyLanguageData(language: string) {
378
+ this.translationsCache.set(language, {});
379
+ }
380
+
381
+ private setLanguageData(language: string, data: TreeTranslationsData) {
382
+ // recursively walk the tree and make it flat, when tree data are provided
383
+ const makeFlat = (data: TreeTranslationsData): Record<string, string> => {
384
+ const result: Record<string, string> = {};
385
+ Object.entries(data).forEach(([key, value]) => {
386
+ // ignore falsy values
387
+ if (!value) {
388
+ return;
389
+ }
390
+ if (typeof value === 'object') {
391
+ Object.entries(makeFlat(value)).forEach(([flatKey, flatValue]) => {
392
+ result[key + '.' + flatKey] = flatValue;
393
+ });
394
+ return;
395
+ }
396
+ result[key] = value as string;
397
+ });
398
+ return result;
399
+ };
400
+
401
+ this.translationsCache.set(language, makeFlat(data));
402
+ }
403
+
404
+ private getFromCache(
405
+ key: string,
406
+ lang: string = this.properties.currentLanguage
407
+ ): string {
408
+ const root: string | Translations = this.translationsCache.get(lang);
409
+
410
+ //if lang is not downloaded or does not exist at all
411
+ if (root === undefined) {
412
+ return undefined;
413
+ }
414
+
415
+ return root[key] as string;
416
+ }
417
+ }
@@ -0,0 +1,17 @@
1
+ import * as Module from '../CoreService';
2
+ import classMock from '@testFixtures/classMock';
3
+ import { Scope } from '../../types';
4
+
5
+ const moduleMock = jest.genMockFromModule('../CoreService');
6
+
7
+ export const CoreService = classMock<Module.CoreService>(
8
+ () => ({
9
+ getApiKeyDetails: jest.fn(async () => {
10
+ return {
11
+ scopes: ['translations.edit', 'keys.edit'] as Scope[],
12
+ projectId: 0,
13
+ } as Module.ApiKeyWithLanguagesModel;
14
+ }),
15
+ }),
16
+ (moduleMock as typeof Module).CoreService
17
+ );
@@ -0,0 +1,79 @@
1
+ jest.dontMock('./Messages');
2
+ jest.dontMock('../services/DependencyService');
3
+
4
+ import { Messages } from './Messages';
5
+ import { DependencyService } from '../services/DependencyService';
6
+
7
+ describe('Messages', () => {
8
+ let messages: Messages;
9
+ let windowAddEventListenerSpy;
10
+ let windowRemoveEventListenerSpy;
11
+
12
+ beforeEach(async () => {
13
+ messages = new DependencyService().messages;
14
+ windowAddEventListenerSpy = jest.spyOn(window, 'addEventListener');
15
+ windowRemoveEventListenerSpy = jest.spyOn(window, 'removeEventListener');
16
+ messages.startListening();
17
+ });
18
+
19
+ afterEach(() => {
20
+ jest.clearAllMocks();
21
+ });
22
+
23
+ test('Can be created', () => {
24
+ expect(messages).toBeInstanceOf(Messages);
25
+ });
26
+
27
+ describe('starListening method', () => {
28
+ test('adds event listener to window', () => {
29
+ expect(windowAddEventListenerSpy).toHaveBeenCalledTimes(1);
30
+ });
31
+ });
32
+
33
+ describe('listening', () => {
34
+ let resolve;
35
+ const promise = new Promise((r) => (resolve = r));
36
+ const callbackMock = jest.fn(() => {
37
+ resolve();
38
+ });
39
+ const data = {};
40
+ let removeListener;
41
+ const sendMessage = () =>
42
+ window.dispatchEvent(
43
+ new MessageEvent('message', {
44
+ data: { type: 'TEST_EVENT', data },
45
+ source: window,
46
+ origin: window.origin,
47
+ })
48
+ );
49
+
50
+ beforeEach(async () => {
51
+ removeListener = messages.listen('TEST_EVENT', callbackMock);
52
+ sendMessage();
53
+ await promise;
54
+ });
55
+
56
+ test('listens to event', async () => {
57
+ expect(callbackMock).toHaveBeenCalledTimes(1);
58
+ expect(callbackMock).toHaveBeenCalledWith(data);
59
+ });
60
+
61
+ test('returns callback to remove listener', () => {
62
+ expect((messages as any).listeners).toHaveLength(1);
63
+ removeListener();
64
+ expect((messages as any).listeners).toHaveLength(0);
65
+ });
66
+ });
67
+
68
+ describe('stopListening method', () => {
69
+ test('stopsListening', () => {
70
+ messages.stopListening();
71
+ expect(windowRemoveEventListenerSpy).toHaveBeenCalledTimes(1);
72
+ expect(windowRemoveEventListenerSpy).toHaveBeenCalledWith(
73
+ windowAddEventListenerSpy.mock.calls[0][0],
74
+ windowAddEventListenerSpy.mock.calls[0][1],
75
+ windowAddEventListenerSpy.mock.calls[0][2]
76
+ );
77
+ });
78
+ });
79
+ });
@@ -0,0 +1,60 @@
1
+ type Listener = {
2
+ type: string;
3
+ callback: (data) => void;
4
+ };
5
+
6
+ type Message = {
7
+ data: any;
8
+ type: string;
9
+ };
10
+
11
+ type TolgeeEvent = {
12
+ data: Message;
13
+ } & MessageEvent;
14
+
15
+ export class Messages {
16
+ private listeners: Listener[] = [];
17
+ private _stopListening: () => void;
18
+
19
+ readonly startListening = () => {
20
+ const receiveMessage = (event: TolgeeEvent) => {
21
+ if (event.source != window) {
22
+ return;
23
+ }
24
+ this.listeners.forEach((listener) => {
25
+ if (listener.type == event.data.type) {
26
+ listener.callback(event.data.data);
27
+ }
28
+ });
29
+ };
30
+
31
+ window.addEventListener('message', receiveMessage, false);
32
+
33
+ typeof this._stopListening === 'function' && this._stopListening();
34
+ this._stopListening = () => {
35
+ window.removeEventListener('message', receiveMessage, false);
36
+ };
37
+ };
38
+
39
+ public stopListening() {
40
+ this._stopListening();
41
+ }
42
+
43
+ readonly listen = (type: string, callback: (data) => void) => {
44
+ const listenerInfo = { type, callback };
45
+ this.listeners.push(listenerInfo);
46
+ // return callback to remove the listener
47
+ return () => {
48
+ this.listeners.splice(this.listeners.indexOf(listenerInfo), 1);
49
+ };
50
+ };
51
+
52
+ readonly send = (type: string, data?: any) => {
53
+ try {
54
+ window.postMessage({ type, data }, window.origin);
55
+ } catch (e) {
56
+ // eslint-disable-next-line no-console
57
+ console.warn('Cannot send message.', e);
58
+ }
59
+ };
60
+ }
@@ -0,0 +1,108 @@
1
+ jest.dontMock('./PluginManager');
2
+ jest.dontMock('../services/DependencyService');
3
+
4
+ import { ElementWithMeta } from '../types';
5
+ import { ElementRegistrar } from '../services/ElementRegistrar';
6
+ import { getMockedInstance } from '@testFixtures/mocked';
7
+ import { PluginManager } from './PluginManager';
8
+ import { DependencyService } from '../services/DependencyService';
9
+ import { TranslationService } from '../services/TranslationService';
10
+ import { Messages } from './Messages';
11
+
12
+ describe('PluginManager', () => {
13
+ let pluginManager: PluginManager;
14
+
15
+ beforeEach(async () => {
16
+ pluginManager = new DependencyService().pluginManager;
17
+ });
18
+
19
+ afterEach(() => {
20
+ jest.clearAllMocks();
21
+ });
22
+
23
+ test('Can be created', () => {
24
+ expect(pluginManager).toBeInstanceOf(PluginManager);
25
+ });
26
+
27
+ describe('take screenshot methods', () => {
28
+ const data = {
29
+ key: 'test_key',
30
+ translations: { en: 'English!' },
31
+ };
32
+
33
+ let resolve;
34
+ const highlightMock = jest.fn();
35
+ const unHighlightMock = jest.fn();
36
+ let listenCallback;
37
+ const cancelMock = jest.fn();
38
+ const revertMock = jest.fn();
39
+
40
+ beforeEach(async () => {
41
+ getMockedInstance(TranslationService).changeTranslations = jest.fn(
42
+ () =>
43
+ new Promise((r) => {
44
+ resolve = r;
45
+ })
46
+ );
47
+
48
+ getMockedInstance(ElementRegistrar).findAllByKey = jest.fn(() => {
49
+ const element = document.createElement(
50
+ 'span'
51
+ ) as any as ElementWithMeta;
52
+ element._tolgee = {
53
+ highlight: highlightMock,
54
+ unhighlight: unHighlightMock,
55
+ } as any;
56
+
57
+ return [element];
58
+ });
59
+
60
+ (getMockedInstance(Messages) as any).send = jest.fn();
61
+
62
+ (getMockedInstance(Messages) as any).listen = jest.fn((_, callback) => {
63
+ listenCallback = callback;
64
+ return cancelMock;
65
+ });
66
+
67
+ pluginManager.takeScreenshot(data);
68
+ resolve(revertMock);
69
+ });
70
+
71
+ test('calls change translation', () => {
72
+ expect(
73
+ getMockedInstance(TranslationService).changeTranslations
74
+ ).toHaveBeenCalledWith(data);
75
+ });
76
+
77
+ test('sends the message to take screenshots', () => {
78
+ expect((getMockedInstance(Messages) as any).send).toHaveBeenCalledTimes(
79
+ 1
80
+ );
81
+ });
82
+
83
+ test('highlights all', () => {
84
+ expect(
85
+ getMockedInstance(ElementRegistrar).findAllByKey
86
+ ).toHaveBeenCalledWith(data.key);
87
+ expect(
88
+ getMockedInstance(ElementRegistrar).findAllByKey
89
+ ).toHaveBeenCalledTimes(1);
90
+ expect(highlightMock).toHaveBeenCalledTimes(1);
91
+ });
92
+
93
+ test('unhighlights all after', () => {
94
+ listenCallback();
95
+ expect(unHighlightMock).toHaveBeenCalledTimes(1);
96
+ });
97
+
98
+ test('reverts the translation change', () => {
99
+ listenCallback();
100
+ expect(revertMock).toHaveBeenCalledTimes(1);
101
+ });
102
+
103
+ test('cancels the listening', () => {
104
+ listenCallback();
105
+ expect(cancelMock).toHaveBeenCalledTimes(1);
106
+ });
107
+ });
108
+ });