-
Notifications
You must be signed in to change notification settings - Fork 1
/
event-driver-hooks.js
353 lines (335 loc) · 10.5 KB
/
event-driver-hooks.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
import { autobind } from 'core-decorators';
const touchSupport = ('ontouchstart' in window) && (navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0);
/**
* browser side hooks for webdriver based event drive test
*/
let config = (conf) => {
_config = {
..._config,
...conf
};
};
let _config = {
port: 8848,
host: '127.0.0.1'
};
let idCount = 0;
let Connection;
// get browserId
let browserId = ((opener || parent).location.search || '').replace(/^\S+id=([0-9]+)\S*/g, (mat, id) => id);
let switchFrame = parent !== window;
// this ext can only run in karma default tap with an iframe#context
let contextFrame = parent.document.getElementById('context');
contextFrame = contextFrame && contextFrame.nodeName === 'IFRAME' ? contextFrame : null;
let fullScreenStyle = { position: 'absolute', left: 0, top: 0, background: '#fff' },
originalStyle = {};
if (contextFrame) {
for (let pro in fullScreenStyle) {
originalStyle[pro] = contextFrame.style[pro];
}
}
function noop() { }
let initialled, $$Browser;
// webdriver api serial
let waitingPromise = Promise.resolve();
// promise ensure serial
let serialPromiseResolve, serialPromiseReject;
class $Browser {
constructor() {
this.__tests;
this.__stack;
}
/**
* @public $next execute next serial test or resolve/reject a waiting promise
* @param {any} status !!status ? reject(status) : resolve()
*/
@autobind
async $next(status) {
let action = this.__tests.shift();
// call before $serial
if (!action && !this.__autoStart) this.__autoStart = true;
if (action) {
await action(status).then(noop, this.__rejectSerial);
} else if (this.__resolveSerial) {
this.__resolveSerial();
}
}
/**
* @public $serial register serial test
* @param {Function} tests as many functions as u want
* @return {Promise}
*/
@autobind
$serial(...tests) {
this.__prom = this.__prom || new Promise((rs, rj) => {
this.__resolveSerial = () => {
rs();
beforeEachHook();
};
this.__rejectSerial = (e) => {
rj(e);
beforeEachHook();
};
});
tests.forEach((test) => {
this.__tests.push(async () => {
await test(this);
// auto run next test
await this.$next();
});
});
if (this.__autoStart) {
this.$next();
this.__autoStart = false;
}
return this.__prom;
}
/**
* @public $pause return promise resolved after timeout ms
* also: await browser.pause(timeout).$apply(); // > timeout, since I/O with socket server
* @param {Number} timeout ms
*/
async $pause(timeout) {
return new Promise((rs) => {
setTimeout(rs, timeout);
});
}
/**
* @public $apply execute right now
* @param {Boolean} applyAndWaitForNext wait for calling browser.$next
* @param {Function} done callback
* @return {Promise} if !!applyAndWaitForNext === false return a resolved promise, else a promise not resolved until browser.$next being called
*/
async $apply(applyAndWaitForNext, done) {
let actions = this.__stack.splice(0);
if (!initialled) return console.error('ensure beforeHook has been called');
let executerPromiseResolve, executerPromiseReject;
let prom;
if (applyAndWaitForNext) {
prom = new Promise((resolve, reject) => {
executerPromiseResolve = resolve;
executerPromiseReject = reject;
});
console.log('add a waiting promise, ensure browser.$next(status) is called at right time');
// !!status === true, then reject
this.__tests.unshift(async (status) => {
!!status ? executerPromiseReject(status) : executerPromiseResolve()
});
}
await waitingPromise;
if (actions.length) {
waitingPromise = wrapPromise((resolve, reject) => {
serialPromiseResolve = resolve;
serialPromiseReject = reject;
}, contextFrame);
this.__callDriver(actions);
await waitingPromise;
}
await prom;
done && done();
}
/**
* @public $applyAndWaitForNext equal to $$action('applyAndWaitForNext')
*/
async $applyAndWaitForNext(done) {
await this.$apply(true, done);
}
/**
* @private __callDriver send Command to server
* @param {Array} actions
*/
__callDriver(actions) {
if (!contextFrame) return console.warn('webdriver driving test can\'t run in current tab', location.href);
Connection.emit('runCommand', {
actions,
browserId,
switchFrame
});
}
/**
* parse browser.api(a, b, c) => ['api', [b, c]], so can be sent to the server and executed by the webdriver.
* @private __toRunnable
* @param {string} def api name
* @param {any} args arguments
*/
__toRunnable(def, ...args) {
this.__stack.push([
def,
args.map((ele) => {
if (ele instanceof Element) {
// if no id, allocate one
ele.id = ele.id || (ele.className && ele.className.split(' ')[0] || 'WebDriverID').replace(/\-/g, '_') + idCount++;
return '#' + ele.id;
} else if (typeof ele === 'function') {
throw Error('can\'t use function ' + ele);
} else {
return ele;
}
})
]);
return this;
}
}
function Browser() {
this.__tests = []; // for register tests
this.__stack = [];// tmp stack for browser[api]
this.__prom = null;
}
$$Browser = Browser.prototype = new $Browser();
let browser = new Browser();
function fullScreen(full = true) {
if (!contextFrame) return;
let tar = full ? fullScreenStyle : originalStyle;
for (let pro in tar) {
contextFrame.style[pro] = tar[pro];
}
}
/**
* load script async
* @param {string} src
* @return promise
*/
async function loadScript(src) {
let script = document.createElement('script');
script.type = 'text/javascript';
let rs, rj, timer;
script.onload = () => {
script.onload = null;
clearTimeout(timer);
rs();
};
let prom = new Promise((resolve, reject) => {
rs = resolve;
rj = reject;
});
script.src = src;
document.head.appendChild(script);
timer = setTimeout(() => rj('load ' + src + ' time out'), 10000);
return prom;
}
async function wrapPromise(fn, wait = true) {
return new Promise((resolve, reject) => {
wait ? fn(resolve, reject) : resolve();
});
}
/**
* run first in before()
* @params {function} done if assigned, call done after promise resolved.
* @return promise
*/
async function beforeHook(done) {
// height & width : 100%
fullScreen();
if (initialled) return done && done();
let { url, host, port } = _config;
if (!url) url = host + ':' + port;
await loadScript('//' + url + '/socket.io/socket.io.js');
// it's hard to share socket with karma
// Connection = (opener || parent).karma.socket;
Connection = io(url);
Connection.on('runBack', (message) => {
// console.log('runBack', message);
message && !message.status ? serialPromiseResolve() : serialPromiseReject(message.status);
});
// whether there is contextFrame, wait
waitingPromise = wrapPromise((resolve) => {
Connection.on('ready', (message) => {
let { supportedDefs = '' } = message;
supportedDefs.split(' ').map((def) => {
$$Browser[def] = function () {
return this.__toRunnable(def, ...arguments);
};
});
// console.log('ready', message);
routePCToMobile();
resolve();
});
});
await waitingPromise;
initialled = true;
done && done();
}
function getPos(ele) {
return ele && (ele.nodeName && ele || document.querySelector(ele)).getBoundingClientRect();
}
/**
* @private route PC support api like moveTo to mobile, write once run both sides.
*/
function routePCToMobile() {
if (touchSupport) {
let curX = 0, curY = 0, isTouchDown = false;
let dict = {
'moveToObject': function (ele, x, y) {
let pos = getPos(ele);
if (pos) {
if (x != null) curX = pos.left + x;
if (y != null) curY = pos.top + y;
} else {
if (x != null) curX = curX + x;
if (y != null) curY = curY + y;
}
if (curX < 0) curX = 0;
if (curY < 0) curY = 0;
return browser;
},
'moveTo': function (ele, x, y) {
browser.moveToObject(ele, x, y);
return isTouchDown ? browser.touchMove(curX, curY) : browser;
},
'click': function (ele) {
return browser.touch(ele, { x: 0, y: 0 });
},
'buttonDown': function () {
isTouchDown = true;
return browser.touchDown(curX, curY);
},
'buttonUp': function () {
isTouchDown = false;
browser.touchUp(curX, curY);
curX = curY = 0;
return browser;
},
'leftClick': function (ele) {
return browser.touchPerform('tap', {
ele: ele
})
}
};
for (let name in dict) {
browser[name] = typeof dict[name] === 'string' ? browser[dict[name]] : dict[name];
}
}
}
/**
* run last in after()
* @params {function} done if assigned, call done after promise resolve
* @return promise
*/
async function afterHook(done) {
fullScreen(false);
done && done();
}
/**
* run before each test, reset browser status
* @return promise
*/
async function beforeEachHook(done) {
browser.__autoStart = browser.__prom = browser.__rejectSerial = browser.__resolveSerial = null;
done && done();
}
export default {
loadScript,
config,
browser,
beforeHook,
beforeEachHook,
afterHook
}
export {
loadScript,
config,
browser,
beforeHook,
beforeEachHook,
afterHook
}