selenium-webdriver 4.43.0 → 4.45.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/CHANGES.md +21 -0
- package/README.md +57 -185
- package/bidi/browsingContextInspector.js +1 -1
- package/bidi/generated/bluetooth.d.ts +223 -0
- package/bidi/generated/bluetooth.js +154 -0
- package/bidi/generated/browser.d.ts +104 -0
- package/bidi/generated/browser.js +107 -0
- package/bidi/generated/browsing_context.d.ts +394 -0
- package/bidi/generated/browsing_context.js +260 -0
- package/bidi/generated/common.d.ts +38 -0
- package/bidi/generated/common.js +30 -0
- package/bidi/generated/emulation.d.ts +176 -0
- package/bidi/generated/emulation.js +133 -0
- package/bidi/generated/input.d.ts +151 -0
- package/bidi/generated/input.js +81 -0
- package/bidi/generated/log.d.ts +39 -0
- package/bidi/generated/log.js +67 -0
- package/bidi/generated/network.d.ts +359 -0
- package/bidi/generated/network.js +230 -0
- package/bidi/generated/permissions.d.ts +22 -0
- package/bidi/generated/permissions.js +43 -0
- package/bidi/generated/script.d.ts +410 -0
- package/bidi/generated/script.js +146 -0
- package/bidi/generated/session.d.ts +119 -0
- package/bidi/generated/session.js +82 -0
- package/bidi/generated/speculation.d.ts +17 -0
- package/bidi/generated/speculation.js +40 -0
- package/bidi/generated/storage.d.ts +84 -0
- package/bidi/generated/storage.js +70 -0
- package/bidi/generated/user_agent_client_hints.d.ts +33 -0
- package/bidi/generated/user_agent_client_hints.js +43 -0
- package/bidi/generated/webextension.d.ts +42 -0
- package/bidi/generated/webextension.js +53 -0
- package/bidi/index.js +133 -21
- package/bin/linux/selenium-manager +0 -0
- package/bin/macos/selenium-manager +0 -0
- package/bin/windows/selenium-manager.exe +0 -0
- package/common/driverFinder.js +2 -1
- package/common/seleniumManager.js +1 -1
- package/eslint.config.js +5 -5
- package/io/zip.js +1 -1
- package/lib/atoms/find-elements.js +272 -68
- package/lib/atoms/get-attribute.js +152 -17
- package/lib/atoms/is-displayed.js +385 -39
- package/lib/http.js +2 -1
- package/lib/pinnedScript.js +0 -1
- package/lib/webdriver.js +2 -2
- package/package.json +18 -16
package/common/driverFinder.js
CHANGED
|
@@ -35,10 +35,11 @@ function getBinaryPaths(capabilities) {
|
|
|
35
35
|
const args = getArgs(capabilities)
|
|
36
36
|
return binaryPaths(args)
|
|
37
37
|
} catch (e) {
|
|
38
|
-
throw Error(
|
|
38
|
+
throw new Error(
|
|
39
39
|
`Unable to obtain browser driver.
|
|
40
40
|
For more information on how to install drivers see
|
|
41
41
|
https://www.selenium.dev/documentation/webdriver/troubleshooting/errors/driver_location/. ${e}`,
|
|
42
|
+
{ cause: e },
|
|
42
43
|
)
|
|
43
44
|
}
|
|
44
45
|
}
|
|
@@ -90,7 +90,7 @@ function binaryPaths(args) {
|
|
|
90
90
|
try {
|
|
91
91
|
output = JSON.parse(spawnResult.stdout.toString())
|
|
92
92
|
} catch (e) {
|
|
93
|
-
throw new Error(`Error executing command for ${smBinary} with ${args}: ${e.toString()}
|
|
93
|
+
throw new Error(`Error executing command for ${smBinary} with ${args}: ${e.toString()}`, { cause: e })
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
logOutput(output)
|
package/eslint.config.js
CHANGED
|
@@ -19,13 +19,13 @@ const globals = require('globals')
|
|
|
19
19
|
const noOnlyTests = require('eslint-plugin-no-only-tests')
|
|
20
20
|
const js = require('@eslint/js')
|
|
21
21
|
const eslintPluginPrettierRecommended = require('eslint-plugin-prettier/recommended')
|
|
22
|
-
const mochaPlugin = require('eslint-plugin-mocha')
|
|
23
|
-
const nodePlugin = require('eslint-plugin-n')
|
|
22
|
+
const mochaPlugin = require('eslint-plugin-mocha').default
|
|
23
|
+
const nodePlugin = require('eslint-plugin-n').default
|
|
24
24
|
|
|
25
25
|
module.exports = [
|
|
26
26
|
js.configs.recommended,
|
|
27
27
|
eslintPluginPrettierRecommended,
|
|
28
|
-
mochaPlugin.configs.
|
|
28
|
+
mochaPlugin.configs.recommended,
|
|
29
29
|
nodePlugin.configs['flat/recommended-script'],
|
|
30
30
|
{
|
|
31
31
|
languageOptions: {
|
|
@@ -80,18 +80,18 @@ module.exports = [
|
|
|
80
80
|
},
|
|
81
81
|
],
|
|
82
82
|
'n/prefer-node-protocol': ['error'],
|
|
83
|
-
'mocha/no-skipped-tests': ['off'],
|
|
84
83
|
'mocha/no-mocha-arrows': ['off'],
|
|
85
84
|
'mocha/no-setup-in-describe': ['off'],
|
|
86
85
|
'mocha/no-top-level-hooks': ['off'],
|
|
87
86
|
'mocha/no-sibling-hooks': ['off'],
|
|
88
87
|
'mocha/no-exports': ['off'],
|
|
89
|
-
'mocha/no-empty-
|
|
88
|
+
'mocha/no-empty-title': ['off'],
|
|
90
89
|
'mocha/max-top-level-suites': ['off'],
|
|
91
90
|
'mocha/consistent-spacing-between-blocks': ['off'],
|
|
92
91
|
'mocha/no-nested-tests': ['off'],
|
|
93
92
|
'mocha/no-pending-tests': ['off'],
|
|
94
93
|
'mocha/no-identical-title': ['off'],
|
|
94
|
+
'mocha/no-async-suite': ['off'],
|
|
95
95
|
'prettier/prettier': [
|
|
96
96
|
'error',
|
|
97
97
|
{
|
package/io/zip.js
CHANGED
|
@@ -1,69 +1,273 @@
|
|
|
1
1
|
// GENERATED CODE - DO NOT EDIT
|
|
2
|
-
module.exports = function()
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
2
|
+
module.exports = (function findElements(target, root) {
|
|
3
|
+
const INVALID_SELECTOR = 'invalid selector';
|
|
4
|
+
const INVALID_ARGUMENT = 'invalid argument';
|
|
5
|
+
const NO_SUCH_ELEMENT = 'no such element';
|
|
6
|
+
function botError(code, message) {
|
|
7
|
+
const err = new Error(message);
|
|
8
|
+
err['code'] = code;
|
|
9
|
+
return err;
|
|
10
|
+
}
|
|
11
|
+
function cssEscapeId(s) {
|
|
12
|
+
return s.replace(/[\s'"\\#.:;,!?+<>=~*^$|%&@`{}\-/\[\]()]/g, (c) => '\\' + c);
|
|
13
|
+
}
|
|
14
|
+
function classNameMany(target, root) {
|
|
15
|
+
if (!target) {
|
|
16
|
+
throw botError(INVALID_SELECTOR, 'No class name specified');
|
|
17
|
+
}
|
|
18
|
+
target = target.trim();
|
|
19
|
+
if (target.indexOf(' ') !== -1) {
|
|
20
|
+
throw botError(INVALID_SELECTOR, 'Compound class names not permitted');
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return Array.from(root.querySelectorAll('.' + target.replace(/\\/g, '\\\\').replace(/\./g, '\\.')));
|
|
24
|
+
}
|
|
25
|
+
catch (_e) {
|
|
26
|
+
throw botError(INVALID_SELECTOR, 'An invalid or illegal class name was specified');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function cssMany(target, root) {
|
|
30
|
+
try {
|
|
31
|
+
return Array.from(root.querySelectorAll(target));
|
|
32
|
+
}
|
|
33
|
+
catch (_e) {
|
|
34
|
+
throw botError(INVALID_SELECTOR, 'An invalid or illegal CSS selector was specified: ' + target);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function idMany(target, root) {
|
|
38
|
+
if (!target) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
if (!/^\d/.test(target)) {
|
|
42
|
+
try {
|
|
43
|
+
return Array.from(root.querySelectorAll('#' + cssEscapeId(target)));
|
|
44
|
+
}
|
|
45
|
+
catch (_e) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return Array.from(root.querySelectorAll('*')).filter(el => el.getAttribute('id') === target);
|
|
50
|
+
}
|
|
51
|
+
function getLinkText(el) {
|
|
52
|
+
const text = el.innerText !== undefined ? el.innerText : el.textContent || '';
|
|
53
|
+
return text.replace(/^[\s]+|[\s]+$/g, '');
|
|
54
|
+
}
|
|
55
|
+
function linkTextMany(target, root, partial) {
|
|
56
|
+
return Array.from(root.querySelectorAll('a')).filter(el => {
|
|
57
|
+
const text = getLinkText(el);
|
|
58
|
+
return partial ? text.indexOf(target) !== -1 : text === target;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function nameMany(target, root) {
|
|
62
|
+
return Array.from(root.querySelectorAll('[name="' + target.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"]'));
|
|
63
|
+
}
|
|
64
|
+
function tagNameMany(target, root) {
|
|
65
|
+
if (target === '') {
|
|
66
|
+
throw botError(INVALID_SELECTOR, 'Unable to locate an element with the tagName ""');
|
|
67
|
+
}
|
|
68
|
+
return Array.from(root.getElementsByTagName(target));
|
|
69
|
+
}
|
|
70
|
+
const DEFAULT_NS_RESOLVER = (function () {
|
|
71
|
+
const ns = { svg: 'http://www.w3.org/2000/svg' };
|
|
72
|
+
return (prefix) => ns[prefix] || null;
|
|
73
|
+
})();
|
|
74
|
+
const ORDERED_NODE_SNAPSHOT_TYPE = 7;
|
|
75
|
+
function xpathMany(target, root) {
|
|
76
|
+
const doc = root.documentElement ? root : root.ownerDocument;
|
|
77
|
+
if (!doc.documentElement) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
let resolver = null;
|
|
82
|
+
if (target.indexOf(':') !== -1) {
|
|
83
|
+
const reversedNs = {};
|
|
84
|
+
const allNodes = doc.getElementsByTagName('*');
|
|
85
|
+
for (let i = 0; i < allNodes.length; i++) {
|
|
86
|
+
const n = allNodes[i];
|
|
87
|
+
const ns = n.namespaceURI;
|
|
88
|
+
if (ns && !reversedNs[ns]) {
|
|
89
|
+
let prefix = n.lookupPrefix(ns);
|
|
90
|
+
if (!prefix) {
|
|
91
|
+
const m = ns.match('.*/(\\w+)/?$');
|
|
92
|
+
prefix = m ? m[1] : 'xhtml';
|
|
93
|
+
}
|
|
94
|
+
reversedNs[ns] = prefix;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const namespaces = {};
|
|
98
|
+
for (const key in reversedNs) {
|
|
99
|
+
namespaces[reversedNs[key]] = key;
|
|
100
|
+
}
|
|
101
|
+
resolver = (prefix) => namespaces[prefix || ''] || null;
|
|
102
|
+
}
|
|
103
|
+
let result = null;
|
|
104
|
+
try {
|
|
105
|
+
result = doc.evaluate(target, root, resolver, ORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
106
|
+
}
|
|
107
|
+
catch (te) {
|
|
108
|
+
if (te.name === 'TypeError') {
|
|
109
|
+
const fallback = doc.createNSResolver ? doc.createNSResolver(doc.documentElement) : DEFAULT_NS_RESOLVER;
|
|
110
|
+
result = doc.evaluate(target, root, fallback, ORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
throw te;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!result) {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
const results = [];
|
|
120
|
+
for (let i = 0; i < result.snapshotLength; i++) {
|
|
121
|
+
const node = result.snapshotItem(i);
|
|
122
|
+
if (!node || node.nodeType !== Node.ELEMENT_NODE) {
|
|
123
|
+
throw botError(INVALID_SELECTOR, 'The result of the xpath expression "' + target + '" is: ' + node + '. It should be an element.');
|
|
124
|
+
}
|
|
125
|
+
results.push(node);
|
|
126
|
+
}
|
|
127
|
+
return results;
|
|
128
|
+
}
|
|
129
|
+
catch (ex) {
|
|
130
|
+
if (ex.name === 'NS_ERROR_ILLEGAL_VALUE') {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
if (ex.code === INVALID_SELECTOR) {
|
|
134
|
+
throw ex;
|
|
135
|
+
}
|
|
136
|
+
throw botError(INVALID_SELECTOR, 'Unable to locate an element with the xpath expression ' + target + ' because of the following error:\n' + ex);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function getClientRect(element) {
|
|
140
|
+
const r = element.getBoundingClientRect();
|
|
141
|
+
return { left: r.left, top: r.top, width: r.width, height: r.height };
|
|
142
|
+
}
|
|
143
|
+
function resolveAnchor(selector) {
|
|
144
|
+
if (selector instanceof Element) {
|
|
145
|
+
return selector;
|
|
146
|
+
}
|
|
147
|
+
if (typeof selector === 'function') {
|
|
148
|
+
return resolveAnchor(selector());
|
|
149
|
+
}
|
|
150
|
+
if (selector && typeof selector === 'object') {
|
|
151
|
+
const found = findElements(selector);
|
|
152
|
+
if (!found.length) {
|
|
153
|
+
throw botError(NO_SUCH_ELEMENT, 'No element has been found by ' + JSON.stringify(selector));
|
|
154
|
+
}
|
|
155
|
+
return found[0];
|
|
156
|
+
}
|
|
157
|
+
throw botError(INVALID_ARGUMENT, 'Selector is of wrong type: ' + JSON.stringify(selector));
|
|
158
|
+
}
|
|
159
|
+
function makeProximityFilter(selector, test) {
|
|
160
|
+
return (candidate) => test(getClientRect(resolveAnchor(selector)), getClientRect(candidate));
|
|
161
|
+
}
|
|
162
|
+
const RELATIVE_STRATEGIES = {
|
|
163
|
+
above: (sel) => makeProximityFilter(sel, (a, c) => c.top + c.height <= a.top),
|
|
164
|
+
below: (sel) => makeProximityFilter(sel, (a, c) => c.top >= a.top + a.height),
|
|
165
|
+
left: (sel) => makeProximityFilter(sel, (a, c) => c.left + c.width <= a.left),
|
|
166
|
+
right: (sel) => makeProximityFilter(sel, (a, c) => c.left >= a.left + a.width),
|
|
167
|
+
near: (sel, distArg) => {
|
|
168
|
+
const distFromSelector = typeof sel['distance'] === 'number'
|
|
169
|
+
? sel['distance']
|
|
170
|
+
: 0;
|
|
171
|
+
const distance = typeof distArg === 'number' && distArg > 0 ? distArg : distFromSelector || 50;
|
|
172
|
+
return (candidate) => {
|
|
173
|
+
const anchor = resolveAnchor(sel);
|
|
174
|
+
if (anchor === candidate)
|
|
175
|
+
return false;
|
|
176
|
+
const a = getClientRect(anchor);
|
|
177
|
+
const c = getClientRect(candidate);
|
|
178
|
+
const expanded = { left: a.left - distance, top: a.top - distance, width: a.width + distance * 2, height: a.height + distance * 2 };
|
|
179
|
+
return (c.left < expanded.left + expanded.width &&
|
|
180
|
+
c.left + c.width > expanded.left &&
|
|
181
|
+
c.top < expanded.top + expanded.height &&
|
|
182
|
+
c.top + c.height > expanded.top);
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
straightAbove: (sel) => makeProximityFilter(sel, (a, c) => c.left < a.left + a.width && c.left + c.width > a.left && c.top + c.height <= a.top),
|
|
186
|
+
straightBelow: (sel) => makeProximityFilter(sel, (a, c) => c.left < a.left + a.width && c.left + c.width > a.left && c.top >= a.top + a.height),
|
|
187
|
+
straightLeft: (sel) => makeProximityFilter(sel, (a, c) => c.top < a.top + a.height && c.top + c.height > a.top && c.left + c.width <= a.left),
|
|
188
|
+
straightRight: (sel) => makeProximityFilter(sel, (a, c) => c.top < a.top + a.height && c.top + c.height > a.top && c.left >= a.left + a.width),
|
|
189
|
+
};
|
|
190
|
+
function sortByProximity(anchor, elements) {
|
|
191
|
+
const ar = getClientRect(anchor);
|
|
192
|
+
const acx = ar.left + Math.max(1, ar.width) / 2;
|
|
193
|
+
const acy = ar.top + Math.max(1, ar.height) / 2;
|
|
194
|
+
return elements.slice().sort((a, b) => {
|
|
195
|
+
const ra = getClientRect(a);
|
|
196
|
+
const rb = getClientRect(b);
|
|
197
|
+
const da = Math.sqrt(Math.pow(acx - (ra.left + Math.max(1, ra.width) / 2), 2) + Math.pow(acy - (ra.top + Math.max(1, ra.height) / 2), 2));
|
|
198
|
+
const db = Math.sqrt(Math.pow(acx - (rb.left + Math.max(1, rb.width) / 2), 2) + Math.pow(acy - (rb.top + Math.max(1, rb.height) / 2), 2));
|
|
199
|
+
return da - db;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function relativeMany(target, root) {
|
|
203
|
+
if (!Object.prototype.hasOwnProperty.call(target, 'root') || !Object.prototype.hasOwnProperty.call(target, 'filters')) {
|
|
204
|
+
throw botError(INVALID_ARGUMENT, 'Locator not suitable for relative locators: ' + JSON.stringify(target));
|
|
205
|
+
}
|
|
206
|
+
const filters = target['filters'];
|
|
207
|
+
if (!Array.isArray(filters)) {
|
|
208
|
+
throw botError(INVALID_ARGUMENT, 'Targets should be an array: ' + JSON.stringify(target));
|
|
209
|
+
}
|
|
210
|
+
let elements;
|
|
211
|
+
const rootTarget = target['root'];
|
|
212
|
+
if (rootTarget instanceof Element) {
|
|
213
|
+
elements = [rootTarget];
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
elements = findElements(rootTarget, root);
|
|
217
|
+
}
|
|
218
|
+
if (!elements.length) {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
const matched = elements.filter(el => {
|
|
222
|
+
if (!el)
|
|
223
|
+
return false;
|
|
224
|
+
return filters.every(filter => {
|
|
225
|
+
const strategy = RELATIVE_STRATEGIES[filter.kind];
|
|
226
|
+
if (!strategy) {
|
|
227
|
+
throw botError(INVALID_ARGUMENT, 'Cannot find filter suitable for ' + filter.kind);
|
|
228
|
+
}
|
|
229
|
+
return strategy(...filter.args)(el);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
const finalFilter = filters[filters.length - 1];
|
|
233
|
+
if (!finalFilter || !RELATIVE_STRATEGIES[finalFilter.kind]) {
|
|
234
|
+
return matched;
|
|
235
|
+
}
|
|
236
|
+
const lastAnchor = resolveAnchor(finalFilter.args[0]);
|
|
237
|
+
return sortByProximity(lastAnchor, matched);
|
|
238
|
+
}
|
|
239
|
+
const actualRoot = root || document;
|
|
240
|
+
const keys = Object.keys(target).filter(k => Object.prototype.hasOwnProperty.call(target, k));
|
|
241
|
+
if (!keys.length) {
|
|
242
|
+
throw botError(INVALID_ARGUMENT, 'Unsupported locator strategy: (empty)');
|
|
243
|
+
}
|
|
244
|
+
const key = keys[0];
|
|
245
|
+
const value = target[key];
|
|
246
|
+
switch (key) {
|
|
247
|
+
case 'className':
|
|
248
|
+
case 'class name':
|
|
249
|
+
return classNameMany(value, actualRoot);
|
|
250
|
+
case 'css':
|
|
251
|
+
case 'css selector':
|
|
252
|
+
return cssMany(value, actualRoot);
|
|
253
|
+
case 'id':
|
|
254
|
+
return idMany(value, actualRoot);
|
|
255
|
+
case 'linkText':
|
|
256
|
+
case 'link text':
|
|
257
|
+
return linkTextMany(value, actualRoot, false);
|
|
258
|
+
case 'partialLinkText':
|
|
259
|
+
case 'partial link text':
|
|
260
|
+
return linkTextMany(value, actualRoot, true);
|
|
261
|
+
case 'name':
|
|
262
|
+
return nameMany(value, actualRoot);
|
|
263
|
+
case 'tagName':
|
|
264
|
+
case 'tag name':
|
|
265
|
+
return tagNameMany(value, actualRoot);
|
|
266
|
+
case 'xpath':
|
|
267
|
+
return xpathMany(value, actualRoot);
|
|
268
|
+
case 'relative':
|
|
269
|
+
return relativeMany(value, actualRoot);
|
|
270
|
+
default:
|
|
271
|
+
throw botError(INVALID_ARGUMENT, 'Unsupported locator strategy: ' + key);
|
|
272
|
+
}
|
|
273
|
+
});
|