-
-
Notifications
You must be signed in to change notification settings - Fork 41
/
injectUI5.js
385 lines (353 loc) · 18.5 KB
/
injectUI5.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
async function clientSide_injectUI5(config, waitForUI5Timeout, browserInstance) {
return await browserInstance.executeAsync((waitForUI5Timeout, done) => {
if (window.bridge) {
// setup sap testing already done
done(true)
}
if (!window.sap || !window.sap.ui) {
// setup sap testing already cant be done due to sap namespace not present on the page
console.error("[browser wdi5] ERR: no ui5 present on page")
// only condition where to cancel the setup process
done(false)
}
// attach the function to be able to use the extracted method later
if (!window.bridge) {
// create empty
window.wdi5 = {
createMatcher: null,
isInitialized: false,
Log: null,
waitForUI5Options: {
timeout: waitForUI5Timeout,
interval: 400
},
objectMap: {
// GUID: {}
},
bWaitStarted: false,
asyncControlRetrievalQueue: []
}
/**
*
* @param {sap.ui.base.Object} object
* @returns uuid
*/
window.wdi5.saveObject = (object) => {
// This is a manual replacement for crypto.randomUUID()
// until it is only available in secure contexts.
// See https://github.com/WICG/uuid/issues/23
const uuid = ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
( c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16) )
window.wdi5.objectMap[uuid] = object
return uuid
}
// load UI5 logger
sap.ui.require(["sap/base/Log"], (Log) => {
// Logger is loaded -> can be use internally
// attach logger to wdi5 to be able to use it globally
window.wdi5.Log = Log
window.wdi5.Log.info("[browser wdi5] injected!")
})
sap.ui.require(["sap/ui/test/autowaiter/_autoWaiterAsync"], (_autoWaiterAsync) => {
window.wdi5.waitForUI5 = function (oOptions, callback, errorCallback) {
oOptions = oOptions || {}
_autoWaiterAsync.extendConfig(oOptions)
const startWaiting = function () {
window.wdi5.bWaitStarted = true;
_autoWaiterAsync.waitAsync(function (sError) {
const nextWaitAsync = window.wdi5.asyncControlRetrievalQueue.shift();
if (nextWaitAsync) {
setTimeout(nextWaitAsync); //use setTimeout to postpone execution to the next event cycle, so that bWaitStarted in the UI5 _autoWaiterAsync is also set to false first
} else {
window.wdi5.bWaitStarted = false;
}
if (sError) {
errorCallback(new Error(sError))
} else {
callback()
}
})
}
if (!window.wdi5.bWaitStarted) {
startWaiting();
} else {
window.wdi5.asyncControlRetrievalQueue.push(startWaiting);
}
}
window.wdi5.Log.info("[browser wdi5] window._autoWaiterAsync used in waitForUI5 function")
})
// attach new bridge
sap.ui.require(["sap/ui/test/RecordReplay"], (RecordReplay) => {
window.bridge = RecordReplay
window.fe_bridge = {} // empty init for fiori elements test api
window.wdi5.Log.info("[browser wdi5] APIs injected!")
window.wdi5.isInitialized = true
// here setup is successful
// known side effect this call triggers the back to node scope, the other sap.ui.require continue to run in background in browser scope
done(true)
})
// make sure the resources are required
// TODO: "sap/ui/test/matchers/Sibling",
sap.ui.require(
[
"sap/ui/test/matchers/BindingPath",
"sap/ui/test/matchers/I18NText",
"sap/ui/test/matchers/Properties",
"sap/ui/test/matchers/Ancestor",
"sap/ui/test/matchers/LabelFor",
"sap/ui/test/matchers/Descendant",
"sap/ui/test/matchers/Interactable"
],
(BindingPath, I18NText, Properties, Ancestor, LabelFor, Descendant, Interactable) => {
/**
* used to dynamically create new control matchers when searching for elements
*/
window.wdi5.createMatcher = (oSelector) => {
// since 1.72.0 the declarative matchers are available. Before that
// you had to instantiate the matchers manually
const oldAPIVersion = "1.72.0"
// check whether we're looking for a control via regex
// hint: no IE support here :)
if (oSelector.id && oSelector.id.startsWith("/", 0)) {
const [sTarget, sRegEx, sFlags] = oSelector.id.match(/\/(.*)\/(.*)/)
oSelector.id = new RegExp(sRegEx, sFlags)
}
// match a regular regex as (partial) matcher
// properties: {
// text: /.*ersi.*/gm
// }
// but not a declarative style regex matcher
// properties: {
// text: {
// regex: {
// source: '.*ersi.*',
// flags: 'gm'
// }
// }
// }
if (
typeof oSelector.properties?.text === "string" &&
oSelector.properties?.text.startsWith("/", 0)
) {
const [_, sRegEx, sFlags] = oSelector.properties.text.match(/\/(.*)\/(.*)/)
oSelector.properties.text = new RegExp(sRegEx, sFlags)
}
if (oSelector.bindingPath) {
// TODO: for the binding Path there is no object creation
// fix (?) for 'leading slash issue' in propertyPath w/ a named model
// openui5 issue in github is open
const hasNamedModel =
oSelector.bindingPath.modelName && oSelector.bindingPath.modelName.length > 0
const isRootProperty =
oSelector.bindingPath.propertyPath &&
oSelector.bindingPath.propertyPath.charAt(0) === "/"
if (
hasNamedModel &&
isRootProperty &&
window.compareVersions.compare("1.81.0", sap.ui.version, ">")
) {
// attach the double leading /
// for UI5 < 1.81
oSelector.bindingPath.propertyPath = `/${oSelector.bindingPath.propertyPath}`
}
}
if (window.compareVersions.compare(oldAPIVersion, sap.ui.version, ">")) {
oSelector.matchers = []
// for version < 1.72 declarative matchers are not available
if (oSelector.bindingPath) {
oSelector.matchers.push(new BindingPath(oSelector.bindingPath))
delete oSelector.bindingPath
}
if (oSelector.properties) {
oSelector.matchers.push(new Properties(oSelector.properties))
delete oSelector.properties
}
if (oSelector.i18NText) {
oSelector.matchers.push(new I18NText(oSelector.i18NText))
delete oSelector.i18NText
}
if (oSelector.labelFor) {
oSelector.matchers.push(new LabelFor(oSelector.labelFor))
delete oSelector.labelFor
}
if (oSelector.ancestor) {
oSelector.matchers.push(new Ancestor(oSelector.ancestor))
delete oSelector.ancestor
}
}
/*
oSelector.matchers = []
// since for these matcher a constructor call is neccessary
if (oSelector.sibling && oSelector.sibling.options) {
// don't construct matcher if not needed
const options = oSelector.sibling.options
delete oSelector.sibling.options
oSelector.matchers.push(new Sibling(oSelector.sibling, options))
delete oSelector.sibling
}
if (oSelector.descendant && (typeof oSelector.descendant.bDirect !== 'undefined')) {
// don't construct matcher if not needed
const bDirect = oSelector.descendant.bDirect
delete oSelector.descendant.bDirect
oSelector.matchers.push(new Descendant(oSelector.descendant, !!bDirect))
delete oSelector.descendant
}
if (oSelector.ancestor && (typeof oSelector.ancestor.bDirect !== 'undefined')) {
// don't construct matcher if not needed
const bDirect = oSelector.ancestor.bDirect
delete oSelector.ancestor.bDirect
oSelector.matchers.push(new Ancestor(oSelector.ancestor, !!bDirect))
delete oSelector.ancestor
}
*/
return oSelector
}
/**
* extract the multi use function to get a UI5 Control from a JSON Webobejct
*/
window.wdi5.getUI5CtlForWebObj = (ui5Control) => {
return jQuery(ui5Control).control(0)
}
/**
* gets a UI5 controls' methods to proxy from browser- to Node.js-runtime
*
* @param {sap.<lib>.<Control>} control UI5 control
* @returns {String[]} UI5 control's method names
*/
window.wdi5.retrieveControlMethods = (control) => {
// create keys of all parent prototypes
let properties = new Set()
let currentObj = control
do {
Object.getOwnPropertyNames(currentObj).map((item) => properties.add(item))
} while ((currentObj = Object.getPrototypeOf(currentObj)))
// filter for:
// @ts-ignore
let controlMethodsToProxy = [...properties.keys()].filter((item) => {
if (typeof control[item] === "function") {
// function
// filter private methods
if (item.startsWith("_")) {
return false
}
if (item.indexOf("Render") !== -1) {
return false
}
// filter not working methods
// and those with a specific api from wdi5/wdio-ui5-service
// prevent overwriting wdi5-control's own init method
const aFilterFunctions = ["$", "getAggregation", "constructor", "fireEvent", "init"]
if (aFilterFunctions.includes(item)) {
return false
}
// if not already discarded -> should be in the result
return true
}
return false
})
return controlMethodsToProxy
}
/**
* flatten all functions and properties on the Prototype directly into the returned object
* @param {object} obj
* @returns {object} all functions and properties of the inheritance chain in a flat structure
*/
window.wdi5.collapseObject = (obj) => {
let protoChain = []
let proto = obj
while (proto !== null) {
protoChain.unshift(proto)
proto = Object.getPrototypeOf(proto)
}
let collapsedObj = {}
protoChain.forEach((prop) => Object.assign(collapsedObj, prop))
return collapsedObj
}
/**
* used as a replacer function in JSON.stringify
* removes circular references in an object
* all credit to https://bobbyhadz.com/blog/javascript-typeerror-converting-circular-structure-to-json
*/
window.wdi5.getCircularReplacer = () => {
const seen = new WeakSet()
return (key, value) => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return
}
seen.add(value)
}
return value
}
}
/**
* if parameter is JS primitive type
* returns {boolean}
* @param {*} test
*/
window.wdi5.isPrimitive = (test) => {
return test !== Object(test)
}
/**
* creates a array of objects containing their id as a property
* @param {[sap.ui.core.Control]} aControls
* @throws {Error} error if the aggregation was not found that has to be catched
* @return {Array} Object
*/
window.wdi5.createControlIdMap = (aControls, controlType = "") => {
// the array of UI5 controls need to be mapped (remove circular reference)
if (!aControls) {
throw new Error("Aggregation was not found!")
}
return aControls.map((element) => {
// just use the absolute ID of the control
if (
(controlType === "sap.m.ComboBox" || controlType === "sap.m.MultiComboBox") &&
element.data("InputWithSuggestionsListItem")
) {
return {
id: element.data("InputWithSuggestionsListItem").getId()
}
} else if (controlType === "sap.m.PlanningCalendar") {
return {
id: `${element.getId()}-CLI`
}
} else {
return {
id: element.getId()
}
}
})
}
/**
* creates an object containing their id as a property
* @param {sap.ui.core.Control} aControl
* @return {Object} Object
*/
window.wdi5.createControlId = (aControl) => {
// the array of UI5 controls need to be mapped (remove circular reference)
if (!Array.isArray(aControl)) {
// if in aControls is a single control -> create an array first
// this is causes by sap.ui.base.ManagedObject -> get Aggregation defines its return value as:
// sap.ui.base.ManagedObject or sap.ui.base.ManagedObject[] or null
// aControls = [aControls]
let item = {
id: aControl.getId()
}
return item
} else {
console.error("error creating new element by id of control: " + aControl)
}
}
window.wdi5.errorHandling = (done, error) => {
window.wdi5.Log.error("[browser wdi5] ERR: ", error)
done({ status: 1, message: error.toString() })
}
}
)
}
}, waitForUI5Timeout)
}
module.exports = {
clientSide_injectUI5
}