@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.
- package/dist/tolgee.cjs.js +2 -4
- package/dist/tolgee.cjs.js.map +1 -1
- package/dist/tolgee.cjs.min.js +1 -1
- package/dist/tolgee.cjs.min.js.map +1 -1
- package/dist/{tolgee.esm.js → tolgee.esm.min.mjs} +2 -2
- package/dist/tolgee.esm.min.mjs.map +1 -0
- package/dist/tolgee.esm.mjs +5690 -0
- package/dist/tolgee.esm.mjs.map +1 -0
- package/dist/tolgee.umd.js +2 -4
- package/dist/tolgee.umd.js.map +1 -1
- package/dist/tolgee.umd.min.js +1 -1
- package/dist/tolgee.umd.min.js.map +1 -1
- package/package.json +10 -9
- package/src/Constants/Global.ts +9 -0
- package/src/Constants/ModifierKey.ts +6 -0
- package/src/Errors/ApiHttpError.ts +8 -0
- package/src/Observer.test.ts +119 -0
- package/src/Observer.ts +68 -0
- package/src/Properties.test.ts +150 -0
- package/src/Properties.ts +112 -0
- package/src/Tolgee.test.ts +473 -0
- package/src/Tolgee.ts +335 -0
- package/src/TolgeeConfig.test.ts +21 -0
- package/src/TolgeeConfig.ts +134 -0
- package/src/__integration/FormatterIcu.test.ts +80 -0
- package/src/__integration/FormatterMissing.ts +54 -0
- package/src/__integration/Tolgee.test.ts +90 -0
- package/src/__integration/TolgeeInvisible.test.ts +145 -0
- package/src/__integration/mockTranslations.ts +6 -0
- package/src/__integration/testConfig.ts +16 -0
- package/src/__testFixtures/classMock.ts +11 -0
- package/src/__testFixtures/createElement.ts +43 -0
- package/src/__testFixtures/createTestDom.ts +25 -0
- package/src/__testFixtures/mocked.ts +25 -0
- package/src/__testFixtures/setupAfterEnv.ts +34 -0
- package/src/helpers/NodeHelper.ts +90 -0
- package/src/helpers/TextHelper.test.ts +62 -0
- package/src/helpers/TextHelper.ts +58 -0
- package/src/helpers/commonTypes.ts +8 -0
- package/src/helpers/encoderPolyfill.ts +96 -0
- package/src/helpers/secret.test.ts +61 -0
- package/src/helpers/secret.ts +68 -0
- package/src/helpers/sleep.ts +2 -0
- package/src/highlighter/HighlightFunctionsInitializer.test.ts +40 -0
- package/src/highlighter/HighlightFunctionsInitializer.ts +61 -0
- package/src/highlighter/MouseEventHandler.test.ts +151 -0
- package/src/highlighter/MouseEventHandler.ts +191 -0
- package/src/highlighter/TranslationHighlighter.test.ts +177 -0
- package/src/highlighter/TranslationHighlighter.ts +113 -0
- package/src/index.ts +10 -0
- package/src/internal.ts +2 -0
- package/src/modules/IcuFormatter.ts +17 -0
- package/src/modules/index.ts +1 -0
- package/src/services/ApiHttpService.ts +85 -0
- package/src/services/CoreService.test.ts +142 -0
- package/src/services/CoreService.ts +76 -0
- package/src/services/DependencyService.test.ts +51 -0
- package/src/services/DependencyService.ts +116 -0
- package/src/services/ElementRegistrar.test.ts +131 -0
- package/src/services/ElementRegistrar.ts +108 -0
- package/src/services/EventEmitter.ts +52 -0
- package/src/services/EventService.ts +14 -0
- package/src/services/ModuleService.ts +14 -0
- package/src/services/ScreenshotService.ts +31 -0
- package/src/services/Subscription.ts +7 -0
- package/src/services/TextService.test.ts +88 -0
- package/src/services/TextService.ts +82 -0
- package/src/services/TranslationService.test.ts +358 -0
- package/src/services/TranslationService.ts +417 -0
- package/src/services/__mocks__/CoreService.ts +17 -0
- package/src/toolsManager/Messages.test.ts +79 -0
- package/src/toolsManager/Messages.ts +60 -0
- package/src/toolsManager/PluginManager.test.ts +108 -0
- package/src/toolsManager/PluginManager.ts +129 -0
- package/src/types/DTOs.ts +25 -0
- package/src/types/apiSchema.generated.ts +6208 -0
- package/src/types.ts +146 -0
- package/src/wrappers/AbstractWrapper.ts +14 -0
- package/src/wrappers/NodeHandler.ts +143 -0
- package/src/wrappers/WrappedHandler.ts +28 -0
- package/src/wrappers/invisible/AttributeHandler.ts +23 -0
- package/src/wrappers/invisible/Coder.ts +65 -0
- package/src/wrappers/invisible/ContentHandler.ts +15 -0
- package/src/wrappers/invisible/CoreHandler.ts +17 -0
- package/src/wrappers/invisible/InvisibleWrapper.ts +59 -0
- package/src/wrappers/invisible/ValueMemory.test.ts +25 -0
- package/src/wrappers/invisible/ValueMemory.ts +16 -0
- package/src/wrappers/text/AttributeHandler.test.ts +117 -0
- package/src/wrappers/text/AttributeHandler.ts +25 -0
- package/src/wrappers/text/Coder.test.ts +298 -0
- package/src/wrappers/text/Coder.ts +202 -0
- package/src/wrappers/text/ContentHandler.test.ts +185 -0
- package/src/wrappers/text/ContentHandler.ts +21 -0
- package/src/wrappers/text/CoreHandler.test.ts +106 -0
- package/src/wrappers/text/CoreHandler.ts +45 -0
- package/src/wrappers/text/TextWrapper.ts +69 -0
- package/dist/tolgee.esm.js.map +0 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
jest.dontMock('./TranslationHighlighter');
|
|
2
|
+
jest.dontMock('../services/DependencyService');
|
|
3
|
+
|
|
4
|
+
import { TranslationHighlighter } from './TranslationHighlighter';
|
|
5
|
+
import classMock from '@testFixtures/classMock';
|
|
6
|
+
import { getMockedInstance } from '@testFixtures/mocked';
|
|
7
|
+
import { Properties } from '../Properties';
|
|
8
|
+
import { createElement } from '@testFixtures/createElement';
|
|
9
|
+
import { DependencyService } from '../services/DependencyService';
|
|
10
|
+
|
|
11
|
+
describe('TranslationHighlighter', () => {
|
|
12
|
+
let translationHighlighter: TranslationHighlighter;
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
const dependencyService = new DependencyService();
|
|
16
|
+
dependencyService.init({});
|
|
17
|
+
translationHighlighter = dependencyService.translationHighlighter;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
window['@tolgee/ui'] = undefined;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('passing UI', () => {
|
|
26
|
+
const checkIt = async () => {
|
|
27
|
+
const mockedElement = createElement(20, 20, true);
|
|
28
|
+
translationHighlighter.listen(mockedElement);
|
|
29
|
+
await translationHighlighter.translationEdit(openEvent, mockedElement);
|
|
30
|
+
expect(rendererViewerMock).toBeCalledTimes(1);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
test('Works when UI is provided using regular provider', async () => {
|
|
34
|
+
getMockedInstance(Properties).config.ui = getUiClassMock();
|
|
35
|
+
await checkIt();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('Works when UI is provided using promise provider', async () => {
|
|
39
|
+
// @ts-ignore
|
|
40
|
+
getMockedInstance(Properties).config.ui = new Promise((resolve) =>
|
|
41
|
+
resolve(getUiClassMock())
|
|
42
|
+
);
|
|
43
|
+
await checkIt();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('works when UI is provided using window provider', async () => {
|
|
47
|
+
getMockedInstance(Properties).config.ui = undefined;
|
|
48
|
+
window['@tolgee/ui'] = {
|
|
49
|
+
UI: getUiClassMock(),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
await checkIt();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('works when UI is provided using window constructor', async () => {
|
|
56
|
+
getMockedInstance(Properties).config.ui = undefined;
|
|
57
|
+
window['@tolgee/ui'] = getUiClassMock();
|
|
58
|
+
await checkIt();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('key rendering', () => {
|
|
63
|
+
test('will open renderer key context menu when multiple nodes', async () => {
|
|
64
|
+
await testNodeCounts(2, 1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('will open renderer key context menu when multiple keys', async () => {
|
|
68
|
+
await testNodeCounts(1, 10);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('will open translation dialog when single key', async () => {
|
|
72
|
+
const mockedElement = createElement(1, 1, true);
|
|
73
|
+
translationHighlighter.listen(mockedElement);
|
|
74
|
+
await translationHighlighter.translationEdit(openEvent, mockedElement);
|
|
75
|
+
|
|
76
|
+
expect(rendererViewerMock).toBeCalledTimes(1);
|
|
77
|
+
expect(rendererViewerMock).toBeCalledWith('key', 'default value');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('will open translation dialog when single key multiplied', async () => {
|
|
81
|
+
const mockedElement = createElement(20, 20, true);
|
|
82
|
+
translationHighlighter.listen(mockedElement);
|
|
83
|
+
await translationHighlighter.translationEdit(openEvent, mockedElement);
|
|
84
|
+
|
|
85
|
+
expect(rendererViewerMock).toBeCalledTimes(1);
|
|
86
|
+
expect(rendererViewerMock).toBeCalledWith('key', 'default value');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('warnings & errors', () => {
|
|
91
|
+
test('will print error on no key', async () => {
|
|
92
|
+
// eslint-disable-next-line no-console
|
|
93
|
+
console.error = jest.fn();
|
|
94
|
+
rendererGetKeyMock = jest.fn(async (): Promise<string> => {
|
|
95
|
+
return;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
rendererViewerMock = jest.fn();
|
|
99
|
+
|
|
100
|
+
getMockedInstance(Properties).config.ui = classMock<any>(
|
|
101
|
+
() => ({
|
|
102
|
+
getKey: rendererGetKeyMock,
|
|
103
|
+
}),
|
|
104
|
+
function () {
|
|
105
|
+
return {};
|
|
106
|
+
} as any
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const mockedElement = createElement(0, 0);
|
|
110
|
+
translationHighlighter.listen(mockedElement);
|
|
111
|
+
|
|
112
|
+
await translationHighlighter.translationEdit(openEvent, mockedElement);
|
|
113
|
+
|
|
114
|
+
// eslint-disable-next-line no-console
|
|
115
|
+
expect(console.error).toBeCalledTimes(1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('will print warning when UI not provided', async () => {
|
|
119
|
+
// eslint-disable-next-line no-console
|
|
120
|
+
console.warn = jest.fn();
|
|
121
|
+
|
|
122
|
+
getMockedInstance(Properties).config.ui = null;
|
|
123
|
+
|
|
124
|
+
const mockedElement = createElement(1, 1);
|
|
125
|
+
translationHighlighter.listen(mockedElement);
|
|
126
|
+
|
|
127
|
+
await translationHighlighter.translationEdit(openEvent, mockedElement);
|
|
128
|
+
|
|
129
|
+
// eslint-disable-next-line no-console
|
|
130
|
+
expect(console.warn).toBeCalledTimes(1);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
let rendererGetKeyMock: (...args) => Promise<string>;
|
|
135
|
+
let rendererViewerMock: (...args) => void;
|
|
136
|
+
|
|
137
|
+
beforeEach(() => {
|
|
138
|
+
rendererGetKeyMock = jest.fn(async (): Promise<string> => {
|
|
139
|
+
return 'test';
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
rendererViewerMock = jest.fn();
|
|
143
|
+
getMockedInstance(Properties).config.ui = getUiClassMock();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const openEvent = new MouseEvent('click');
|
|
147
|
+
|
|
148
|
+
const testNodeCounts = async (nodeCount, keyCount) => {
|
|
149
|
+
const mockedElement = createElement(nodeCount, keyCount);
|
|
150
|
+
translationHighlighter.listen(mockedElement);
|
|
151
|
+
await translationHighlighter.translationEdit(openEvent, mockedElement);
|
|
152
|
+
expect(rendererGetKeyMock).toBeCalledTimes(1);
|
|
153
|
+
|
|
154
|
+
const keySet = new Set();
|
|
155
|
+
for (let i = 0; i < nodeCount * keyCount; i++) {
|
|
156
|
+
keySet.add(`key ${i}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
expect(rendererGetKeyMock).toBeCalledTimes(1);
|
|
160
|
+
expect(rendererGetKeyMock).toBeCalledWith({ keys: keySet, openEvent });
|
|
161
|
+
expect(rendererGetKeyMock).not.toBeCalledWith({
|
|
162
|
+
keys: new Set(['key 0']),
|
|
163
|
+
openEvent,
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const getUiClassMock = () =>
|
|
168
|
+
classMock<any>(
|
|
169
|
+
() => ({
|
|
170
|
+
getKey: rendererGetKeyMock,
|
|
171
|
+
renderViewer: rendererViewerMock,
|
|
172
|
+
}),
|
|
173
|
+
function () {
|
|
174
|
+
return {};
|
|
175
|
+
} as any
|
|
176
|
+
);
|
|
177
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { ElementWithMeta } from '../types';
|
|
2
|
+
import { PluginManager } from '../toolsManager/PluginManager';
|
|
3
|
+
import { DependencyService } from '../services/DependencyService';
|
|
4
|
+
|
|
5
|
+
type KeyWithDefault = { key: string; defaultValue?: string };
|
|
6
|
+
|
|
7
|
+
export class TranslationHighlighter {
|
|
8
|
+
public pluginManager: PluginManager;
|
|
9
|
+
private _renderer: any;
|
|
10
|
+
|
|
11
|
+
constructor(private dependencies: DependencyService) {}
|
|
12
|
+
|
|
13
|
+
private static getKeyOptions(node: ElementWithMeta): KeyWithDefault[] {
|
|
14
|
+
const nodes = Array.from(node._tolgee.nodes);
|
|
15
|
+
return nodes.reduce(
|
|
16
|
+
(acc, curr) => [
|
|
17
|
+
...acc,
|
|
18
|
+
...curr._tolgee.keys.map((k) => ({
|
|
19
|
+
key: k.key,
|
|
20
|
+
defaultValue: k.defaultValue,
|
|
21
|
+
})),
|
|
22
|
+
],
|
|
23
|
+
[]
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
listen(element: ElementWithMeta & ElementCSSInlineStyle) {
|
|
28
|
+
this.dependencies.highlightFunctionInitializer.initFunctions(element);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private async getRenderer() {
|
|
32
|
+
if (this._renderer === undefined) {
|
|
33
|
+
const possibleProviders = [
|
|
34
|
+
this.dependencies.properties.config.ui,
|
|
35
|
+
window['@tolgee/ui'],
|
|
36
|
+
];
|
|
37
|
+
for (const possiblePromise of possibleProviders) {
|
|
38
|
+
// if dynamic import is used
|
|
39
|
+
const possibleObject =
|
|
40
|
+
possiblePromise instanceof Promise
|
|
41
|
+
? await possiblePromise
|
|
42
|
+
: possiblePromise;
|
|
43
|
+
|
|
44
|
+
// extract .UI property
|
|
45
|
+
const possibleProvider =
|
|
46
|
+
typeof possibleObject === 'object'
|
|
47
|
+
? possibleObject?.UI
|
|
48
|
+
: possibleObject;
|
|
49
|
+
|
|
50
|
+
if (typeof possibleProvider === 'function') {
|
|
51
|
+
this._renderer = new possibleProvider(this.dependencies);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (this._renderer === undefined) {
|
|
56
|
+
// eslint-disable-next-line no-console
|
|
57
|
+
console.warn(
|
|
58
|
+
'Tolgee UI is not provided. To translate interactively provide tolgee ui constructor to "ui" configuration property. ' +
|
|
59
|
+
'To disable highlighting use production mode.'
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return this._renderer;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private async getKeyAndDefault(
|
|
67
|
+
mouseEvent: MouseEvent,
|
|
68
|
+
element: ElementWithMeta
|
|
69
|
+
): Promise<KeyWithDefault> {
|
|
70
|
+
if (element._tolgee.wrappedWithElementOnlyKey) {
|
|
71
|
+
return {
|
|
72
|
+
key: element._tolgee.wrappedWithElementOnlyKey,
|
|
73
|
+
defaultValue: element._tolgee.wrappedWithElementOnlyDefaultHtml,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const keysWithDefaults = TranslationHighlighter.getKeyOptions(element);
|
|
77
|
+
|
|
78
|
+
// create Set to remove duplicated key values
|
|
79
|
+
const keySet = new Set(
|
|
80
|
+
keysWithDefaults.map((keyWithDefault) => keyWithDefault.key)
|
|
81
|
+
);
|
|
82
|
+
if (keySet.size > 1) {
|
|
83
|
+
const renderer = await this.getRenderer();
|
|
84
|
+
// this opens the popover where user chooses the key
|
|
85
|
+
const selectedKey = await renderer.getKey({
|
|
86
|
+
keys: keySet,
|
|
87
|
+
openEvent: mouseEvent,
|
|
88
|
+
});
|
|
89
|
+
// get the key with default
|
|
90
|
+
const found = keysWithDefaults.find((kwd) => kwd.key === selectedKey);
|
|
91
|
+
if (found) {
|
|
92
|
+
return found;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (keySet.size === 1) {
|
|
96
|
+
return keysWithDefaults[0];
|
|
97
|
+
}
|
|
98
|
+
// eslint-disable-next-line no-console
|
|
99
|
+
console.error('No key to translate. This seems like a bug in tolgee.');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public translationEdit = async (e: MouseEvent, element: ElementWithMeta) => {
|
|
103
|
+
const renderer = await this.getRenderer();
|
|
104
|
+
if (typeof renderer === 'object') {
|
|
105
|
+
const key = await this.getKeyAndDefault(e, element);
|
|
106
|
+
if (key) {
|
|
107
|
+
renderer.renderViewer(key.key, key.defaultValue);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Tolgee } from './Tolgee';
|
|
2
|
+
export { TolgeeConfig } from './TolgeeConfig';
|
|
3
|
+
export { ModifierKey } from './Constants/ModifierKey';
|
|
4
|
+
export { TOLGEE_WRAPPED_ONLY_DATA_ATTRIBUTE } from './Constants/Global';
|
|
5
|
+
export * from './modules';
|
|
6
|
+
export * from './types/DTOs';
|
|
7
|
+
export * from './types';
|
|
8
|
+
export * from './Constants/Global';
|
|
9
|
+
export * from './helpers/NodeHelper';
|
|
10
|
+
export * from './helpers/TextHelper';
|
package/src/internal.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import IntlMessageFormat from 'intl-messageformat';
|
|
2
|
+
import { TolgeeModule } from '../types';
|
|
3
|
+
export const IcuFormatter: TolgeeModule = class {
|
|
4
|
+
static type = 'formatter' as const;
|
|
5
|
+
|
|
6
|
+
cache = new Map<string, IntlMessageFormat>();
|
|
7
|
+
|
|
8
|
+
format({ translation, language, params }) {
|
|
9
|
+
const ignoreTag = !Object.values(params).find(
|
|
10
|
+
(p) => typeof p === 'function'
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
return new IntlMessageFormat(translation, language, undefined, {
|
|
14
|
+
ignoreTag,
|
|
15
|
+
}).format(params) as string;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { IcuFormatter } from './IcuFormatter';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Properties } from '../Properties';
|
|
2
|
+
import { ApiHttpError } from '../Errors/ApiHttpError';
|
|
3
|
+
import { ArgumentTypes } from '../helpers/commonTypes';
|
|
4
|
+
|
|
5
|
+
type FetchArgumentTypes = ArgumentTypes<typeof fetch>;
|
|
6
|
+
|
|
7
|
+
type Tail<T extends any[]> = ((...args: T) => any) extends (
|
|
8
|
+
_: infer First,
|
|
9
|
+
...rest: infer Rest
|
|
10
|
+
) => any
|
|
11
|
+
? T extends any[]
|
|
12
|
+
? Rest
|
|
13
|
+
: ReadonlyArray<Rest[number]>
|
|
14
|
+
: [];
|
|
15
|
+
|
|
16
|
+
export class ApiHttpService {
|
|
17
|
+
constructor(private properties: Properties) {}
|
|
18
|
+
|
|
19
|
+
private static async handleErrors(response: Response) {
|
|
20
|
+
if (response.status >= 400) {
|
|
21
|
+
const error = new ApiHttpError(response);
|
|
22
|
+
try {
|
|
23
|
+
const data = await response.json();
|
|
24
|
+
error.code = data.code;
|
|
25
|
+
} catch (e) {
|
|
26
|
+
// eslint-disable-next-line no-console
|
|
27
|
+
console.warn('Tolgee server responded with invalid status code.');
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
return response;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async fetch(...args: ArgumentTypes<typeof fetch>) {
|
|
35
|
+
if (typeof args[0] === 'object') {
|
|
36
|
+
return fetch({ ...args[0], url: this.getUrl((args[0] as any).url) }).then(
|
|
37
|
+
(r) => ApiHttpService.handleErrors(r)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const [url, ...rest] = args;
|
|
41
|
+
return fetch(this.getUrl(url), ...rest).then((r) =>
|
|
42
|
+
ApiHttpService.handleErrors(r)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async fetchJson(...args: ArgumentTypes<typeof fetch>) {
|
|
47
|
+
return this.fetch(...args).then((res) => {
|
|
48
|
+
return res.json();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async post(
|
|
53
|
+
url,
|
|
54
|
+
body,
|
|
55
|
+
init: FetchArgumentTypes[1] = {},
|
|
56
|
+
...rest: Tail<Tail<FetchArgumentTypes>>
|
|
57
|
+
) {
|
|
58
|
+
return this.fetch(
|
|
59
|
+
url,
|
|
60
|
+
{
|
|
61
|
+
body: JSON.stringify(body),
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: {
|
|
64
|
+
'Content-Type': 'application/json',
|
|
65
|
+
},
|
|
66
|
+
...init,
|
|
67
|
+
},
|
|
68
|
+
...rest
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async postJson(
|
|
73
|
+
url,
|
|
74
|
+
body,
|
|
75
|
+
init: FetchArgumentTypes[1] = {},
|
|
76
|
+
...rest: Tail<Tail<FetchArgumentTypes>>
|
|
77
|
+
) {
|
|
78
|
+
return this.post(url, body, init, ...rest).then((res) => res.json());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private getUrl(path: string) {
|
|
82
|
+
const querySeparator = path.indexOf('?') < 0 ? '?' : '&';
|
|
83
|
+
return `${this.properties.config.apiUrl}/${path}${querySeparator}ak=${this.properties.config.apiKey}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
jest.dontMock('./CoreService');
|
|
2
|
+
jest.dontMock('./DependencyService');
|
|
3
|
+
|
|
4
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
5
|
+
import { CoreService } from './CoreService';
|
|
6
|
+
import { getMockedInstance } from '@testFixtures/mocked';
|
|
7
|
+
import { ApiHttpService } from './ApiHttpService';
|
|
8
|
+
import { mocked } from 'ts-jest/utils';
|
|
9
|
+
import { Properties } from '../Properties';
|
|
10
|
+
import { Scope } from '../types';
|
|
11
|
+
import { DependencyService } from './DependencyService';
|
|
12
|
+
|
|
13
|
+
describe('CoreService', () => {
|
|
14
|
+
let coreService: CoreService;
|
|
15
|
+
let mockedFetchJson;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
coreService = new DependencyService().coreService;
|
|
19
|
+
getMockedInstance(Properties).preferredLanguages = new Set<string>();
|
|
20
|
+
getMockedInstance(Properties).config = {
|
|
21
|
+
inputPrefix: '{{',
|
|
22
|
+
inputSuffix: '}}',
|
|
23
|
+
restrictedElements: [],
|
|
24
|
+
tagAttributes: {
|
|
25
|
+
'*': ['aria-label'],
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
mockedFetchJson = mocked(getMockedInstance(ApiHttpService).fetchJson);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
jest.clearAllMocks();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('can be created', () => {
|
|
36
|
+
expect(coreService).not.toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('getLanguages', () => {
|
|
40
|
+
const dummyLanguages = ['dummyLang1', 'dummyLang2'];
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
mockedFetchJson.mockImplementation(async () => {
|
|
44
|
+
return {
|
|
45
|
+
_embedded: {
|
|
46
|
+
languages: dummyLanguages.map((l) => ({
|
|
47
|
+
tag: l,
|
|
48
|
+
})),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('will return languages returned from api http service', async () => {
|
|
55
|
+
expect(await coreService.getLanguages()).toEqual(new Set(dummyLanguages));
|
|
56
|
+
expect(mockedFetchJson).toBeCalledTimes(1);
|
|
57
|
+
expect(mockedFetchJson).toBeCalledWith(`v2/projects/languages?size=1000`);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('sets preferred languages of properties', async () => {
|
|
61
|
+
getMockedInstance(Properties).preferredLanguages = new Set([
|
|
62
|
+
dummyLanguages[0],
|
|
63
|
+
]);
|
|
64
|
+
await coreService.getLanguages();
|
|
65
|
+
expect(getMockedInstance(Properties).preferredLanguages).toEqual(
|
|
66
|
+
new Set([dummyLanguages[0]])
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('getScopes', () => {
|
|
72
|
+
// eslint-disable-next-line no-console
|
|
73
|
+
console.error = jest.fn();
|
|
74
|
+
|
|
75
|
+
test('will switch to production mode on error', async () => {
|
|
76
|
+
mocked(mockedFetchJson).mockImplementation(async () => {
|
|
77
|
+
throw new Error();
|
|
78
|
+
});
|
|
79
|
+
await coreService.getApiKeyDetails();
|
|
80
|
+
expect(getMockedInstance(Properties).mode).toEqual('production');
|
|
81
|
+
// eslint-disable-next-line no-console
|
|
82
|
+
expect(console.error).toBeCalledTimes(2);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('will return value from http service', async () => {
|
|
86
|
+
const mockedReturn = {
|
|
87
|
+
scopes: ['translations.view', 'translations.edit'],
|
|
88
|
+
};
|
|
89
|
+
mocked(mockedFetchJson).mockImplementation(async () => mockedReturn);
|
|
90
|
+
expect(await coreService.getApiKeyDetails()).toEqual(mockedReturn);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('loadApiKeyDetails', () => {
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
const mockedReturn = {
|
|
97
|
+
scopes: ['translations.edit'],
|
|
98
|
+
projectId: 0,
|
|
99
|
+
};
|
|
100
|
+
mocked(mockedFetchJson).mockImplementation(async () => mockedReturn);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('will set properties.scopes on run in development mode', async () => {
|
|
104
|
+
const propertiesMock = getMockedInstance(Properties);
|
|
105
|
+
propertiesMock.mode = 'development';
|
|
106
|
+
await coreService.loadApiKeyDetails();
|
|
107
|
+
expect(propertiesMock.scopes).toContain('translations.edit' as Scope);
|
|
108
|
+
expect(propertiesMock.scopes).not.toContain('translations.view' as Scope);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('will set properties.projectId on run in development mode', async () => {
|
|
112
|
+
const propertiesMock = getMockedInstance(Properties);
|
|
113
|
+
propertiesMock.mode = 'development';
|
|
114
|
+
await coreService.loadApiKeyDetails();
|
|
115
|
+
expect(propertiesMock.projectId).toEqual(0);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('Authorization', () => {
|
|
120
|
+
test('will return proper value on isAuthorizedTo', () => {
|
|
121
|
+
getMockedInstance(Properties).scopes = [
|
|
122
|
+
'translations.edit',
|
|
123
|
+
'translations.view',
|
|
124
|
+
] as Scope[];
|
|
125
|
+
expect(coreService.isAuthorizedTo('keys.edit')).toBeFalsy();
|
|
126
|
+
expect(coreService.isAuthorizedTo('translations.view')).toBeTruthy();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('will return proper on checkScopes', () => {
|
|
130
|
+
getMockedInstance(Properties).scopes = [
|
|
131
|
+
'translations.edit',
|
|
132
|
+
'translations.view',
|
|
133
|
+
] as Scope[];
|
|
134
|
+
expect(jest.fn(() => coreService.checkScope('keys.edit'))).toThrowError();
|
|
135
|
+
const checkMock = jest.fn(() =>
|
|
136
|
+
coreService.checkScope('translations.view')
|
|
137
|
+
);
|
|
138
|
+
checkMock();
|
|
139
|
+
expect(checkMock).toReturn();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Properties } from '../Properties';
|
|
2
|
+
import { ApiHttpService } from './ApiHttpService';
|
|
3
|
+
import { Scope } from '../types';
|
|
4
|
+
import { LanguageModel, PagedModelLanguageModel } from '../types/DTOs';
|
|
5
|
+
import { components } from '../types/apiSchema.generated';
|
|
6
|
+
|
|
7
|
+
export type ApiKeyWithLanguagesModel =
|
|
8
|
+
components['schemas']['ApiKeyWithLanguagesModel'];
|
|
9
|
+
|
|
10
|
+
export class CoreService {
|
|
11
|
+
private languagePromise: Promise<PagedModelLanguageModel>;
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
private properties: Properties,
|
|
15
|
+
private apiHttpService: ApiHttpService
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
async getLanguages(): Promise<Set<string>> {
|
|
19
|
+
if (!(this.languagePromise instanceof Promise)) {
|
|
20
|
+
this.languagePromise = this.apiHttpService.fetchJson(
|
|
21
|
+
`v2/projects/languages?size=1000`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const languages = new Set(
|
|
26
|
+
(await this.languagePromise)._embedded.languages.map((l) => l.tag)
|
|
27
|
+
);
|
|
28
|
+
this.properties.preferredLanguages = new Set<string>(
|
|
29
|
+
Array.from(this.properties.preferredLanguages).filter((l) =>
|
|
30
|
+
languages.has(l)
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
return languages;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async getLanguagesFull(): Promise<LanguageModel[]> {
|
|
37
|
+
this.getLanguages();
|
|
38
|
+
const languages = await this.languagePromise;
|
|
39
|
+
return languages._embedded.languages;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async getApiKeyDetails(): Promise<ApiKeyWithLanguagesModel> {
|
|
43
|
+
try {
|
|
44
|
+
return await this.apiHttpService.fetchJson(`v2/api-keys/current`);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
// eslint-disable-next-line no-console
|
|
47
|
+
console.error(e);
|
|
48
|
+
// eslint-disable-next-line no-console
|
|
49
|
+
console.error(
|
|
50
|
+
'Error getting scopes. Trying to switch to production mode!'
|
|
51
|
+
);
|
|
52
|
+
this.properties.mode = 'production';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
isAuthorizedTo(scope: Scope) {
|
|
57
|
+
return this.properties.scopes.indexOf(scope) > -1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
checkScope(scope: Scope) {
|
|
61
|
+
if (!this.isAuthorizedTo(scope)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"Api key not permitted to do this, please add 'translations.view' scope."
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async loadApiKeyDetails() {
|
|
69
|
+
if (this.properties.scopes === undefined) {
|
|
70
|
+
const details = await this.getApiKeyDetails();
|
|
71
|
+
this.properties.scopes = details.scopes as Scope[];
|
|
72
|
+
this.properties.projectId = details.projectId;
|
|
73
|
+
this.properties.permittedLanguageIds = details.permittedLanguageIds;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
jest.dontMock('./DependencyService');
|
|
2
|
+
jest.dontMock('../TolgeeConfig');
|
|
3
|
+
|
|
4
|
+
import { getMockedInstance } from '@testFixtures/mocked';
|
|
5
|
+
import { mocked } from 'ts-jest/utils';
|
|
6
|
+
import { InvisibleWrapper } from '../wrappers/invisible/InvisibleWrapper';
|
|
7
|
+
import { TextWrapper } from '../wrappers/text/TextWrapper';
|
|
8
|
+
import { DependencyService } from './DependencyService';
|
|
9
|
+
|
|
10
|
+
describe('DependecyStore', () => {
|
|
11
|
+
let dependecyService: DependencyService;
|
|
12
|
+
|
|
13
|
+
const config = {
|
|
14
|
+
apiKey: 'yep',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
dependecyService = new DependencyService();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
jest.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('inits translation service', () => {
|
|
26
|
+
dependecyService.init(config);
|
|
27
|
+
dependecyService.run();
|
|
28
|
+
expect(
|
|
29
|
+
mocked(dependecyService.translationService).initStatic
|
|
30
|
+
).toBeCalledTimes(1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('sets config to properties', () => {
|
|
34
|
+
dependecyService.init(config);
|
|
35
|
+
expect(dependecyService.properties.config.apiKey).toEqual(config.apiKey);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('inits text wrapper', () => {
|
|
39
|
+
dependecyService.init(config);
|
|
40
|
+
dependecyService.run();
|
|
41
|
+
expect(dependecyService.wrapper).toEqual(getMockedInstance(TextWrapper));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('inits invisible wrapper', () => {
|
|
45
|
+
dependecyService.init({ ...config, wrapperMode: 'invisible' });
|
|
46
|
+
dependecyService.run();
|
|
47
|
+
expect(dependecyService.wrapper).toEqual(
|
|
48
|
+
getMockedInstance(InvisibleWrapper)
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
});
|