@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,90 @@
|
|
|
1
|
+
jest.autoMockOff();
|
|
2
|
+
|
|
3
|
+
import { waitFor } from '@testing-library/dom';
|
|
4
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
5
|
+
import { Tolgee } from '..';
|
|
6
|
+
import mockTranslations from './mockTranslations';
|
|
7
|
+
import fetchMock from 'jest-fetch-mock';
|
|
8
|
+
import { testConfig } from './testConfig';
|
|
9
|
+
|
|
10
|
+
const API_URL = 'http://localhost';
|
|
11
|
+
const API_KEY = 'dummyApiKey';
|
|
12
|
+
|
|
13
|
+
const fetch = fetchMock.mockResponse(async (req) => {
|
|
14
|
+
if (req.url.includes('/v2/api-keys/current')) {
|
|
15
|
+
return JSON.stringify(testConfig);
|
|
16
|
+
} else if (req.url.includes('/v2/projects/translations/en')) {
|
|
17
|
+
return JSON.stringify(mockTranslations);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
throw new Error('Invalid request');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('Tolgee invisble mode', () => {
|
|
24
|
+
let tolgee: Tolgee;
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
fetch.enableMocks();
|
|
27
|
+
tolgee = Tolgee.init({
|
|
28
|
+
targetElement: document.body,
|
|
29
|
+
apiKey: API_KEY,
|
|
30
|
+
apiUrl: API_URL,
|
|
31
|
+
inputPrefix: '{{',
|
|
32
|
+
inputSuffix: '}}',
|
|
33
|
+
});
|
|
34
|
+
document.body.innerHTML = '';
|
|
35
|
+
await tolgee.run();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
tolgee.stop();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('it translate text in body', async () => {
|
|
43
|
+
document.body.innerHTML = '{{hello_world}}';
|
|
44
|
+
|
|
45
|
+
await waitFor(() => {
|
|
46
|
+
const el = document
|
|
47
|
+
.evaluate(
|
|
48
|
+
"descendant-or-self::*[contains(text(), 'Hello world!')]",
|
|
49
|
+
document.body,
|
|
50
|
+
null,
|
|
51
|
+
0
|
|
52
|
+
)
|
|
53
|
+
.iterateNext();
|
|
54
|
+
|
|
55
|
+
expect(el).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('returns default when provided', async () => {
|
|
60
|
+
expect(
|
|
61
|
+
await tolgee.translate({
|
|
62
|
+
key: 'nonexistant',
|
|
63
|
+
noWrap: true,
|
|
64
|
+
defaultValue: 'This is default',
|
|
65
|
+
})
|
|
66
|
+
).toEqual('This is default');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('returns empty value if orEmpty is true', async () => {
|
|
70
|
+
expect(
|
|
71
|
+
await tolgee.translate({
|
|
72
|
+
key: 'nonexistant',
|
|
73
|
+
noWrap: true,
|
|
74
|
+
orEmpty: true,
|
|
75
|
+
})
|
|
76
|
+
).toEqual('');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('will return key when no translation found', async () => {
|
|
80
|
+
expect(
|
|
81
|
+
await tolgee.translate({ key: 'test\\.key.this\\.is\\.it', noWrap: true })
|
|
82
|
+
).toEqual('test\\.key.this\\.is\\.it');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('will return proper text without any dot', async () => {
|
|
86
|
+
expect(
|
|
87
|
+
await tolgee.translate({ key: 'text without any dot', noWrap: true })
|
|
88
|
+
).toEqual('text without any dot');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
jest.autoMockOff();
|
|
2
|
+
|
|
3
|
+
import { waitFor } from '@testing-library/dom';
|
|
4
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
5
|
+
import { Tolgee } from '..';
|
|
6
|
+
import mockTranslations from './mockTranslations';
|
|
7
|
+
import fetchMock from 'jest-fetch-mock';
|
|
8
|
+
import { testConfig } from './testConfig';
|
|
9
|
+
import { decodeFromText } from '../helpers/secret';
|
|
10
|
+
|
|
11
|
+
const API_URL = 'http://localhost';
|
|
12
|
+
const API_KEY = 'dummyApiKey';
|
|
13
|
+
|
|
14
|
+
const fetch = fetchMock.mockResponse(async (req) => {
|
|
15
|
+
if (req.url.includes('/v2/api-keys/current')) {
|
|
16
|
+
return JSON.stringify(testConfig);
|
|
17
|
+
} else if (req.url.includes('/v2/projects/translations/en')) {
|
|
18
|
+
return JSON.stringify(mockTranslations);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
throw new Error('Invalid request');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('Tolgee invisble mode', () => {
|
|
25
|
+
let tolgee: Tolgee;
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
fetch.enableMocks();
|
|
28
|
+
tolgee = Tolgee.init({
|
|
29
|
+
targetElement: document.body,
|
|
30
|
+
apiKey: API_KEY,
|
|
31
|
+
apiUrl: API_URL,
|
|
32
|
+
wrapperMode: 'invisible',
|
|
33
|
+
});
|
|
34
|
+
document.body.innerHTML = '';
|
|
35
|
+
await tolgee.run();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
tolgee.stop();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('handles translation in text', async () => {
|
|
43
|
+
const translation = await tolgee.translate('hello_world');
|
|
44
|
+
|
|
45
|
+
document.body.innerHTML = `<div id="test">${translation}</div>`;
|
|
46
|
+
|
|
47
|
+
await waitFor(() => {
|
|
48
|
+
return expect(
|
|
49
|
+
// @ts-ignore
|
|
50
|
+
document.getElementById('test')._tolgee
|
|
51
|
+
).not.toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(
|
|
55
|
+
decodeFromText(document.getElementById('test').textContent).length
|
|
56
|
+
).toEqual(0);
|
|
57
|
+
expect(document.getElementById('test').textContent).toEqual('Hello world!');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('handles missing translation in text', async () => {
|
|
61
|
+
const translation = await tolgee.translate('nonexistant');
|
|
62
|
+
|
|
63
|
+
document.body.innerHTML = `<div id="test">${translation}</div>`;
|
|
64
|
+
|
|
65
|
+
await waitFor(() => {
|
|
66
|
+
return expect(
|
|
67
|
+
// @ts-ignore
|
|
68
|
+
document.getElementById('test')._tolgee
|
|
69
|
+
).not.toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(
|
|
73
|
+
decodeFromText(document.getElementById('test').textContent).length
|
|
74
|
+
).toEqual(0);
|
|
75
|
+
expect(document.getElementById('test').textContent).toEqual('nonexistant');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('handles default value in text', async () => {
|
|
79
|
+
const translation = await tolgee.translate({
|
|
80
|
+
key: 'nonexistant',
|
|
81
|
+
defaultValue: 'Default value!',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
document.body.innerHTML = `<div id="test">${translation}</div>`;
|
|
85
|
+
|
|
86
|
+
await waitFor(() => {
|
|
87
|
+
return expect(
|
|
88
|
+
// @ts-ignore
|
|
89
|
+
document.getElementById('test')._tolgee
|
|
90
|
+
).not.toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(
|
|
94
|
+
decodeFromText(document.getElementById('test').textContent).length
|
|
95
|
+
).toEqual(0);
|
|
96
|
+
expect(document.getElementById('test').textContent).toEqual(
|
|
97
|
+
'Default value!'
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('handles translation in attribute', async () => {
|
|
102
|
+
const translation = await tolgee.translate('hello_world');
|
|
103
|
+
|
|
104
|
+
document.body.innerHTML = `<div id="test" title="${translation}"></div>`;
|
|
105
|
+
|
|
106
|
+
await waitFor(() => {
|
|
107
|
+
return expect(
|
|
108
|
+
// @ts-ignore
|
|
109
|
+
document.getElementById('test')._tolgee
|
|
110
|
+
).not.toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(
|
|
114
|
+
decodeFromText(document.getElementById('test').getAttribute('title'))
|
|
115
|
+
.length
|
|
116
|
+
).toEqual(0);
|
|
117
|
+
expect(document.getElementById('test').getAttribute('title')).toEqual(
|
|
118
|
+
'Hello world!'
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('handles translation on select', async () => {
|
|
123
|
+
const translation = await tolgee.translate('hello_world');
|
|
124
|
+
|
|
125
|
+
document.body.innerHTML = `
|
|
126
|
+
<select id="select">
|
|
127
|
+
<option id="option">${translation}</option>
|
|
128
|
+
</select>
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
await waitFor(() => {
|
|
132
|
+
return expect(
|
|
133
|
+
// @ts-ignore
|
|
134
|
+
document.getElementById('select')._tolgee
|
|
135
|
+
).not.toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(
|
|
139
|
+
decodeFromText(document.getElementById('option').textContent).length
|
|
140
|
+
).toEqual(0);
|
|
141
|
+
expect(document.getElementById('option').textContent).toEqual(
|
|
142
|
+
'Hello world!'
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const testConfig = {
|
|
2
|
+
id: 42,
|
|
3
|
+
key: 'asdfasdsfasdfasdfasdf',
|
|
4
|
+
username: 'john@toe@tolgee.com',
|
|
5
|
+
userFullName: 'John Doe',
|
|
6
|
+
projectId: 15,
|
|
7
|
+
projectName: 'Tolgee test',
|
|
8
|
+
scopes: [
|
|
9
|
+
'screenshots.view',
|
|
10
|
+
'screenshots.upload',
|
|
11
|
+
'screenshots.delete',
|
|
12
|
+
'keys.edit',
|
|
13
|
+
'translations.edit',
|
|
14
|
+
'translations.view',
|
|
15
|
+
],
|
|
16
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export default <T>(
|
|
2
|
+
implementationFn: () => Partial<T>,
|
|
3
|
+
baseConstructor?: new (...args) => any
|
|
4
|
+
) => {
|
|
5
|
+
function theMock() {
|
|
6
|
+
Object.assign(this, new baseConstructor());
|
|
7
|
+
Object.assign(this, implementationFn());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return jest.fn().mockImplementation(theMock);
|
|
11
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ElementWithMeta, NodeMeta, NodeWithMeta } from '../types';
|
|
2
|
+
import { TOLGEE_ATTRIBUTE_NAME } from '../Constants/Global';
|
|
3
|
+
|
|
4
|
+
export const createElement = (
|
|
5
|
+
nodesCount: number,
|
|
6
|
+
keysCount: number,
|
|
7
|
+
sameKeys = false
|
|
8
|
+
) => {
|
|
9
|
+
const mockedElement = document.createElement(
|
|
10
|
+
'div'
|
|
11
|
+
) as Element as ElementWithMeta;
|
|
12
|
+
|
|
13
|
+
let keyNum = 0;
|
|
14
|
+
|
|
15
|
+
const cn = (text) => {
|
|
16
|
+
const node = document.createTextNode(text) as Node as NodeWithMeta;
|
|
17
|
+
const keys = [];
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < keysCount; i++) {
|
|
20
|
+
keys.push({
|
|
21
|
+
key: `key${sameKeys ? `` : ` ${keyNum++}`}`,
|
|
22
|
+
params: { a: 'aaa' },
|
|
23
|
+
defaultValue: 'default value',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
node._tolgee = {
|
|
27
|
+
oldTextContent: `"${text}" before translation.`,
|
|
28
|
+
keys,
|
|
29
|
+
} as NodeMeta;
|
|
30
|
+
return node;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const nodes = [];
|
|
34
|
+
for (let i = 0; i < nodesCount; i++) {
|
|
35
|
+
nodes.push(cn(`text ${i}`));
|
|
36
|
+
}
|
|
37
|
+
mockedElement._tolgee = {
|
|
38
|
+
nodes: new Set(nodes),
|
|
39
|
+
};
|
|
40
|
+
mockedElement.append(...nodes);
|
|
41
|
+
mockedElement.setAttribute(TOLGEE_ATTRIBUTE_NAME, '');
|
|
42
|
+
return mockedElement;
|
|
43
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const createTestDom = (document: Document) => {
|
|
2
|
+
const c = {
|
|
3
|
+
keyInRoot: 'key_in_root',
|
|
4
|
+
keyInRootDiv: 'aaa_translate_Key.aaa',
|
|
5
|
+
hereKey: 'here',
|
|
6
|
+
hereTooKey: 'here_too',
|
|
7
|
+
appendedKey: 'appended_key',
|
|
8
|
+
ariaLabelKey: 'aria_label_key',
|
|
9
|
+
optionKey: 'option_key',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
document.body = document.createElement('body');
|
|
13
|
+
document.body.innerHTML = `{{${c.keyInRoot}}}' +
|
|
14
|
+
'<div id="rootDiv">Some trash... {{${c.keyInRootDiv}}}' +
|
|
15
|
+
' <div>Some other text to translate <span>{{${c.hereKey}}} and {{${c.hereTooKey}}}</span> text continues.'</div> +
|
|
16
|
+
' <div id="multipleTextNodes">Before text</div>' +
|
|
17
|
+
' <div aria-label="this is {{${c.ariaLabelKey}}} label"></div>' +
|
|
18
|
+
' <select><option>{{${c.optionKey}}}</option></select>'+
|
|
19
|
+
'<div>`;
|
|
20
|
+
document.getElementById('multipleTextNodes').append('some text node');
|
|
21
|
+
document.getElementById('multipleTextNodes').append(`{{${c.appendedKey}}}`);
|
|
22
|
+
document.getElementById('multipleTextNodes').append(` after text`);
|
|
23
|
+
|
|
24
|
+
return c;
|
|
25
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { TolgeeConfig } from '../TolgeeConfig';
|
|
2
|
+
import { Properties } from '../Properties';
|
|
3
|
+
import { CoreService } from '../services/CoreService';
|
|
4
|
+
import { Observer } from '../Observer';
|
|
5
|
+
import { mocked } from 'ts-jest/utils';
|
|
6
|
+
import { TranslationService } from '../services/TranslationService';
|
|
7
|
+
import { TextService } from '../services/TextService';
|
|
8
|
+
import { EventService } from '../services/EventService';
|
|
9
|
+
import { EventEmitterImpl } from '../services/EventEmitter';
|
|
10
|
+
import { ContentHandler } from '../wrappers/text/ContentHandler';
|
|
11
|
+
|
|
12
|
+
export const configMock = mocked(TolgeeConfig);
|
|
13
|
+
export const propertiesMock = mocked(Properties);
|
|
14
|
+
export const coreServiceMock = mocked(CoreService);
|
|
15
|
+
export const observerMock = mocked(Observer);
|
|
16
|
+
export const translationServiceMock = mocked(TranslationService);
|
|
17
|
+
export const textServiceMock = mocked(TextService);
|
|
18
|
+
export const eventServiceMock = mocked(EventService);
|
|
19
|
+
export const eventEmitterMock = mocked(EventEmitterImpl);
|
|
20
|
+
export const tolgeeConfigMock = mocked(TolgeeConfig);
|
|
21
|
+
export const textHandlerMock = mocked(ContentHandler);
|
|
22
|
+
|
|
23
|
+
export const getMockedInstance = <T>(constructor: new (...args) => T) => {
|
|
24
|
+
return mocked(constructor).mock.instances[0];
|
|
25
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
jest.dontMock('../helpers/NodeHelper.ts');
|
|
2
|
+
|
|
3
|
+
import { NodeHelper } from '../helpers/NodeHelper';
|
|
4
|
+
import { prettyDOM } from '@testing-library/dom';
|
|
5
|
+
|
|
6
|
+
expect.extend({
|
|
7
|
+
toBeFoundIn(xpath, contextNode) {
|
|
8
|
+
const result = NodeHelper.evaluate(xpath, contextNode);
|
|
9
|
+
if (result.length > 0 && document.contains(result[0])) {
|
|
10
|
+
return {
|
|
11
|
+
message: () =>
|
|
12
|
+
`Xpath ${xpath} should not be found in ${contextNode}\n\n${prettyDOM(
|
|
13
|
+
contextNode
|
|
14
|
+
)}`,
|
|
15
|
+
pass: true,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
message: () =>
|
|
20
|
+
`Xpath ${xpath} should be found in ${contextNode}\n\n${prettyDOM(
|
|
21
|
+
contextNode
|
|
22
|
+
)}`,
|
|
23
|
+
pass: false,
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
} as any);
|
|
27
|
+
|
|
28
|
+
declare global {
|
|
29
|
+
namespace jest {
|
|
30
|
+
interface Matchers<R> {
|
|
31
|
+
toBeFoundIn(contextNode: Node): R;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ArgumentTypes } from './commonTypes';
|
|
2
|
+
import { TOLGEE_TARGET_ATTRIBUTE } from '../Constants/Global';
|
|
3
|
+
|
|
4
|
+
export class NodeHelper {
|
|
5
|
+
static evaluate<T extends Node>(
|
|
6
|
+
...args: ArgumentTypes<typeof NodeHelper.evaluateGenerator>
|
|
7
|
+
): T[] {
|
|
8
|
+
return Array.from(this.evaluateGenerator(...args)) as T[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static evaluateToSingle<T extends Node>(
|
|
12
|
+
...args: ArgumentTypes<typeof NodeHelper.evaluateGenerator>
|
|
13
|
+
): T {
|
|
14
|
+
const arr = this.evaluate<T>(...args);
|
|
15
|
+
if (arr.length === 1) {
|
|
16
|
+
return arr[0];
|
|
17
|
+
}
|
|
18
|
+
if (arr.length < 1) {
|
|
19
|
+
throw new Error('No element found');
|
|
20
|
+
}
|
|
21
|
+
throw new Error('Multiple elements found');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public static closestElement(node: Element | Text) {
|
|
25
|
+
if (node instanceof Text) {
|
|
26
|
+
return node.parentElement;
|
|
27
|
+
}
|
|
28
|
+
return node;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static getParentElement(node: Node): Element | undefined {
|
|
32
|
+
if (node.parentElement) {
|
|
33
|
+
return node.parentElement;
|
|
34
|
+
}
|
|
35
|
+
if ((node as Attr).ownerElement) {
|
|
36
|
+
return (node as Attr).ownerElement;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static isElementTargetElement(element: Element): boolean {
|
|
41
|
+
return element.hasAttribute(TOLGEE_TARGET_ATTRIBUTE);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static markElementAsTargetElement(element: Element): void {
|
|
45
|
+
element.setAttribute(TOLGEE_TARGET_ATTRIBUTE, '');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static unmarkElementAsTargetElement(element: Element): void {
|
|
49
|
+
element.removeAttribute(TOLGEE_TARGET_ATTRIBUTE);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static nodeContains(descendant: Node, node: Node) {
|
|
53
|
+
if (descendant.contains(node)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
if (node instanceof Attr) {
|
|
57
|
+
const ownerContainsAttr =
|
|
58
|
+
node.ownerElement &&
|
|
59
|
+
Object.values(node.ownerElement.attributes).indexOf(node) > -1;
|
|
60
|
+
if (descendant.contains(node.ownerElement) && ownerContainsAttr) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private static *evaluateGenerator<T extends Node>(
|
|
68
|
+
expression: string,
|
|
69
|
+
targetNode: Node
|
|
70
|
+
): Generator<T> {
|
|
71
|
+
let node: Node;
|
|
72
|
+
const evaluated = document.evaluate(
|
|
73
|
+
expression,
|
|
74
|
+
targetNode,
|
|
75
|
+
undefined,
|
|
76
|
+
XPathResult.ANY_TYPE
|
|
77
|
+
);
|
|
78
|
+
while ((node = evaluated.iterateNext()) !== null) {
|
|
79
|
+
yield node as T;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static getNodeText(node: Node) {
|
|
84
|
+
return node.textContent;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static setNodeText(node: Node, text: string) {
|
|
88
|
+
node.textContent = text;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
jest.dontMock('./TextHelper');
|
|
2
|
+
|
|
3
|
+
import { TextHelper } from './TextHelper';
|
|
4
|
+
|
|
5
|
+
describe('TextHelper', () => {
|
|
6
|
+
describe('will split', () => {
|
|
7
|
+
test('on non escaped properly', () => {
|
|
8
|
+
const strings = TextHelper.splitOnNonEscapedDelimiter(
|
|
9
|
+
'text.to.split',
|
|
10
|
+
'.'
|
|
11
|
+
);
|
|
12
|
+
expect(strings).toEqual(['text', 'to', 'split']);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('on escaped and non escaped properly', () => {
|
|
16
|
+
const strings = TextHelper.splitOnNonEscapedDelimiter(
|
|
17
|
+
'text\\.to\\.split',
|
|
18
|
+
'.'
|
|
19
|
+
);
|
|
20
|
+
expect(strings).toEqual(['text.to.split']);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('empty string properly', () => {
|
|
24
|
+
const strings = TextHelper.splitOnNonEscapedDelimiter('', '.');
|
|
25
|
+
expect(strings).toEqual(['']);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('correctly when it begins with escape', async () => {
|
|
29
|
+
const strings = TextHelper.splitOnNonEscapedDelimiter('\\.aa', '.');
|
|
30
|
+
expect(strings).toEqual(['.aa']);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('correctly when it ends with escape', async () => {
|
|
34
|
+
const strings = TextHelper.splitOnNonEscapedDelimiter('aa\\.', '.');
|
|
35
|
+
expect(strings).toEqual(['aa.']);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('correctly when it contains escaped escape character', async () => {
|
|
39
|
+
const strings = TextHelper.splitOnNonEscapedDelimiter('aa\\\\.', '.');
|
|
40
|
+
expect(strings).toEqual(['aa\\', '']);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('correctly when it contains escaped escape character and delimiter is escaped', async () => {
|
|
44
|
+
const strings = TextHelper.splitOnNonEscapedDelimiter('aa\\\\\\.', '.');
|
|
45
|
+
expect(strings).toEqual(['aa\\.']);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('It remove escapes', () => {
|
|
50
|
+
test('basically', async () => {
|
|
51
|
+
expect(TextHelper.removeEscapes('t\\t')).toEqual('tt');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('not if escape character is escaped', async () => {
|
|
55
|
+
expect(TextHelper.removeEscapes('\\\\')).toEqual('\\');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('if there is escaped escaped character', async () => {
|
|
59
|
+
expect(TextHelper.removeEscapes('\\\\\\')).toEqual('\\');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export class TextHelper {
|
|
2
|
+
public static splitOnNonEscapedDelimiter(
|
|
3
|
+
string: string,
|
|
4
|
+
delimiter: string
|
|
5
|
+
): string[] {
|
|
6
|
+
const result = [];
|
|
7
|
+
let actual = '';
|
|
8
|
+
let escaped = false;
|
|
9
|
+
for (let i = 0; i < string.length; i++) {
|
|
10
|
+
const char = string[i];
|
|
11
|
+
if (char === '\\' && !escaped) {
|
|
12
|
+
escaped = true;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (escaped) {
|
|
16
|
+
escaped = false;
|
|
17
|
+
actual += char;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (char === delimiter) {
|
|
21
|
+
result.push(actual);
|
|
22
|
+
actual = '';
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
actual += char;
|
|
26
|
+
}
|
|
27
|
+
result.push(actual);
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public static isCharEscaped(position: number, fullString: string) {
|
|
32
|
+
let escapeCharsCount = 0;
|
|
33
|
+
while (position > -1 && fullString[position - 1] === '\\') {
|
|
34
|
+
escapeCharsCount++;
|
|
35
|
+
position--;
|
|
36
|
+
}
|
|
37
|
+
return escapeCharsCount % 2 == 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public static removeEscapes(string: string) {
|
|
41
|
+
let result = '';
|
|
42
|
+
let escaped = false;
|
|
43
|
+
for (let i = 0; i < string.length; i++) {
|
|
44
|
+
const char = string[i];
|
|
45
|
+
if (char === '\\' && !escaped) {
|
|
46
|
+
escaped = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (escaped) {
|
|
50
|
+
escaped = false;
|
|
51
|
+
result += char;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
result += char;
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
}
|