├── .editorconfig
├── .gitignore
├── .npmignore
├── .vscode
└── launch.json
├── CHANGE.md
├── LICENSE
├── README.md
├── README_zh-cn.md
├── bin
└── uirecorder
├── buildcrx.bat
├── buildcrx.sh
├── chrome-extension
├── img
│ ├── alert.png
│ ├── back.png
│ ├── cancel.png
│ ├── end.png
│ ├── expect.png
│ ├── fail.png
│ ├── hover.png
│ ├── icon-128.png
│ ├── icon-64.png
│ ├── icon-disable.png
│ ├── icon-record.png
│ ├── icon.png
│ ├── jscode.png
│ ├── jump.png
│ ├── mloading.gif
│ ├── ok.png
│ ├── sleep.png
│ ├── success.png
│ ├── text.png
│ ├── vars.png
│ └── warn.png
├── js
│ ├── background.js
│ ├── foreground.js
│ ├── jquery-3.4.1.min.js
│ ├── mobile.js
│ └── start.js
├── manifest.json
├── mobile.html
└── start.html
├── doc
└── zh-cn
│ ├── pc-advanced.md
│ ├── pc-faq.md
│ ├── pc-standard.md
│ └── readme.md
├── i18n
├── en.js
├── zh-cn.js
└── zh-tw.js
├── index.js
├── lib
├── builder
│ └── java.js
├── i18n.js
├── init.js
├── start.js
└── update.js
├── logo.png
├── package-lock.json
├── package.json
├── project
├── .editorconfig
├── .gitignore1
├── README.md
├── commons
│ └── commons.md
├── hosts
├── install.sh
├── package.json
├── run.bat
├── run.sh
├── screenshots
│ └── screenshots.md
├── uploadfiles
│ └── uploadfiles.md
└── vslaunch.json
├── screenshot
├── shot1.png
├── shot2.png
├── shot3.png
└── shot4.png
├── template
├── javaTemplate.java
├── jwebdriver-mobile.js
└── jwebdriver.js
├── tool
└── uirecorder.crx
├── uirecorder.log
└── uirecorder.pem
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | # Apply for all files
8 | [*]
9 |
10 | charset = utf-8
11 |
12 | indent_style = space
13 | indent_size = 4
14 |
15 | end_of_line = lf
16 | insert_final_newline = true
17 | trim_trailing_whitespace = true
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | test
4 | *.sw*
5 | *.un~
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | test
4 | chrome-extension
5 | buildcrx.bat
6 | uirecorder.pem
7 | screenshot
8 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible Node.js debug attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Debug UIRecorder Local",
11 | "cwd": "${workspaceRoot}/test/test",
12 | "args": [ "${file}"],
13 | "console": "integratedTerminal"
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/CHANGE.md:
--------------------------------------------------------------------------------
1 | UI Recorder change log
2 | ====================
3 | ## ver 3.5.3(2020-10-29)
4 | 1.Fix:Debugger Mode
5 |
6 | ## ver 3.4.3(2020-07-28)
7 | 1.Fix: chrome options default config
8 |
9 | ## ver 3.3.0(2020-02-22)
10 | 1.Feat: support mobile emulator
11 |
12 | ## ver 3.2.0(2020-01-20)
13 | 1.Feat: support customer reporter dirname
14 |
15 | ## ver 3.1.10(2020-01-19)
16 | 1.Feat: modify mobile template
17 |
18 | ## ver 3.1.9(2020-01-15)
19 | 1.Feat: support group tag template
20 |
21 | ## ver 3.1.8(2019-12-21)
22 | 1.Feat: update chromedriver version latest
23 |
24 | ## ver 3.1.7(2019-10-23)
25 | 1.Feat:update chromedriver version 77.0.0
26 |
27 | ## ver 3.1.6(2019-08-28)
28 | 1.Feat: support specific symbol in Chinese
29 |
30 | ## ver 3.1.5(2019-08-28)
31 | 1.Feat: by default, process always exit 0 when test fail
32 |
33 | ## ver 3.1.4(2019-08-28)
34 | 1.Feat: by default, process always exit 0 when test fail
35 |
36 | ## ver 3.1.3(2019-07-08)
37 | 1.Feat:support module test
38 |
39 | ## ver 3.1.2(2019-07-31)
40 | 1.Feat:chromedriver version 76.0.0
41 |
42 | ## ver 3.1.1(2019-07-30)
43 | 1.Fix: vscode lauch
44 |
45 | ## ver 3.1.0(2019-07-16)
46 | 1.Feature: screenshots image url use relative path
47 |
48 | ## ver 3.0.3(2019-07-13)
49 | 1.Feature: support jquery syntax
50 |
51 | ## ver 3.0.2(2019-07-12)
52 | 1.Feature: remove chrome-extension input size
53 |
54 | ## ver 3.0.1(2019-07-09)
55 | 1.Feature: modify macaca-reporter
56 |
57 | ## ver 3.0.0(2019-06-20)
58 | 1.Feature: mocha@3 -> mocha@5
59 | 2.Feature: mocha-parllel-tests@1.x -> mocha-parallel-tests@2.x
60 | 3.Feature: mochawesome-uirecorder -> macaca-reporter
61 |
62 | ## ver 2.6.0(2019-05-16)
63 | 1.Feature: remove default chromeOptions in uirecorder template
64 |
65 | ## ver 2.5.47(2019-02-22)
66 | 1.Feature: support window size maximize
67 |
68 | ## ver 2.5.46 (2019-01-07)
69 | 1.Feature: support custom template
70 |
71 | ## ver 2.5.45 (2018-11-14)
72 | 1.Fix: use windowSize(1024, 768) instead of maximize() default
73 | 2.Update: update dependencies version
74 |
75 | ## ver 2.5.43 (2018-10-19)
76 | 1.Fix: catch error infos when browser.maximize() throw error
77 |
78 | ## ver 2.5.42 (2018-5-19)
79 |
80 | 1. Fix: skip node_modules directory
81 |
82 | ## ver 2.5.41 (2018-5-4)
83 |
84 | 1. Fix: fix chromedriver issue
85 | 2. Fix: fix rand port issue for mobile mode
86 |
87 | ## ver 2.5.40 (2018-3-6)
88 |
89 | 1. Add: support define window open order when start record
90 |
91 | ## ver 2.5.39 (2018-3-1)
92 |
93 | 1. fix issue for 2.5.37
94 |
95 | ## ver 2.5.38 (2018-2-26)
96 |
97 | 1. fix issue for 2.5.37
98 | 2. fix issue: above & below not support number compare
99 |
100 | ## ver 2.5.37 (2018-2-9)
101 |
102 | 1. Add: support open checker browser and set maximize by default (uirecorder --default)
103 |
104 | ## ver 2.5.36 (2017-12-15)
105 |
106 | 1. Improve: change to random listen port
107 | 2. Add: support read wdproxy from env when recording
108 | 3. Fix: not escape the regular expression when add expect
109 |
110 | ## ver 2.5.35 (2017-11-1)
111 |
112 | 1. Add: support detect chromedriver bin file
113 | 2. Fix: kill uirecorder after chromedriver killed
114 |
115 | ## ver 2.5.34 (2017-10-25)
116 |
117 | 1. Add: bail after first test failure
118 | 2. Fix: support save acceptAlert after cmd beforeunload
119 | 3. Add: support not copy screenshot when no failed `--reporter-options copyShotOnlyFail=true`
120 |
121 | ## ver 2.5.33 (2017-9-20)
122 |
123 | 1. Add: add `value` to attr switch
124 |
125 | ## ver 2.5.32 (2017-9-14)
126 |
127 | 1. Fix: fix text path issue when contain space
128 |
129 | ## ver 2.5.31 (2017-9-7)
130 |
131 | 1. Fix: fix double event issue when change black value list
132 | 2. Add: add document
133 |
134 | ## ver 2.5.30 (2017-9-4)
135 |
136 | 1. Fix: fix update var issue after load module
137 |
138 | ## ver 2.5.29 (2017-9-1)
139 |
140 | 1. Add: add black list for text path(exclude special character: ×)
141 | 1. Add: save all cookies info to `xxx.cookie` when get screenshot
142 |
143 | ## ver 2.5.28 (2017-8-31)
144 |
145 | 1. Fix: fix issue when get unexpected alert msg #138
146 |
147 | ## ver 2.5.27 (2017-8-15)
148 |
149 | 1. Add: show version in recorder pannel
150 | 2. Add: support text path for pc mode #85
151 |
152 | ## ver 2.5.26 (2017-8-15)
153 |
154 | 1. Fix: fix a click issue for pc mode
155 |
156 | ## ver 2.5.25 (2017-8-11)
157 |
158 | 1. Fix: support remote url with any extension name #132
159 | 2. Add: support paste text for mobile mode
160 | 3. Fix: fix init proxy issue #124
161 |
162 | ## ver 2.5.24 (2017-8-9)
163 |
164 | 1. Fix: delete `\r` for windows, when paste multi line
165 |
166 | ## ver 2.5.23 (2017-8-7)
167 |
168 | 1. Add: support define device name for mobile mode
169 | 2. Add: support proxy for init cmd (read config from cnpm) #124
170 | 3. Add: support set proxy for pc uicase
171 |
172 | ## ver 2.5.22 (2017-7-31)
173 |
174 | 1. Fix: fix drag issue again #125
175 | 2. Add: show error message when parse config file failed
176 |
177 | ## ver 2.5.21 (2017-7-28)
178 |
179 | 1. Fix: fix drag issue #125
180 |
181 | ## ver 2.5.20 (2017-7-25)
182 |
183 | 1. Fix: fix expect jscode issue #119
184 |
185 | ## ver 2.5.19 (2017-7-25)
186 |
187 | 1. Fix: fix load spec failed when expect image diff #123
188 | 2. Add: support spec jump for mobile mode
189 | 3. Add: save test file when click save button only
190 |
191 | ## ver 2.5.18 (2017-7-24)
192 |
193 | 1. Fix: fix xpath issue for ios
194 |
195 | ## ver 2.5.16 (2017-7-24)
196 |
197 | 1. Add: support class value black list
198 | 2. Fix: fix some record failed issue for ios
199 |
200 | ## ver 2.5.15 (2017-7-6)
201 |
202 | 1. Add: support expect count for mobile mode
203 |
204 | ## ver 2.5.14 (2017-7-5)
205 |
206 | 1. Fix: change to use cnpm registry when init project, more faster, more stable
207 |
208 | ## ver 2.5.13 (2017-6-30)
209 |
210 | 1. Add: support image diff for mobile mode
211 |
212 | ## ver 2.5.12 (2017-6-28)
213 |
214 | 1. Fix: fix jump issue #116
215 |
216 | ## ver 2.5.11 (2017-6-22)
217 |
218 | 1. Fix: fix sleep&jscode issue when work with checker mode
219 | 2. Fix: fix eval issue
220 | 3. Add: add url info to html log file
221 |
222 | ## ver 2.5.10 (2017-6-13)
223 |
224 | 1. Add: support save log to `uirecorder.log` after test record
225 | 2. Add: support expect with image diff
226 |
227 | ## ver 2.5.9 (2017-6-6)
228 |
229 | 1. Add: support insert single template string without add variable
230 | 2. Add: support var template when eval jscode, `document.title="{{varname}}";`
231 | 3. Fix: support chrome v59
232 | 4. Add: support expect dom count
233 |
234 | ## ver 2.5.8 (2017-6-5)
235 |
236 | 1. Fix: fix continue record when pass filename from cli
237 | 2. Improve: improve hover mode, use single hover mode by default
238 |
239 | ## ver 2.5.7 (2017-6-2)
240 |
241 | 1. Add: support save source code when save screenshot
242 |
243 | ## ver 2.5.6 (2017-5-27)
244 |
245 | 1. Improve: support auto hover mode
246 | 2. Add: support expect the result after js eval in front browser
247 |
248 | ## ver 2.5.5 (2017-5-23)
249 |
250 | 1. Fix: Support new version of macaca ios driver
251 |
252 | ## ver 2.5.4 (2017-5-17)
253 |
254 | 1. Fix: Support new version of macaca android driver
255 |
256 | ## ver 2.5.3 (2017-5-11)
257 |
258 | 1. Support new version chromedriver
259 |
260 | ## ver 2.5.2 (2017-5-9)
261 |
262 | 1. Fix: fix notContain
263 |
264 | ## ver 2.5.1 (2017-5-9)
265 |
266 | 1. Add: Support debug local
267 |
268 | ## ver 2.5.0 (2017-5-8)
269 |
270 | 1. Add: Merge start and init command to default command, just use `uirecorder` command
271 | 2. Add: support es7 async
272 | 3. Add: support debug for vscode
273 | 4. Add: support notContain for expect (by stevobm)
274 | 5. Add: support install project dependencies and webdriver dependencies when init project
275 | 6. Add: skip filename input step when pass from cmd args
276 |
277 | ## ver 2.4.14 (2017-4-18)
278 |
279 | 1. Update: update chromedriver to v2.29.0
280 |
281 | ## ver 2.4.12 (2017-4-10)
282 |
283 | 1. Fix: hidden recorder tool pannel when loading module
284 |
285 | ## ver 2.4.11 (2017-4-1)
286 |
287 | 1. Fix: Support macaca new wda source api
288 |
289 | ## ver 2.4.10 (2017-3-29)
290 |
291 | 1. Fix: No reset browser size when continue recording
292 |
293 | ## ver 2.4.9 (2017-3-21)
294 |
295 | 1. Fix: data-testid not work
296 |
297 | ## ver 2.4.8 (2017-3-21)
298 |
299 | 1. Fix: disable get screenshot when closeWindow
300 | 2. Fix: insert new var failed in other iframe context
301 |
302 | ## ver 2.4.7 (2017-3-20)
303 |
304 | 1. Fix: insert var failed when add new var in iframe context
305 | 2. Add: support get data-testid before get id
306 | 3. Add: add new feature eval jscode in browser side
307 |
308 | ## ver 2.4.6 (2017-3-18)
309 |
310 | 1. Add: add localhost hosts tip for mac system
311 | 2. Update: update selenium-standalone to version v6.1.0
312 | 3. Fix: stop chromedriver and browser before recorder ended
313 | 4. Fix: add i18n text for var template dialog
314 | 5. Add: add title to recorder browser and checker browser
315 |
316 | ## ver 2.4.5 (2017-3-17)
317 |
318 | 1. Add: add attr data-test
319 | 2. Add: support auto show text dialog when mobile recording
320 | 3. Add: support js template string to insert var(pc), jump url(pc), send keys(mobile), expect(pc, mobile)
321 | 4. Del: delete support to faker.js
322 | 5. Fix: support to chrome v57
323 |
324 | ## ver 2.4.4 (2017-3-14)
325 |
326 | 1. Fix: not record sendKeys when paste in recorder dom area
327 |
328 | ## ver 2.4.3 (2017-3-7)
329 |
330 | 1. Fix: delay 1 second to init recorder browser
331 |
332 | ## ver 2.4.2 (2017-3-7)
333 |
334 | 1. Fix: fix update var failed in next page when recording
335 |
336 | ## ver 2.4.1 (2017-3-6)
337 |
338 | 1. Add: support save paste text when record pc test
339 |
340 | ## ver 2.4.0 (2017-3-6)
341 |
342 | 1. Fix: fix continue record issue when json file is missing
343 | 2. Remove: remove runtime
344 | 3. Update: support new version of macaca
345 | 4. Add: support new feature for mobile record: sleep, text, back, alert, expect, end
346 | 5. Add: support ios real device
347 | 6. Add: support download app file from url
348 | 7. Add: support continue record for mobile
349 |
350 | ## ver 2.3.32 (2017-2-17)
351 |
352 | 1. Fix: fix continue record issue
353 | 2. Fix: not save file when record zero step
354 | 3. Add: support optionClick
355 |
356 | ## ver 2.3.31 (2017-2-14)
357 |
358 | 1. Fix: add escape to module name when call spec
359 | 2. Add: support continue record
360 |
361 | ## ver 2.3.30 (2017-2-10)
362 |
363 | 1. Fix: fix raw path issue
364 | 2. Add: support disable id and name when recording
365 | 3. Add: support edit attr value black when recording
366 | 4. Add: support add sleep time
367 |
368 | ## ver 2.3.29 (2017-2-10)
369 |
370 | 1. Add: support save raw cmd json file
371 |
372 | ## ver 2.3.28 (2017-2-9)
373 |
374 | 1. Add: support expect after hover in mac os
375 | 2. Fix: fix some case skiped issue
376 | 3. Add: support show hosts in html reporter
377 | 4. Add: support use unicode file name for test case
378 | 5. Add: support start selenium-standalone server by npm cmd: `npm run server`
379 | 6. Fix: fix throw no error issue when expect a non existed dom
380 | 7. Fix: fix issue when expect a string contain `'`
381 |
382 | ## ver 2.3.27 (2017-2-4)
383 |
384 | 1. Fix: fix chromedriver install failed issue
385 | 2. Fix: fix run failed in windows system
386 |
387 | ## ver 2.3.26 (2017-1-16)
388 |
389 | 1. Add: support single test by run command
390 | 2. Add: support hide dom before expect
391 |
392 | ## ver 2.3.25 (2017-1-12)
393 |
394 | 1. Add: support scroll in element
395 |
396 | ## ver 2.3.24 (2017-1-6)
397 |
398 | 1. Fix: support node v7.x
399 | 2. Update: up chromedriver to v2.27
400 |
401 | ## ver 2.3.23 (2017-1-6)
402 |
403 | 1. Add: check page error after page loaded
404 |
405 | ## ver 2.3.22 (2017-1-4)
406 |
407 | 1. Add: support disable path attr temporary
408 | 2. Fix: fix updatevar failed issue when in iframe
409 |
410 | ## ver 2.3.19 (2016-12-28)
411 |
412 | 1. Fix: exclude uirecorder tool panel doms when get dom path
413 | 2. Fix: fix aria upload role issue
414 |
415 | ## ver 2.3.16 (2016-12-28)
416 |
417 | 1. Fix: fix upload check failed issue
418 | 2. Add: support aria role for upload
419 |
420 | ## ver 2.3.15 (2016-12-27)
421 |
422 | 1. Add: update mochawesome-uirecorder, add support lightbox
423 |
424 | ## ver 2.3.13 (2016-12-24)
425 |
426 | 1. Fix: update jwebdriver to v2.0.5, fix issue: send key to rich editor failed with first time
427 |
428 | ## ver 2.3.12 (2016-12-23)
429 |
430 | 1. Fix: fix double event issue when in rich editor
431 |
432 | ## ver 2.3.11 (2016-12-23)
433 |
434 | 1. Fix: fix click event lost issue in some special case
435 |
436 | ## ver 2.3.10 (2016-12-22)
437 |
438 | 1. Add: support jump to var only url
439 |
440 | ## ver 2.3.9 (2016-12-21)
441 |
442 | 1. Add: support change webdriver host & port by env
443 | 2. Add: change to use chromedriver (https://www.npmjs.com/package/chromedriver)
444 |
445 | ## ver 2.3.8 (2016-12-20)
446 |
447 | 1. Add: support auto check update
448 |
449 | ## ver 2.3.7 (2016-12-20)
450 |
451 | 1. Add: support more expect type: notEqual, above, below, match, notMatch
452 | 2. Add: add readme: How to add expect after hover?
453 |
454 | ## ver 2.3.6 (2016-12-20)
455 |
456 | 1. Fix: fix jwebdriver chai issue when throw error by promise
457 |
458 | ## ver 2.3.5 (2016-12-19)
459 |
460 | 1. Fix: fix mouseUp issue when change window
461 |
462 | ## ver 2.3.4 (2016-12-16)
463 |
464 | 1. Fix: fix mouseDown issue when open new window
465 |
466 | ## ver 2.3.3 (2016-12-16)
467 |
468 | 1. Update: up chromedriver to v2.26
469 |
470 | ## ver 2.3.2 (2016-12-13)
471 |
472 | 1. Add: module dialog change to jump to dialog, support for url jump
473 |
474 | ## ver 2.3.0 (2016-12-12)
475 |
476 | 1. Fix: switchWindow losted when check browser is disabled (PC)
477 | 2. Add: support update var with webdriver (PC)
478 | 2. Add: support define different hosts file for different runtime (PC)
479 |
480 | ## ver 2.2.18 (2016-12-6)
481 |
482 | 1. Fix: exit with code 0 when use mochawesome-uirecorder, support to jenkins
483 |
484 | ## ver 2.2.15 (2016-12-6)
485 |
486 | 1. Fix: support test case saved in third level directory
487 |
488 | ## ver 2.2.14 (2016-12-5)
489 |
490 | 1. Fix: fix mouseUp issue again
491 |
492 | ## ver 2.2.13 (2016-12-5)
493 |
494 | 1. Fix: fix frame id issue
495 | 2. Fix: fix mouseUp issue
496 | 3. Add: support set delay time for expect
497 |
498 | ## ver 2.2.12 (2016-12-2)
499 |
500 | 1. Fix: update jwebdriver to fix findVisbile issue
501 | 2. Fix: fix drag drop issue in some special case
502 |
503 | ## ver 2.2.11 (2016-12-1)
504 |
505 | 1. Add: support record for shadow dom
506 |
507 | ## ver 2.2.10 (2016-12-1)
508 |
509 | 1. Fix: update jwebdriver, fix local ip issue
510 |
511 | ## ver 2.2.9 (2016-11-30)
512 |
513 | 1. Fix: disable flash when recording
514 |
515 | ## ver 2.2.8 (2016-11-30)
516 |
517 | 1. Fix: fix project files for mobile mode
518 | 2. Fix: fix some issues for record tool panel
519 |
520 | ## ver 2.2.7 (2016-11-29)
521 |
522 | 1. Add: support parallel test
523 |
524 | ## ver 2.2.4 (2016-11-29)
525 |
526 | 1. Add: change to local file upload
527 |
528 | ## ver 2.2.0 (2016-11-29)
529 |
530 | 1. Add: show config path for user confirm runtime
531 | 2. Add: init full test project
532 | 3. Add: add tutorial how to dock jenkins
533 | 4. Fix: fix websocket connect failed when system use invalid proxy
534 |
535 | ## ver 2.1.17 (2016-11-25)
536 |
537 | 1. Add: support runtime switch
538 |
539 | ## ver 2.1.16 (2016-11-25)
540 |
541 | 1. Fix: fix var string issue again
542 |
543 | ## ver 2.1.15 (2016-11-24)
544 |
545 | 1. Fix: fix var string no support for boolean type
546 |
547 | ## ver 2.1.14 (2016-11-23)
548 |
549 | 1. Add: add simulate input event when insert var in recording
550 | 2. Add: show common spec lists in start page
551 |
552 | ## ver 2.1.11 (2016-11-18)
553 |
554 | 1. Update: update to chromedriver v2.25
555 | 2. Fix: fix common test load failed issue
556 | 3. Fix: fix show text failed when i18n load slowly
557 | 4. Add: support var edit feature
558 |
559 | ## ver 2.1.10 (2016-11-17)
560 |
561 | 1. Add: create commons directory when init config
562 | 2. Add: add change log link to README
563 |
564 | ## ver 2.1.9 (2016-11-17)
565 |
566 | 1. Add: show version at top
567 |
568 | ## ver 2.1.7 (2016-11-17)
569 |
570 | 1. Fix: fix screenshots filename issue
571 |
572 | ## ver 2.1.5 (2016-11-16)
573 |
574 | 1. Fix: fix add double expect commands issue
575 | 2. Add: add reporter mochawesome-uirecorder, support list screenshots
576 |
577 | ## ver 2.1.4 (2016-11-15)
578 |
579 | 1. Fix: fix root path
580 | 2. Fix: fix readme
581 |
582 | ## ver 2.1.3 (2016-11-15)
583 |
584 | 1. Add: support start with url include var
585 | 2. Add: support expect to string include var
586 | 3. Add: add mochawesome reporter to readme
587 |
588 | ## ver 2.1.2 (2016-11-14)
589 |
590 | 1. Fix: fix dblClick crash issue
591 |
592 | ## ver 2.1.1 (2016-11-14)
593 |
594 | 1. Add: support new expect type: alert
595 | 2. Fix: fix issue for expect type: url, title, cookie, localStorage, sessionStorage
596 |
597 | ## ver 2.1.0 (2016-11-11)
598 |
599 | 1. Add: support save test case into sub directory
600 | 2. Fix: fix issue when url contain space at front or end
601 | 3. Add: create screenshots directory when init config
602 |
603 | ## ver 2.0.3 (2016-11-9)
604 |
605 | 1. Add: save screenshots after each step
606 | 2. Fix: fix bin issue for mac & linux
607 | 3. Fix: fix some issue for pc record
608 | 4. Add: support edit path when expect dom
609 |
610 | ## ver 2.0.0 (2016-11-8)
611 |
612 | 1. Add: Support jWebDriver v2.0.0
613 | 2. Add: Support macaca for mobile record
614 |
615 | ## ver 1.4.0 (2016-9-22)
616 |
617 | 1. Add: add default help to cli
618 | 2. Add: support define browser size for test case
619 |
620 | ## ver 1.3.0 (2016-9-20)
621 |
622 | 1. Add: find visible elements for DOM PATH, short path length, more compatibility
623 | 2. Fix: fix chrome open failed issue when computer is very slow
624 | 3. Update: update to chromedriver v2.24
625 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 - 2017 Alibaba
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # UI Recorder
2 |
3 | ---
4 |
5 | 
6 |
7 | [](https://www.npmjs.com/package/uirecorder)
8 | [](https://www.npmjs.com/package/uirecorder)
9 | [](https://www.npmjs.com/package/uirecorder)
10 | [](https://www.npmjs.com/package/uirecorder)
11 | [](https://testerhome.com/github_statistics)
12 |
13 | UI Recorder is multi-platform UI test case recorder like [Selenium IDE](http://docs.seleniumhq.org/projects/ide/) but more powerful than Selenium IDE!
14 |
15 | UI Recorder is easy to use, even zero cost.
16 |
17 | 1. Official Site: [https://www.yuque.com/artist/uirecorder/](https://www.yuque.com/artist/uirecorder/)
18 | 2. Language Switch: [English](https://github.com/alibaba/uirecorder/blob/master/README.md), [中文](https://github.com/alibaba/uirecorder/blob/master/README_zh-cn.md)
19 | 3. Change log: [CHANGE](https://github.com/alibaba/uirecorder/blob/master/CHANGE.md)
20 | 4. Video Tutorial:[PC中文教程](http://v.youku.com/v_show/id_XMTY4NTk5NjI4MA==.html)
21 |
22 |
23 | # Features
24 |
25 | 1. Support all user operation: key event, mouse event, alert, file upload, drag, svg, shadow dom
26 | 2. Support mobile native APP(Android, iOS) recorde, powered by [Macaca](https://macacajs.github.io)
27 | 3. No interference when recording: the same as self test
28 | 4. Record test file saved in local
29 | 5. Support kinds of expect: val,text,displayed,enabled,selected,attr,css,url,title,cookie,localStorage,sessionStorage
30 | 6. Support image diff
31 | 7. Support powerful var string
32 | 8. Support common test case: one case call another
33 | 9. Support parallel test
34 | 10. Support i18n: en, zh-cn, zh-tw
35 | 11. Support screenshots after each step
36 | 12. Support HTML report & JUnit report
37 | 13. Support multi systems: Windows, Mac, Linux
38 | 14. Test file base on NodeJs: [jWebDriver](http://jwebdriver.com/)
39 |
40 |
41 |
42 | # Contributors
43 |
44 | |[
itestauipi](https://github.com/itestauipi)
|[
Stngle](https://github.com/Stngle)
|[
yaniswang](https://github.com/yaniswang)
|[
xudafeng](https://github.com/xudafeng)
|[
undead25](https://github.com/undead25)
|[
stevobm](https://github.com/stevobm)
|
45 | | :---: | :---: | :---: | :---: | :---: | :---: |
46 | |[
micosty](https://github.com/micosty)
|[
ali-lion](https://github.com/ali-lion)
|[
alibaba-oss](https://github.com/alibaba-oss)
|[
felizalde](https://github.com/felizalde)
|[
portokallidis](https://github.com/portokallidis)
|[
snapre](https://github.com/snapre)
|
47 | [
paradite](https://github.com/paradite)
|[
WingOfTime](https://github.com/WingOfTime)
|[
zquancai](https://github.com/zquancai)
48 |
49 | This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Sat Apr 30 2022 21:11:26 GMT+0800`.
50 |
51 |
52 |
53 | # Screenshots
54 |
55 | 
56 |
57 | 
58 |
59 | 
60 |
61 | 
62 |
63 | # Video demo
64 |
65 |
66 | 
67 |
68 | 
69 |
70 | # Quick start
71 |
72 |
73 | ## Install
74 |
75 | 1. Install NodeJs (version >= v7.x)
76 |
77 | > [https://nodejs.org/](https://nodejs.org/)
78 |
79 | > `sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share}` (Mac, Linux)
80 |
81 | 2. Install chrome
82 |
83 | > [https://www.google.com/chrome/](https://www.google.com/chrome/)
84 |
85 | 3. Install UI Recorder
86 |
87 | > `npm install uirecorder mocha -g`
88 |
89 | ## PC record
90 |
91 |
92 | 1. Init test project
93 |
94 | > Create new folder
95 |
96 | > `uirecorder init`
97 |
98 | 2. Start record test case
99 |
100 | > edit hosts file
101 |
102 | > `uirecorder sample/test.spec.js`
103 |
104 | 3. Start WebDriver Server
105 |
106 | 4. Run test case
107 |
108 | > Run all case: `source run.sh` ( Linux|Mac ) or `run.bat` ( Windows )
109 |
110 | > Run single case: `source run.sh sample/test.spec.js` ( Linux|Mac ) or `run.bat sample/test.spec.js` ( Windows )
111 |
112 | 5. Get reports & screenshots
113 |
114 | > ./reports/index.html
115 |
116 | > ./reports/index.xml (JUnit)
117 |
118 | > ./reports/index.json
119 |
120 | > ./screenshots/
121 |
122 | ## More Platform Support
123 |
124 |
125 |
126 |
131 |
132 |
133 |
134 | 1. Install & start macaca server:
135 |
136 | > Install [Macaca](http://macacajs.github.io)
137 |
138 | > Connect your mobile or open emulator
139 |
140 | > `macaca server --port 4444`
141 |
142 | 2. Init test project
143 |
144 | > Create new folder
145 |
146 | > `uirecorder init --mobile`
147 |
148 | 3. Start record test case
149 |
150 | > `uirecorder --mobile sample/test.spec.js`
151 |
152 | 4. Run test case
153 |
154 | > Run all case: `source run.sh` ( Linux|Mac ) or `run.bat` ( Windows )
155 |
156 | > Run single case: `source run.sh sample/test.spec.js` ( Linux|Mac ) or `run.bat sample/test.spec.js` ( Windows )
157 |
158 | 5. Get reports & screenshots
159 |
160 | > ./reports/index.html
161 |
162 | > ./reports/index.xml (JUnit)
163 |
164 | > ./reports/index.json
165 |
166 | > ./screenshots/
167 |
168 | # Documentation Translations
169 |
170 | 1. [中文使用手册](https://github.com/alibaba/uirecorder/blob/master/doc/zh-cn/readme.md)
171 |
172 | # QA
173 |
174 | ## How to debug test code
175 |
176 | 1. Install [Visual Studio Code](https://code.visualstudio.com/) & open Visual Studio Code
177 | 2. Open the project root folder by vs code
178 | 3. Open test file, add break point
179 | 4. press `F5` key to start, press `F10` key to run next line
180 |
181 | ## How to deploy WebDriver Server
182 |
183 | 1. How to run selenium standalone server?
184 |
185 | > `npm run server`
186 |
187 | 2. Selenium Grid: [https://github.com/SeleniumHQ/selenium/wiki/Grid2](https://github.com/SeleniumHQ/selenium/wiki/Grid2)
188 | 3. F2etest: [https://github.com/alibaba/f2etest](https://github.com/alibaba/f2etest)
189 |
190 | ## How to change webdriver host & port by env temporary, debug for local?
191 |
192 | 1. `export webdriver=127.0.0.1:4444` or `set webdriver=127.0.0.1:4444` (Windows)
193 |
194 | Tip: port is not required, For example: `export webdriver=127.0.0.1`
195 |
196 | ## How to dock Jenkins?
197 |
198 | 1. Add commands
199 |
200 | source ./install.sh
201 | source ./run.sh
202 |
203 | 2. Add reports
204 |
205 | > [JUnit](https://wiki.jenkins-ci.org/display/JENKINS/JUnit+Plugin): `reports/index.xml`
206 |
207 | > [HTML](https://wiki.jenkins-ci.org/display/JENKINS/HTML+Publisher+Plugin): `reports/index.html`
208 |
209 | ## How to filter unstable path
210 |
211 | 1. Because some attribute values are random or unstable, we can't record a stable CSS selector
212 | 2. We can filter the attributes with a blacklist. You can type `uirecorder init` and then input the blacklist from the command line
213 |
214 | Tip: blacklist is a regex, you can use it like this: `/attr_\d+/`
215 |
216 | ## How to record common test case?
217 |
218 | 1. Record `commons/login.mod.js`
219 | 2. Record `sample/test.spec.js`
220 |
221 | 1. please input `login.mod.js` in recorder start page or jump test case in page
222 | 2. After `login.mod.js` loaded, then recorder other steps
223 |
224 | 3. `source run.sh` ( Linux|Mac ) or `run.bat` ( Windows )
225 |
226 | ## How to record file upload?
227 |
228 | 1. UI Recorder only support native file compont
229 | 2. direct click `` or click ``, the placeholder button must mark as `upload` with `role` or `data-role`
230 | 3. File must save to `uploadfiles/` directory
231 |
232 | ## How to use vars
233 |
234 | edit config.json
235 |
236 | {
237 | "recorder": {
238 | ...
239 | },
240 | "webdriver": {
241 | ...
242 | },
243 | "vars": {
244 | "productId": "123456",
245 | "productName": "mp3"
246 | }
247 | }
248 |
249 | 1. start with url: `http://xxx.com/product?id={{productId}}`
250 | 2. add new var with tool panel
251 | 3. update var with tool panel
252 | 4. jump url with tool panel: `http://xxx.com/product?id={{productId}}`
253 | 5. insert vars string with tool panel: `{{productName}}` or `aaa{{productName}}bbb`
254 | 6. expect to var string: `{{productName}}` or `aaa{{productName}}bbb`
255 |
256 | Tip: All var string also support js template string, For example: `{{productName}}, ${new Date().getTime()}, ${parseInt(testVars.a)+parseInt(testVars.b)}`
257 |
258 |
259 | ## How to add hover multiple or add expect after a hover?
260 |
261 | 1. Press down `Ctrl` or `Command` button
262 | 2. Click `Add Hover` Button, enter hover mode
263 | 3. Release `Ctrl` or `Command` button
264 | 4. Click the dom you want to hover (can add multiple)
265 | 5. Click `Add Expect` Button
266 | 6. Click the dom you want to expect
267 | 7. Press `Esc` button or click `End Hover` Button, exit hover mode
268 |
269 | ## How to expect the value after js eval in front browser?
270 |
271 | 1. `Add Expect`, select type `jscode`
272 | 2. sync mode: `return document.title`
273 | 3. function mode:
274 |
275 | function(){
276 | var str = "aaa";
277 | return str;
278 | }
279 |
280 | 4. async mode:
281 |
282 | function(done){
283 | setTimeout(function(){
284 | done(123);
285 | }, 100);
286 | }
287 |
288 | ## How to hide doms before expect?
289 |
290 | 1. `uirecorder init`
291 | 2. Input css selector when init `Hide before expect`
292 | 3. `uirecorder start`
293 | 4. UIRecorder will hide matched doms before expect, then you can expect the dom behind the mask div
294 |
295 | ## How to record option click?
296 |
297 | Some steps is not very important, but occasionally displayed, this steps will expect to success always.
298 |
299 | 1. Press 'Alt' button
300 | 2. Click the target DOM
301 |
302 | ## How to use image diff?
303 |
304 | 1. Install GraphicsMagick
305 |
306 | > brew install graphicsmagick (Mac)
307 |
308 | > sudo apt-get install graphicsmagick (Linux)
309 |
310 | > http://www.graphicsmagick.org/download.html (Windows)
311 |
312 | 2. Add expect with imgdiff
313 |
314 | > select expect type: imgdiff
315 |
316 | > select target element
317 |
318 | 3. Rebuild the baseline image
319 |
320 | > `source run.sh sample/test.spec.js --rebuilddiff` (Mac | Linux)
321 |
322 | > `run.bat sample/test.spec.js --rebuilddiff` (Windows)
323 |
324 | ## Can't do when recording
325 |
326 | 1. don't change url in location bar
327 | 2. don't change focus by TAB key
328 | 3. don't use dblclick, WebDriver no support
329 | 4. don't select text by mouse, WebDriver no support
330 | 5. don't focus to background window manualy
331 | 6. don't click useless DOM, only record key steps
332 |
333 | ## How develop test friendly code?
334 |
335 | 1. please dont't use random id or name
336 | 2. please name a id for DOM area
337 | 3. add label for form
338 | 4. please listen click event instead of mousedown
339 |
340 | ## How to set udid to mobile test?
341 |
342 | 1. `export devices=xxx1,xxx2` (windows: `set devices=xxx1,xxx2`)
343 | 2. `source run.sh` ( Linux|Mac ) or `run.bat` ( Windows )
344 |
345 | ## How to save raw cmds json?
346 |
347 | 1. `uirecorder start --raw`
348 | 2. After test saved, then you can get 2 files: `sample/test.spec.js`, `sample/test.spec.json`
349 |
350 | ## Other Tips
351 |
352 | 1. Mac system: localhost must place in hosts
353 |
354 | # License
355 |
356 | UIRecorder is released under the MIT license.
357 |
358 | # Thanks
359 |
360 | * jWebDriver: [https://github.com/yaniswang/jWebDriver](https://github.com/yaniswang/jWebDriver)
361 | * chai: [https://github.com/chaijs/chai](https://github.com/chaijs/chai)
362 | * macaca-mocha-parallel-tests: [https://github.com/macacajs/macaca-mocha-parallel-tests](https://github.com/macacajs/macaca-mocha-parallel-tests)
363 | * macaca-reporter: [https://github.com/macacajs/macaca-reporter](https://github.com/macacajs/macaca-reporter)
364 |
--------------------------------------------------------------------------------
/README_zh-cn.md:
--------------------------------------------------------------------------------
1 | # UI Recorder
2 |
3 | ---
4 |
5 | 
6 |
7 | [](https://www.npmjs.com/package/uirecorder)
8 | [](https://www.npmjs.com/package/uirecorder)
9 | [](https://www.npmjs.com/package/uirecorder)
10 | [](https://www.npmjs.com/package/uirecorder)
11 | [](https://testerhome.com/github_statistics)
12 |
13 | UI Recorder 是一款面向多端的 UI 自动化录制工具,类似于[Selenium IDE](http://docs.seleniumhq.org/projects/ide/) 但比Selenium IDE 更加强大!
14 |
15 | UI Recorder 非常简单易用,零成本解决测试回归问题。
16 |
17 | 1. 官方网站: [https://www.yuque.com/artist/uirecorder](https://www.yuque.com/artist/uirecorder/)
18 | 2. 语言切换: [English](https://github.com/alibaba/uirecorder/blob/master/README.md), [中文](https://github.com/alibaba/uirecorder/blob/master/README_zh-cn.md)
19 | 3. 变更日志: [CHANGE](https://github.com/alibaba/uirecorder/blob/master/CHANGE.md)
20 | 4. 视频教程:[PC中文教程](http://v.youku.com/v_show/id_XMTY4NTk5NjI4MA==.html)
21 | 5. 钉钉交流群:30684524(加入验证:UIRecorder录制),下载钉钉:[https://www.dingtalk.com/](https://www.dingtalk.com/)
22 | 6. 最新中文手册:[语雀文档](https://www.yuque.com/artist/uirecorder)、[Github Page](https://alibaba.github.io/uirecorder/build/#/artist/uirecorder/hbqzpl)
23 |
24 | # 功能
25 |
26 | 1. 支持所有用户行为: 键盘事件, 鼠标事件, alert, 文件上传, 拖放, svg, shadow dom
27 | 2. 全平台支持,移动端 Android, iOS 录制, 基于 [Macaca](https://macacajs.github.io) 实现
28 | 3. 无干扰录制: 和正常测试无任何区别,无需任何交互
29 | 4. 录制用例存储在本地
30 | 5. 支持丰富的断言类型: val,text,displayed,enabled,selected,attr,css,url,title,cookie,localStorage,sessionStorage
31 | 6. 支持图片对比
32 | 7. 支持强大的变量字符串
33 | 8. 支持公共测试用例: 允许用例中动态调用另外一个
34 | 9. 支持并发测试
35 | 10. 支持多国语言: 英文, 简体中文, 繁体中文
36 | 11. 支持单步截图
37 | 12. 支持HTML报告和JUnit报告
38 | 13. 全系统支持: Windows, Mac, Linux
39 | 14. 基于Nodejs的测试用例: [jWebDriver](http://jwebdriver.com/)
40 |
41 | # 软件截图
42 |
43 | 
44 |
45 | 
46 |
47 | 
48 |
49 | 
50 |
51 | # 视频演示
52 |
53 | 
54 |
55 | 
56 |
57 | # 快速开始
58 |
59 | ## 安装
60 |
61 | 1. 安装 NodeJs (版本号 >= v7.x)
62 |
63 | > [https://nodejs.org/](https://nodejs.org/)
64 |
65 | > `sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share}` (Mac, Linux)
66 |
67 | 2. 安装 chrome
68 |
69 | > [https://www.google.com/chrome/](https://www.google.com/chrome/)
70 |
71 | 3. 安装 UI Recorder
72 |
73 | > `npm install uirecorder mocha -g`
74 |
75 | ## PC录制
76 |
77 | 1. 初始化测试工程
78 |
79 | > 创建新文件夹
80 |
81 | > `uirecorder init`
82 |
83 | 2. 开始录制测试用例
84 |
85 | > 修改hosts文件
86 |
87 | > `uirecorder sample/test.spec.js`
88 |
89 | 3. 启动WebDriver服务器
90 |
91 | 4. 运行测试用例
92 |
93 | > 运行所有脚本: `source run.sh` ( Linux|Mac ) 或 `run.bat` ( Windows )
94 |
95 | > 运行单个脚本: `source run.sh sample/test.spec.js` ( Linux|Mac ) 或 `run.bat sample/test.spec.js` ( Windows )
96 |
97 | 5. 获得测试报告和单步截图
98 |
99 | > ./reports/index.html
100 |
101 | > ./reports/index.xml (JUnit)
102 |
103 | > ./reports/index.json
104 |
105 | > ./screenshots/
106 |
107 | ## 更多平台
108 |
109 |
110 |
111 |
116 |
117 |
118 |
119 | 1. 安装并且启动macaca server:
120 |
121 | > 安装 [Macaca](http://macacajs.github.io)
122 |
123 | > 连接你的手机或模拟器
124 |
125 | > `macaca server --port 4444`
126 |
127 | 2. 初始化测试工程
128 |
129 | > 创建新文件夹
130 |
131 | > `uirecorder init --mobile`
132 |
133 | 3. 开始录制测试用例
134 |
135 | > `uirecorder --mobile sample/test.spec.js`
136 |
137 | 4. 运行测试用例
138 |
139 | > 运行所有脚本: `source run.sh` ( Linux|Mac ) 或 `run.bat` ( Windows )
140 |
141 | > 运行单个脚本: `source run.sh sample/test.spec.js` ( Linux|Mac ) 或 `run.bat sample/test.spec.js` ( Windows )
142 |
143 | 5. 获得测试报告和单步截图
144 |
145 | > ./reports/index.html
146 |
147 | > ./reports/index.xml (JUnit)
148 |
149 | > ./reports/index.json
150 |
151 | > ./screenshots/
152 |
153 | # 文档翻译
154 |
155 | 1. [中文使用手册](/doc/zh-cn/readme.md)
156 |
157 |
158 | # License
159 |
160 | UIRecorder is released under the MIT license.
161 |
162 | # 感谢
163 |
164 | * jWebDriver: [https://github.com/yaniswang/jWebDriver](https://github.com/yaniswang/jWebDriver)
165 | * chai: [https://github.com/chaijs/chai](https://github.com/chaijs/chai)
166 | * macaca-mocha-parallel-tests: [https://github.com/macacajs/macaca-mocha-parallel-tests](https://github.com/macacajs/macaca-mocha-parallel-tests)
167 | * macaca-reporter: [https://github.com/macacajs/macaca-reporter](https://github.com/macacajs/macaca-reporter)
168 |
--------------------------------------------------------------------------------
/bin/uirecorder:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var fs = require('fs');
4 | var path = require('path');
5 | var program = require('commander');
6 | var UIRecorder = require('../');
7 |
8 | var pkg = require('../package.json');
9 |
10 | function map(val) {
11 | var objMap = {};
12 | val.split(',').forEach(function(item){
13 | var arrItem = item.split(/\s*=\s*/);
14 | objMap[arrItem[0]] = arrItem[1]?arrItem[1]:true;
15 | });
16 | return objMap;
17 | }
18 |
19 | // stdout log
20 | var rawStdoutWrite = process.stdout.write;
21 | fs.writeFileSync('uirecorder.log', '')
22 | process.stdout.write = function(string, encoding, fileDescriptor) {
23 | var nocolor = string.replace(/(\[\d+[mdc]|\[K)/gi, '');
24 | fs.appendFileSync('uirecorder.log', nocolor)
25 | rawStdoutWrite.apply(process.stdout, arguments);
26 | }
27 |
28 | console.log((" __ ______ ____ __ \n\
29 | / / / / _/ / __ \\___ _________ _________/ /__ _____\n\
30 | / / / // / / /_/ / _ \\/ ___/ __ \\/ ___/ __ / _ \\/ ___/\n\
31 | / /_/ // / / _, _/ __/ /__/ /_/ / / / /_/ / __/ / \n\
32 | \\____/___/ /_/ |_|\\___/\\___/\\____/_/ \\__,_/\\___/_/ v"+pkg.version).green);
33 | console.log('');
34 | console.log('Official Site: http://uirecorder.com'.cyan);
35 | console.log('------------------------------------------------------------------\n'.green);
36 |
37 | program
38 | .version(pkg.version)
39 | .option('-l, --lang ', 'change language')
40 | .option('--no-color', 'disable colors')
41 | .option('-m --mobile', 'mobile mode')
42 | .option('-d --debug', 'debug mode')
43 | .option('-r --raw', 'save raw cmds')
44 | .option('--default', 'open checker browser and set 1024x768 by default')
45 | .option('--device [value]', 'set mobile device name');
46 |
47 | var cmd = null;
48 |
49 | program.command('init')
50 | .description('Init UIRecorder config file')
51 | .action(function(){
52 | cmd = 'init';
53 | UIRecorder.init({
54 | locale: program.lang,
55 | mobile: program.mobile,
56 | debug: program.debug
57 | });
58 | });
59 |
60 | program.command('start')
61 | .description('Start recorder')
62 | .action(function(){
63 | cmd = 'start'
64 | UIRecorder.start({
65 | locale: program.lang,
66 | cmdFilename: program.args.length === 2 ? program.args[0] : '',
67 | mobile: program.mobile,
68 | debug: program.debug,
69 | raw: program.raw,
70 | default: program.default,
71 | mobileDevice: program.device
72 | });
73 | });
74 |
75 | program.parse(process.argv);
76 | UIRecorder.checkUpdate(program.lang);
77 |
78 | if(cmd === null){
79 | // default command
80 | var rootPath = getRootPath();
81 |
82 | var configPath = 'config.json';
83 | var configFile = path.resolve(rootPath + '/' + configPath);
84 | var configJson = {};
85 |
86 | if(fs.existsSync(configFile)){
87 | UIRecorder.start({
88 | locale: program.lang,
89 | cmdFilename: program.args.length === 1 ? program.args[0] : '',
90 | mobile: program.mobile,
91 | debug: program.debug,
92 | raw: program.raw,
93 | default: program.default,
94 | mobileDevice: program.device
95 | });
96 | }
97 | else{
98 | console.log(configPath.bold+' '+__('file_missed').red);
99 | console.log('');
100 | UIRecorder.init({
101 | locale: program.lang,
102 | mobile: program.mobile,
103 | debug: program.debug
104 | });
105 | }
106 |
107 | }
108 |
109 | // get test root
110 | function getRootPath(){
111 | var rootPath = path.resolve('.');
112 | while(rootPath){
113 | if(fs.existsSync(rootPath + '/config.json')){
114 | break;
115 | }
116 | rootPath = rootPath.substring(0, rootPath.lastIndexOf(path.sep));
117 | }
118 | return rootPath;
119 | }
120 |
--------------------------------------------------------------------------------
/buildcrx.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --pack-extension=%cd%\chrome-extension --pack-extension-key=%cd%\uirecorder.pem
3 | move chrome-extension.crx tool/uirecorder.crx
4 | echo Build done!
5 |
--------------------------------------------------------------------------------
/buildcrx.sh:
--------------------------------------------------------------------------------
1 | /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --pack-extension=./chrome-extension --pack-extension-key=./uirecorder.pem
2 | mv chrome-extension.crx tool/uirecorder.crx
3 | echo Build done!
4 |
--------------------------------------------------------------------------------
/chrome-extension/img/alert.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/alert.png
--------------------------------------------------------------------------------
/chrome-extension/img/back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/back.png
--------------------------------------------------------------------------------
/chrome-extension/img/cancel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/cancel.png
--------------------------------------------------------------------------------
/chrome-extension/img/end.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/end.png
--------------------------------------------------------------------------------
/chrome-extension/img/expect.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/expect.png
--------------------------------------------------------------------------------
/chrome-extension/img/fail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/fail.png
--------------------------------------------------------------------------------
/chrome-extension/img/hover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/hover.png
--------------------------------------------------------------------------------
/chrome-extension/img/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/icon-128.png
--------------------------------------------------------------------------------
/chrome-extension/img/icon-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/icon-64.png
--------------------------------------------------------------------------------
/chrome-extension/img/icon-disable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/icon-disable.png
--------------------------------------------------------------------------------
/chrome-extension/img/icon-record.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/icon-record.png
--------------------------------------------------------------------------------
/chrome-extension/img/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/icon.png
--------------------------------------------------------------------------------
/chrome-extension/img/jscode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/jscode.png
--------------------------------------------------------------------------------
/chrome-extension/img/jump.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/jump.png
--------------------------------------------------------------------------------
/chrome-extension/img/mloading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/mloading.gif
--------------------------------------------------------------------------------
/chrome-extension/img/ok.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/ok.png
--------------------------------------------------------------------------------
/chrome-extension/img/sleep.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/sleep.png
--------------------------------------------------------------------------------
/chrome-extension/img/success.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/success.png
--------------------------------------------------------------------------------
/chrome-extension/img/text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/text.png
--------------------------------------------------------------------------------
/chrome-extension/img/vars.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/vars.png
--------------------------------------------------------------------------------
/chrome-extension/img/warn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/chrome-extension/img/warn.png
--------------------------------------------------------------------------------
/chrome-extension/js/background.js:
--------------------------------------------------------------------------------
1 | var ENABLE_ICON1 = 'img/icon.png';
2 | var ENABLE_ICON2 = 'img/icon-record.png';
3 | var DISABLE_ICON = 'img/icon-disable.png';
4 |
5 | var isWorking = true;
6 | var workIcon = 1;
7 | var workIconTimer = null;
8 | var recordConfig = null;
9 | var isModuleLoading = false;
10 |
11 | // i18n
12 | var i18n = {};
13 | var __ = function(str){
14 | var args = arguments;
15 | str = i18n[str] || str;
16 | var count = 0;
17 | str = str.replace(/%s/g, function(){
18 | count ++;
19 | return args[count] || '';
20 | });
21 | return str;
22 | };
23 |
24 | // Global events port
25 | var mapGlobalEvents = {};
26 | var GlobalEvents = {
27 | on: function(type, handler){
28 | var arrEvents = mapGlobalEvents[type] || [];
29 | arrEvents.push(handler);
30 | mapGlobalEvents[type] = arrEvents;
31 | },
32 | emit: function(type, data){
33 | sendGlobalEvents({
34 | type: type,
35 | data: data
36 | });
37 | },
38 | _emit: function(type, data){
39 | var arrEvents = mapGlobalEvents[type] || [];
40 | arrEvents.forEach(function(handler){
41 | handler(data);
42 | });
43 | }
44 | };
45 | var mapPorts = {};
46 | var maxPortId = 0;
47 | chrome.extension.onConnect.addListener(function(port) {
48 | var portId = maxPortId++;
49 | mapPorts[portId] = port;
50 | var tabId = port.sender.tab.id;
51 | port.onMessage.addListener(function(msg){
52 | sendGlobalEvents(msg, tabId);
53 | });
54 | port.onDisconnect.addListener(function(port){
55 | delete mapPorts[portId];
56 | });
57 | });
58 | function sendGlobalEvents(msg, senderTabId){
59 | GlobalEvents._emit(msg.type, msg.data);
60 | var port, tabId;
61 | for(var portId in mapPorts){
62 | port = mapPorts[portId];
63 | tabId = port.sender.tab.id;
64 | if(senderTabId !== undefined && senderTabId !== tabId){
65 | port = null;
66 | }
67 | port && port.postMessage(msg);
68 | }
69 | }
70 |
71 | // websocket to ui recorder server
72 | var wsSocket;
73 | function connectServer(port){
74 | if(!wsSocket){
75 | wsSocket = new WebSocket('ws://127.0.0.1:'+port, "protocolOne");
76 | wsSocket.onopen = function (event) {
77 | console.log('ws connected!');
78 | }
79 | wsSocket.onmessage = function (message) {
80 | message = message.data;
81 | try{
82 | message = JSON.parse(message);
83 | }
84 | catch(e){}
85 | var type = message.type;
86 | var data = message.data;
87 | switch(type){
88 | case 'config':
89 | recordConfig = data;
90 | i18n = recordConfig.i18n;
91 | GlobalEvents.emit('updateConfig', recordConfig);
92 | break;
93 | case 'checkResult':
94 | chrome.notifications.create({
95 | type: 'basic',
96 | iconUrl: 'img/'+(data.success?'success':'fail')+'.png',
97 | title: data.success?__('exec_succeed'):__('exec_failed'),
98 | message: data.title
99 | });
100 | GlobalEvents.emit('checkResult', data);
101 | break;
102 | case 'moduleStart':
103 | isModuleLoading = true;
104 | recordConfig.isModuleLoading = true;
105 | chrome.notifications.create({
106 | type: 'basic',
107 | iconUrl: 'img/warn.png',
108 | title: __('module_start_title'),
109 | message: __('module_start_message', data.file)
110 | });
111 | GlobalEvents.emit('moduleStart');
112 | break;
113 | case 'moduleEnd':
114 | isModuleLoading = false;
115 | recordConfig.isModuleLoading = false;
116 | chrome.notifications.create({
117 | type: 'basic',
118 | iconUrl: 'img/'+(data.success?'success':'fail')+'.png',
119 | title: __('module_end_title'),
120 | message: __('module_end_message', data.success?__('succeed'):__('failed'), data.file)
121 | });
122 | GlobalEvents.emit('moduleEnd');
123 | break;
124 | case 'mobileAppInfo':
125 | GlobalEvents.emit('mobileAppInfo', data);
126 | break;
127 |
128 | }
129 | }
130 | wsSocket.onclose = function(){
131 | wsSocket = null;
132 | console.log('ws closed!');
133 | }
134 | }
135 | }
136 |
137 | GlobalEvents.on('updatePathAttr', function(newAttr){
138 | var arrPathAttrs = recordConfig.pathAttrs;
139 | arrPathAttrs.forEach(function(attr){
140 | if(attr.name === newAttr.name){
141 | attr.on = newAttr.on;
142 | }
143 | });
144 | });
145 |
146 | function sendWsMessage(type, data){
147 | if(wsSocket){
148 | var message = {
149 | type: type,
150 | data: data
151 | };
152 | wsSocket.send(JSON.stringify(message));
153 | }
154 | }
155 |
156 | // set recorder work status
157 | function setRecorderWork(enable){
158 | isWorking = enable;
159 | if(isWorking){
160 | chrome.browserAction.setTitle({title: __('icon_record_tip')});
161 | chrome.browserAction.setIcon({path: workIcon===1?ENABLE_ICON1:ENABLE_ICON2});
162 | workIcon *= -1;
163 | workIconTimer = setTimeout(function(){
164 | setRecorderWork(true);
165 | }, 1000);
166 | }
167 | else{
168 | clearTimeout(workIconTimer);
169 | chrome.browserAction.setTitle({title: __('icon_end_tip')});
170 | chrome.browserAction.setIcon({path: DISABLE_ICON});
171 | }
172 | }
173 |
174 | var arrTasks = [];
175 | var lastWindow = -1;
176 | var allKeyMap = {};
177 | var allMouseMap = {};
178 |
179 | // save recoreded command
180 | function saveCommand(windowId, frame, cmd, data){
181 | if(isModuleLoading){
182 | return;
183 | }
184 | var cmdInfo = {
185 | window: windowId,
186 | frame: frame,
187 | cmd: cmd,
188 | data: data,
189 | fix: false
190 | };
191 |
192 | checkLostKey(windowId);
193 |
194 | switch(cmd){
195 | case 'keyDown':
196 | allKeyMap[data.character] = cmdInfo;
197 | break;
198 | case 'keyUp':
199 | delete allKeyMap[data.character];
200 | break;
201 | case 'mouseDown':
202 | allMouseMap[data.button] = cmdInfo;
203 | break;
204 | case 'mouseUp':
205 | delete allMouseMap[data.button];
206 | break;
207 | }
208 |
209 | execNextCommand(cmdInfo);
210 | }
211 |
212 | // 补足丢失的事件
213 | function checkLostKey(windowId){
214 | if(windowId !== lastWindow){
215 | if(lastWindow !== -1){
216 | var cmdInfo;
217 | for(var key in allKeyMap){
218 | cmdInfo = allKeyMap[key];
219 | execNextCommand({
220 | window: cmdInfo.window,
221 | frame: cmdInfo.frame,
222 | cmd: 'keyUp',
223 | data: cmdInfo.data,
224 | fix: true
225 | });
226 | }
227 | allKeyMap = {};
228 | for(var button in allMouseMap){
229 | cmdInfo = allMouseMap[button];
230 | execNextCommand({
231 | window: cmdInfo.window,
232 | frame: cmdInfo.frame,
233 | cmd: 'mouseUp',
234 | data: cmdInfo.data,
235 | fix: true
236 | });
237 | }
238 | allMouseMap = {};
239 | }
240 | lastWindow = windowId;
241 | }
242 | }
243 |
244 | var isRunning = false;
245 | function execNextCommand(newCmdInfo){
246 | if(newCmdInfo){
247 | arrTasks.push(newCmdInfo);
248 | }
249 | if(arrTasks.length > 0 && isRunning === false){
250 | var cmdInfo = arrTasks.shift();
251 | console.log('cmd: { window: '+cmdInfo.window+', frame: '+cmdInfo.frame+', cmd: '+cmdInfo.cmd+ ', data:', JSON.stringify(cmdInfo.data) + ', fix: '+cmdInfo.fix+' }');
252 | isRunning = true;
253 | sendWsMessage('saveCmd', cmdInfo);
254 | setTimeout(function(){
255 | isRunning = false;
256 | execNextCommand();
257 | }, 200);
258 | }
259 | }
260 |
261 | // manage window id
262 | var arrWindows = [];
263 | function getWindowId(tabId){
264 | for(var i=0,len=arrWindows.length;i
'+__('button_text_text')+'',
83 | (mobilePlatform === 'Android'?'
'+__('button_back_text')+'':''),
84 | '
'+__('button_alert_text')+'',
85 | '
'+__('button_expect_text')+'',
86 | '
'+__('button_sleep_text')+'',
87 | '
'+__('button_jump_text')+'',
88 | '
'+__('button_end_text')+''
89 | ];
90 | divToolsPannel.innerHTML = arrToolsHtml.join('');
91 | divDialogBottom.innerHTML = '
'+__('dialog_ok')+'
'+__('dialog_cancel')+'';
92 | }
93 |
94 | divToolsPannel.addEventListener('click', function(event){
95 | var target = event.target;
96 | if(target.tagName === 'IMG'){
97 | target = target.parentNode;
98 | }
99 | isSelectorMode = false;
100 | var name = target.name;
101 | switch(name){
102 | case 'text':
103 | showTextDailog();
104 | break;
105 | case 'back':
106 | saveCommand('back');
107 | break;
108 | case 'alert':
109 | showAlertDailog();
110 | break;
111 | case 'expect':
112 | isSelectorMode = true;
113 | break;
114 | case 'sleep':
115 | showSleepDailog();
116 | break;
117 | case 'jump':
118 | showJumpDailog();
119 | break;
120 | case 'end':
121 | chrome.runtime.sendMessage({
122 | type: 'save'
123 | });
124 | break;
125 | }
126 | });
127 |
128 | // 对话框
129 | var okCallback = null;
130 | var cancelCallback = null;
131 | divDomDialog.addEventListener('click', function(event){
132 | event.stopPropagation();
133 | event.preventDefault();
134 | var target = event.target;
135 | if(target.tagName === 'IMG'){
136 | target = target.parentNode;
137 | }
138 | var name = target.name;
139 | switch(name){
140 | case 'uirecorder-ok':
141 | okCallback();
142 | break;
143 | case 'uirecorder-cancel':
144 | hideDialog();
145 | cancelCallback && cancelCallback();
146 | break;
147 | }
148 | });
149 | divDomDialog.addEventListener('keydown', function(event){
150 | var keyCode = event.keyCode;
151 | if(keyCode === 13 && (event.ctrlKey || event.metaKey)){
152 | okCallback();
153 | }
154 | });
155 | document.addEventListener('keyup', function(event){
156 | var keyCode = event.keyCode;
157 | switch(keyCode){
158 | case 27:
159 | if(isShowDialog){
160 | hideDialog();
161 | cancelCallback && cancelCallback();
162 | }
163 | isSelectorMode = false;
164 | break;
165 | }
166 | });
167 | // 显示对话框
168 | function showDialog(title, content, events){
169 | domDialogTitle.innerHTML = title;
170 | domDialogContent.innerHTML = content;
171 | okCallback = events.onOk;
172 | cancelCallback = events.onCancel;
173 | divDomDialog.style.display = 'block';
174 | divDomDialogMask.style.display = 'block';
175 | isShowDialog = true;
176 | var onInit = events.onInit;
177 | if(onInit){
178 | onInit();
179 | }
180 | setDialogCenter();
181 | }
182 | function setDialogCenter(){
183 | divDomDialog.style.marginTop = '-'+(divDomDialog.offsetHeight/2)+'px';
184 | divDomDialog.style.marginLeft = '-'+(divDomDialog.offsetWidth/2)+'px';
185 | }
186 | // 隐藏对话框
187 | function hideDialog(){
188 | domDialogTitle.innerHTML = '';
189 | domDialogContent.innerHTML = '';
190 | divDomDialog.style.display = 'none';
191 | divDomDialogMask.style.display = 'none';
192 | isShowDialog = false;
193 | isSelectorMode = false;
194 | }
195 |
196 | // 延迟对话框
197 | function showSleepDailog(){
198 | var strHtml = '';
199 | var domSleepTime;
200 | showDialog(__('dialog_sleep_title'), strHtml, {
201 | onInit: function(){
202 | domSleepTime = document.getElementById('uirecorder-sleeptime');
203 | domSleepTime.select();
204 | domSleepTime.focus();
205 | },
206 | onOk: function(){
207 | var time = domSleepTime.value;
208 | if(/\d+/.test(time)){
209 | saveCommand('sleep', time);
210 | hideDialog();
211 | }
212 | else{
213 | domSleepTime.focus();
214 | alert(__('dialog_sleep_time_tip'));
215 | }
216 | }
217 | });
218 | }
219 |
220 | // 跳转对话框
221 | function showJumpDailog(){
222 | var arrHtmls = ['');
227 | var domTarget;
228 | showDialog(__('dialog_jump_title'), arrHtmls.join(''), {
229 | onInit: function(){
230 | domTarget = document.getElementById('uirecorder-target');
231 | domTarget.select();
232 | domTarget.focus();
233 | },
234 | onOk: function(){
235 | var target = domTarget.value;
236 | if(target){
237 | saveCommand('module', target);
238 | hideDialog();
239 | }
240 | else{
241 | domTarget.focus();
242 | }
243 | }
244 | });
245 | }
246 |
247 | // 文字对话框
248 | function showTextDailog(text){
249 | var strHtml = '';
250 | var domTextContent;
251 | showDialog(__('dialog_text_title'), strHtml, {
252 | onInit: function(){
253 | domTextContent = document.getElementById('textContent');
254 | domTextContent.select();
255 | domTextContent.focus();
256 | domTextContent.value = text || '';
257 | },
258 | onOk: function(){
259 | var text = domTextContent.value;
260 | if(text){
261 | try{
262 | eval('\`'+text+'\`');
263 | }
264 | catch(e){
265 | return alert(e);
266 | }
267 | saveCommand('sendKeys', text+(mobilePlatform === 'iOS' ? '\n' : '{ESCAPE}'));
268 | hideDialog();
269 | }
270 | else{
271 | domTextContent.focus();
272 | alert(__('dialog_text_content_tip'));
273 | }
274 | }
275 | });
276 | }
277 |
278 | // 文字对话框
279 | function showAlertDailog(){
280 | var strHtml = '
';
281 | var domAlertOption;
282 | showDialog(__('dialog_alert_title'), strHtml, {
283 | onInit: function(){
284 | domAlertOption = document.getElementById('alertOption');
285 | },
286 | onOk: function(){
287 | var alertOption = domAlertOption.value;
288 | saveCommand(alertOption+'Alert');
289 | hideDialog();
290 | }
291 | });
292 | }
293 |
294 | // 断言对话框
295 | function showExpectDailog(path){
296 | var arrHtmls = [
297 | ''
304 | ];
305 | var domExpectSleep, domExpectType, domExpectPath, domExpectCompare, domExpectTo;
306 | showDialog(__('dialog_expect_title'), arrHtmls.join(''), {
307 | onInit: function(){
308 | domExpectSleep = document.getElementById('expectSleep');
309 | domExpectType = document.getElementById('expectType');
310 | domExpectPath = document.getElementById('expectPath');
311 | domExpectCompare = document.getElementById('expectCompare');
312 | domExpectTo = document.getElementById('expectTo');
313 |
314 | domExpectSleep.value = '300';
315 | domExpectPath.value = path;
316 | domExpectTo.focus();
317 | domExpectType.onchange = function(){
318 | var type = domExpectType.value;
319 | switch(type){
320 | case 'imgdiff':
321 | domExpectCompare.value = 'below';
322 | domExpectTo.value = 5;
323 | break;
324 | }
325 | };
326 | },
327 | onOk: function(){
328 | var sleep = domExpectSleep.value;
329 | var type = domExpectType.value;
330 | var path = domExpectPath.value;
331 | var compare = domExpectCompare.value;
332 | var to = domExpectTo.value;
333 | try{
334 | switch(compare){
335 | case 'equal':
336 | case 'notEqual':
337 | /^(true|false)$/.test(to)?eval(to):eval('\`'+to+'\`');
338 | break;
339 | case 'match':
340 | case 'notMatch':
341 | eval(to);
342 | break;
343 | default:
344 | eval('\`'+to+'\`');
345 | break;
346 | }
347 | }
348 | catch(e){
349 | return alert(e);
350 | }
351 | saveCommand('expect', {
352 | sleep: sleep,
353 | type: type,
354 | path: path,
355 | compare: compare,
356 | to: to
357 | });
358 | hideDialog();
359 | }
360 | });
361 | }
362 |
363 | // get event
364 | var appSource = null;
365 | var appTree = null;
366 | var appWidth = 0, appHeight = 0;
367 | var imgWidth = 0, imgHeight = 0;
368 | var checkResult = true;
369 | var scaleX = 1, scaleY =1;
370 | var mapNodeValueCount = {};
371 | var arrKeyAttrs = ['resource-id', 'name', 'text'];
372 | function saveCommand(cmd, data){
373 | var cmdData = {
374 | cmd: cmd,
375 | data: data
376 | };
377 | checkResult = null;
378 | setTimeout(function(){
379 | if(checkResult === null){
380 | showLoading();
381 | }
382 | }, 500);
383 | hideLine();
384 | chrome.runtime.sendMessage({
385 | type: 'command',
386 | data: cmdData
387 | });
388 | }
389 | function scanAllNode(){
390 | mapNodeValueCount = {};
391 | scanNode(appTree)
392 | }
393 | function scanNode(node){
394 | arrKeyAttrs.forEach(function(name){
395 | var value = node[name];
396 | if(value){
397 | var mapCount = mapNodeValueCount[name] || {};
398 | mapCount[value] = mapCount[value] && mapCount[value] + 1 || 1;
399 | mapNodeValueCount[name] = mapCount;
400 | }
401 | })
402 | node.class = node.class || ('XCUIElementType' + node.type);
403 | var bounds = node.bounds || '';
404 | if(bounds){
405 | var match = bounds.match(/^\[([\d\.]+),([\d\.]+)\]\[([\d\.]+),([\d\.]+)\]$/);
406 | if(match){
407 | node.startX = parseInt(match[1], 10);
408 | node.startY = parseInt(match[2], 10);
409 | node.endX = parseInt(match[3], 10);
410 | node.endY = parseInt(match[4], 10);
411 |
412 | }
413 | match = bounds.match(/\{([\d\.]+),\s*([\d\.]+)\},\s*\{([\d\.]+),\s*([\d\.]+)\}/);
414 | if(match){
415 | node.startX = parseInt(match[1], 10);
416 | node.startY = parseInt(match[2], 10);
417 | node.endX = node.startX + parseInt(match[3], 10);
418 | node.endY = node.startY + parseInt(match[4], 10);
419 | }
420 | node.boundSize = (node.endX - node.startX) * (node.endY - node.startY);
421 | }
422 | else if(node.rect){
423 | var rect = node.rect;
424 | node.startX = rect.x;
425 | node.startY = rect.y;
426 | node.endX = rect.x + rect.width;
427 | node.endY = rect.y + rect.height;
428 | node.boundSize = (node.endX - node.startX) * (node.endY - node.startY);
429 | }
430 | var childNodes = node.children || node.node;
431 | if(childNodes){
432 | node.children = childNodes;
433 | if(!Array.isArray(childNodes)){
434 | childNodes = [childNodes];
435 | }
436 | var childNode;
437 | for(var i=0;i 20 ? leftstr(text, 20) + '...' : text;
460 | nodeInfo.text = text;
461 | }
462 | nodeInfo.path = getNodeXPath(bestNode);
463 | }
464 | else{
465 | nodeInfo.x = x;
466 | nodeInfo.y = y;
467 | }
468 | return nodeInfo;
469 | }
470 | function getBestNode(node, x, y, bestNodeInfo){
471 | if(node.boundSize && x >= node.startX && x <= node.endX && y >= node.startY && y <= node.endY || node.boundSize === undefined){
472 | var childNodes = node.children;
473 | if(childNodes){
474 | if(!Array.isArray(childNodes)){
475 | childNodes = [childNodes];
476 | }
477 | for(var i=0;i 1 ? '['+index+']' : '') + XPath;
503 | node = node.parentNode;
504 | }
505 | return '/'+XPath;
506 | }
507 | function getNodeClassIndex(node){
508 | var index = 0;
509 | var className = node.class;
510 | var parentNode = node.parentNode;
511 | if(className && parentNode && Array.isArray(parentNode.children) && parentNode.children.length > 1){
512 | var childNodes = parentNode.children, childNode;
513 | index = -1;
514 | for(var i=0;i 1000 ? 3 : 2;
582 | scaleX /= rate;
583 | scaleY /= rate;
584 | }
585 | checkResult !== null && hideLoading();
586 | });
587 | GlobalEvents.on('checkResult', function(data){
588 | checkResult = data.success || false;
589 | });
590 | GlobalEvents.on('moduleEnd', function(){
591 | checkResult = true;
592 | });
593 | var downX = -9999, downY = -9999, downTime = 0;
594 | screenshot.addEventListener('click', function(event){
595 | var upX = event.offsetX, upY = event.offsetY;
596 | var clickDuration = new Date().getTime() - downTime;
597 | if(Math.abs(downX - upX) < 20 && Math.abs(downY - upY) < 20){
598 | var cmdData = getNodeInfo(Math.floor(upX * scaleX), Math.floor(upY * scaleY));
599 | if(isSelectorMode){
600 | if(cmdData.path){
601 | showExpectDailog(cmdData.path);
602 | }
603 | }
604 | else{
605 | var pressTime = new Date().getTime() - downTime;
606 | cmdData.duration = (pressTime / 1000).toFixed(2);
607 | saveCommand(clickDuration>500?'press':'click', cmdData);
608 | }
609 | downTime = 0;
610 | }
611 | });
612 | screenshot.addEventListener('mousedown', function(event){
613 | downX = event.offsetX;
614 | downY = event.offsetY;
615 | downTime = new Date().getTime();
616 | event.stopPropagation();
617 | event.preventDefault();
618 | });
619 | screenshot.addEventListener('mouseup', function(event){
620 | var upX = event.offsetX, upY = event.offsetY;
621 | if(downX >=0 && downY >= 0 &&
622 | upX >= 0 && upY >= 0 &&
623 | (Math.abs(downX - upX) >= 20 || Math.abs(downY - upY) >= 20)){
624 | var dragTime = new Date().getTime() - downTime;
625 | saveCommand('drag', {
626 | fromX: Math.floor(downX * scaleX),
627 | fromY: Math.floor(downY * scaleY),
628 | toX: Math.floor(upX * scaleX),
629 | toY: Math.floor(upY * scaleY),
630 | duration: (dragTime / 1000).toFixed(2)
631 | });
632 | downTime = 0;
633 | }
634 | event.stopPropagation();
635 | event.preventDefault();
636 | });
637 | document.addEventListener('mouseup', function(event){
638 | if(downTime !== 0){
639 | var upX = event.clientX < screenshot.offsetLeft ? 0 : screenshot.width -1;
640 | var upY = event.clientY;
641 | var dragTime = new Date().getTime() - downTime;
642 | saveCommand('drag', {
643 | fromX: Math.floor(downX * scaleX),
644 | fromY: Math.floor(downY * scaleY),
645 | toX: Math.floor(upX * scaleX),
646 | toY: Math.floor(upY * scaleY),
647 | duration: (dragTime / 1000).toFixed(2)
648 | });
649 | downTime = 0;
650 | }
651 | });
652 | screenshot.addEventListener('mousemove', function(event){
653 | var bestNodeInfo = {
654 | node: null,
655 | boundSize: 0
656 | };
657 | var x = Math.floor(event.offsetX * scaleX);
658 | var y = Math.floor(event.offsetY * scaleY);
659 | getBestNode(appTree, x, y, bestNodeInfo);
660 | var node = bestNodeInfo.node;
661 | if(node){
662 | var offsetLeft = screenshot.offsetLeft;
663 | var offsetTop = screenshot.offsetTop;
664 |
665 | var left = node.startX / scaleX;
666 | var top = node.startY / scaleY;
667 | var width = node.endX / scaleX - left;
668 | var height = node.endY / scaleY - top;
669 |
670 | showLine(left + offsetLeft, top + offsetTop, width, height);
671 | }
672 | else{
673 | hideLine();
674 | }
675 | });
676 | document.addEventListener('keypress', function(event){
677 | if(!isLoading && !isShowDialog){
678 | showTextDailog(event.key);
679 | event.stopPropagation();
680 | event.preventDefault();
681 | }
682 | });
683 |
684 | document.addEventListener('paste', function(event){
685 | if(!isLoading && !isShowDialog){
686 | var text = event.clipboardData.getData('text');
687 | if(text){
688 | saveCommand('sendKeys', text+(mobilePlatform === 'iOS' ? '\n' : '{ESCAPE}'));
689 | }
690 | }
691 | });
692 |
693 | // 计算字节长度,中文两个字节
694 | function byteLen(text){
695 | var count = 0;
696 | for(var i=0,len=text.length;i 255 ? 2 : 1;
699 | }
700 | return count;
701 | }
702 |
703 | // 从左边读取限制长度的字符串
704 | function leftstr(text, limit){
705 | var substr = '';
706 | var count = 0;
707 | var char;
708 | for(var i=0,len=text.length;i 255 ? 2 : 1;
712 | if(count >= limit){
713 | return substr;
714 | }
715 | }
716 | return substr;
717 | }
718 |
719 | })();
720 |
721 |
--------------------------------------------------------------------------------
/chrome-extension/js/start.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | var frmStart = document.getElementById('formStart');
3 | var txtUrl = document.getElementById('url');
4 | var btnStart = document.getElementById('btnStart');
5 | var lstCommons = document.getElementById('commons');
6 |
7 | // i18n
8 | var i18n = {};
9 | var __ = function(str){
10 | var args = arguments;
11 | str = i18n[str] || str;
12 | var count = 0;
13 | str = str.replace(/%s/g, function(){
14 | count ++;
15 | return args[count] || '';
16 | });
17 | return str;
18 | };
19 |
20 | var testVars = {};
21 |
22 | // 全局事件
23 | var mapGlobalEvents = {};
24 | var eventPort = chrome.extension.connect();
25 | var GlobalEvents = {
26 | on: function(type, handler){
27 | var arrEvents = mapGlobalEvents[type] || [];
28 | arrEvents.push(handler);
29 | mapGlobalEvents[type] = arrEvents;
30 | },
31 | emit: function(type, data){
32 | eventPort.postMessage({
33 | type: type,
34 | data: data
35 | });
36 | },
37 | _emit: function(type, data){
38 | var arrEvents = mapGlobalEvents[type] || [];
39 | arrEvents.forEach(function(handler){
40 | handler(data);
41 | });
42 | }
43 | };
44 | eventPort.onMessage.addListener(function(msg) {
45 | GlobalEvents._emit(msg.type, msg.data);
46 | });
47 |
48 | var mapParams = {};
49 | location.search.replace(/([^\?=]+)=([^&]*)/ ,function(all, key, value){
50 | mapParams[key] = value;
51 | });
52 |
53 | // load config
54 | function updateConfig(config){
55 | i18n = config.i18n;
56 | testVars = config.testVars;
57 | txtUrl.setAttribute('placeholder', __('jump_placeholder'));
58 | btnStart.textContent = __('start_button');
59 | var specLists = config.specLists;
60 | var arrHtmls = [];
61 | for(var i in specLists){
62 | arrHtmls.push('');
63 | }
64 | lstCommons.innerHTML = arrHtmls.join('');
65 | }
66 | GlobalEvents.on('updateConfig', updateConfig);
67 | chrome.runtime.sendMessage({
68 | type: 'initBackService',
69 | data: {
70 | port: mapParams.port
71 | }
72 | });
73 |
74 | txtUrl.focus();
75 | frmStart.onsubmit = function(){
76 | var url = txtUrl.value;
77 | url = url.replace(/^\s+|\s+$/g, '');
78 | if(/^([\w-]+\.)+(com|net|org|com\.cn)(\s+|$)/.test(url)){
79 | url = 'http://' + url;
80 | }
81 | var varStr = url;
82 | try{
83 | varStr = eval('\`'+varStr+'\`');
84 | }
85 | catch(e){
86 | alert(e);
87 | return false;
88 | }
89 | varStr = getVarStr(varStr);
90 | if(/^https?:\/\//i.test(url) || /^https?:\/\//i.test(varStr)){
91 | chrome.runtime.sendMessage({
92 | type: 'command',
93 | data: {
94 | frame: null,
95 | cmd: 'url',
96 | data: url
97 | }
98 | });
99 | location.href = varStr;
100 | }
101 | else if(/\.js$/.test(url)){
102 | chrome.runtime.sendMessage({
103 | type: 'command',
104 | data: {
105 | frame: null,
106 | cmd: 'module',
107 | data: url
108 | }
109 | });
110 | }
111 | else{
112 | alert(__('jump_alert'));
113 | }
114 | return false;
115 | }
116 |
117 | function getVarStr(str){
118 | if(typeof str === 'string'){
119 | return str.replace(/\{\{(.+?)\}\}/g, function(all, key){
120 | return testVars[key] || '';
121 | });
122 | }
123 | else{
124 | return str;
125 | }
126 | }
127 | })();
128 |
129 |
--------------------------------------------------------------------------------
/chrome-extension/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "UI Recorder",
4 | "description": "UI Recorder for chrome",
5 | "version": "1.0.0",
6 | "browser_action": {
7 | "default_icon": "img/icon.png",
8 | "default_title": "UI Recorder"
9 | },
10 | "content_scripts": [{
11 | "matches": [ "http://*/*", "https://*/*" ],
12 | "all_frames": true,
13 | "match_about_blank": true,
14 | "run_at": "document_start",
15 | "js": [
16 | "js/foreground.js",
17 | "js/jquery-3.4.1.min.js"
18 | ]
19 | }],
20 | "web_accessible_resources": [
21 | "js/foreground.js",
22 | "img/hover.png",
23 | "img/sleep.png",
24 | "img/expect.png",
25 | "img/jump.png",
26 | "img/end.png",
27 | "img/ok.png",
28 | "img/cancel.png",
29 | "img/vars.png",
30 | "img/success.png",
31 | "img/fail.png",
32 | "img/text.png",
33 | "img/alert.png",
34 | "img/back.png",
35 | "img/jscode.png"
36 | ],
37 | "background": {
38 | "scripts": [ "js/background.js" ]
39 | },
40 | "icons": {
41 | "16": "img/icon.png",
42 | "64": "img/icon-64.png",
43 | "128": "img/icon-128.png"
44 | },
45 | "permissions": ["tabs", "webNavigation", "\u003Call_urls\u003E", "notifications"],
46 | "content_security_policy": "script-src 'self' 'unsafe-eval';"
47 | }
48 |
--------------------------------------------------------------------------------
/chrome-extension/mobile.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UI Recorder
6 |
7 |
157 |
158 |
159 |
160 |
![]()
161 |
162 |
163 |

164 |
165 |
166 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
--------------------------------------------------------------------------------
/chrome-extension/start.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | UI Recorder
7 |
8 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
258 | © alibaba.com
259 |
260 |
261 |
262 |
263 |
--------------------------------------------------------------------------------
/doc/zh-cn/pc-advanced.md:
--------------------------------------------------------------------------------
1 | UIRecorder PC高级使用
2 | ============================
3 |
4 | 如何解决属性值不稳定问题?
5 | -------------------
6 |
7 | 有很多开发会写一些随机的属性值,例如某些随机的id值,这种属性值会导致录制的脚本完全无法持续运行。
8 |
9 | 针对这个问题,我们提供了3套解决方案
10 |
11 | 1. `属性值黑名单正则`: 可以编写正则表达式过滤掉那些不稳定的属性值,例如:`/attr_value_\d+/`,如果需要过滤多种属性值,可以这么写:`/attr_value1_\d+|attr_value2_\d+/`。这个值可以在`uirecorder init`命令中进行配置,也可以在录制过程中的录制面板上即时修改。
12 | 2. `属性开关`:在录制面板上,可以通过临时切换不同属性项的开启和关闭,灵活组合出适合自己业务的PATH生成方案,例如某些场景下不适合用`text`,就可以临时将`text`属性项关闭掉。
13 | 3. `class值黑名单正则`:对于某些不稳定的class值,我们同样提供了黑名单功能,此功能需要通过`uirecorder init`命令进行配置。
14 |
15 | 变量功能怎么用?
16 | -------------------
17 |
18 | 编辑config.json
19 |
20 | {
21 | "recorder": {
22 | ...
23 | },
24 | "webdriver": {
25 | ...
26 | },
27 | "vars": {
28 | "productId": "123456",
29 | "productName": "mp3"
30 | }
31 | }
32 |
33 | 1. 开始页面输入: `http://xxx.com/product?id={{productId}}`
34 | 2. 录制界面中使用工具面创建新变量
35 | 3. 录制界面中使用工具面更新旧变量的值
36 | 4. 录制界面中使用工具面板跳转URL: `http://xxx.com/product?id={{productId}}`
37 | 5. 录制界面中使用工具面板插入变量字符串: `{{productName}}` 或 `aaa{{productName}}bbb`
38 | 6. 断言中使用变量字符串: `{{productName}}` 或 `aaa{{productName}}bbb`
39 |
40 | 提示: 所有变量字符串均支持JS语法的模板字符串,例如:`{{productName}}, ${new Date().getTime()}, ${parseInt(testVars.a)+parseInt(testVars.b)}`
41 |
42 | 怎么录制及使用公共脚本?
43 | -------------------
44 |
45 | 1. 录制 `commons/login.mod.js`
46 | 2. 录制 `sample/test.spec.js`
47 |
48 | 1. 在录制浏览器的开始页面时输入 `login.mod.js`,或者在输入框的右端点击下拉小三角,选择需要的脚本
49 | 2. 或者在录制中间页面点击:`脚本跳转`,随后同上
50 | 3. 当`login.mod.js`加载完成后,继续别的步骤的录制
51 |
52 | 3. `source run.sh sample/test.spec.js` ( Linux|Mac ) 或 `run.bat sample/test.spec.js` ( Windows )
53 |
54 | 如何录制文件上传步骤?
55 | -------------------
56 |
57 | 1. UI Recorder仅支持Native文件上传, 不支持FLASH上传
58 | 2. 直接点击`` 或点击 ``, 占位按钮必需要用`role`或`data-role`标注为`upload`
59 | 3. 上传的文件必需保存在`uploadfiles/`文件夹中
60 |
61 | 如何断点调试生成的脚本?
62 | -------------------
63 |
64 | 1. 安装 [Visual Studio Code](https://code.visualstudio.com/) ,然后打开它
65 | 2. 在vs code中打开项目根目录
66 | 3. 打开测试脚本, 添加断点
67 | 4. 按 `F5` 键执行脚本, 按 `F10` 键执行下一行
68 |
69 | 如何断言浏览器eval js代码后的结果?
70 | ----------------
71 |
72 | 1. `添加断言`, 选择类型: `jscode`
73 | 2. 同步模式: `return document.title`
74 | 3. 函数模式:
75 |
76 | function(){
77 | var str = "aaa";
78 | return str;
79 | }
80 |
81 | 4. 异步模式:
82 |
83 | function(done){
84 | setTimeout(function(){
85 | done(123);
86 | }, 100);
87 | }
88 |
89 | 如何在断言前隐藏DOM结点?
90 | ----------------
91 |
92 | 1. `uirecorder init`
93 | 2. 在初始化`断言前隐藏`选项时,输入需要隐藏的css选择器
94 | 3. `uirecorder start`
95 | 4. UIRecorder会在断言前隐藏所有匹配的DOM结点,然后就可以断言那些隐藏在mask层后面的DOM
96 |
97 | 如何录制可选的点击?
98 | ----------------
99 |
100 | 某些步骤不是非常重要,但却偶尔会出现,这些步骤会总是断言为成功。
101 |
102 | 1. 按下'Alt'键
103 | 2. 点击目标DOM
104 |
105 | 如何使用图片对比功能?
106 | ----------------
107 |
108 | 1. 安装GraphicsMagick
109 |
110 | > brew install graphicsmagick (Mac)
111 |
112 | > sudo apt-get install graphicsmagick (Linux)
113 |
114 | > http://www.graphicsmagick.org/download.html (Windows)
115 |
116 | 2. 添加图片对比断言
117 |
118 | > 选择断言类型: imgdiff
119 |
120 | > 选择目标控件
121 |
122 | 3. 当业务变化时,我们可以通过以下命令更新基线图片
123 |
124 | > `source run.sh sample/test.spec.js --rebuilddiff` (Mac | Linux)
125 |
126 | > `run.bat sample/test.spec.js --rebuilddiff` (Windows)
127 |
128 | 如何导出原数据?
129 | ----------------
130 |
131 | 如果希望基于UIRecorder录制出来的步骤生成JAVA等别的语言自动化脚本,可以使用我们的原数据导出功能。
132 |
133 | 此功能可以在生成js语法的自动化脚本同时,也生成json格式的原数据。基于此json文件,我们就可以自由的翻译成任何语言的自动化脚本。
134 |
135 | 1. `uirecorder start --raw`
136 | 2. 录制完后,就可以获得2个文件: `sample/test.spec.js`, `sample/test.spec.json`
137 |
138 | 如何接入Jenkins?
139 | ----------------
140 |
141 | 1. 添加命令
142 |
143 | source ./install.sh
144 | source ./run.sh
145 |
146 | 2. 添加报告
147 |
148 | > [JUnit](https://wiki.jenkins-ci.org/display/JENKINS/JUnit+Plugin): `reports/index.xml`
149 |
150 | > [HTML](https://wiki.jenkins-ci.org/display/JENKINS/HTML+Publisher+Plugin): `reports/index.html`
151 |
152 | 国内用户可以通过oschina和cnpm提升部署效率,修改install.sh如下:
153 |
154 | ls ~/nvm || git clone https://git.oschina.net/yaniswang/nvm.git ~/nvm
155 | source ~/nvm/nvm.sh
156 | export NVM_NODEJS_ORG_MIRROR="http://npm.taobao.org/mirrors/node"
157 | nvm install v7.10.0
158 | npm install --registry=https://registry.npmmirror.com
159 |
160 | 如何失败时才生成截图?
161 | -------------------
162 |
163 | 1. 编辑文件:`package.json`, 确保`mochawesome-uirecorder`版本在`1.5.22`及以上
164 | 2. 在`--reporter mochawesome-uirecorder`后面添加:` --reporter-options copyShotOnlyFail=true`
165 |
--------------------------------------------------------------------------------
/doc/zh-cn/pc-faq.md:
--------------------------------------------------------------------------------
1 | 常见问题
2 | =================
3 |
4 | 1. `SyntaxError: missing ) after argument list`
5 |
6 | 解决方案:升级到NodeJs v10 以上版本
7 |
8 | 2. `session not created exception from disconnected: unable to connect to renderer`
9 |
10 | 解决方案:系统hosts文件中添加`127.0.0.1 localhost`
11 |
--------------------------------------------------------------------------------
/doc/zh-cn/pc-standard.md:
--------------------------------------------------------------------------------
1 | UIRecorder PC标准入门
2 | ===================
3 |
4 | 如何录制正常网页操作步骤?
5 | -------------------
6 |
7 | 1. 首先安装UIRecorder,这个请参考项目介绍首页:[https://github.com/alibaba/uirecorder](https://github.com/alibaba/uirecorder)
8 | 2. 创建新目录并进入,然后初始化UIRecorder工程目录:`uirecorder init`, 根据具体情况输入相关的参数,几个回车后,我们的准备工作就结束了,这里详细介绍下几个参数分别是什么作用
9 |
10 | 1. `Path扩展属性配置,除id,name,class之外`: 此参数,用来定义网页DOM中哪些属性可以用来定位我们的控件,默认值已经包括了主流的属性,一般情况下不需要修改
11 | 2. `属性值黑名单正则`:网页中当某些属性值是随机或者不稳定的时候,我们可以通过这个配置来忽略那些属性值,从而保证我们的自动化脚本稳定性,例如我们可以这样配置:`/attr_name_\d+/`,如果需要配置多个属性,可以这么写:`/attr1_\d+|attr2_\d+|/`,详细更多的正则教程请自行搜索
12 | 3. `class值黑名单正则`:某些场景下,class属性的值会干扰自动化录制,比如当鼠标移到某个控件上时会动态的添加一个class样式名称,就可能会被录制到PATH中,从而导致自动化无法稳定重放。此时就可以将那个class名称加到黑名单中,例如:`/class_name/`,详细值的配置请参考属性值黑名单
13 | 4. `断言前隐藏`:某些业务场景下,会在最上层显示一层透明的div,用来实现一些高级的富应用需求,会导致无法直接断言透明div下方的控件。此种场景下,可以将透明div的PATH配置在此属性中,即可实现在断言时自动移除透明的div,断言后再自动恢复
14 | 5. `WebDriver域名或IP`:WebDriver执行机的IP地址,默认是指向本地,如果有部署grid执行机集群,可以指向对应的执行机集群IP
15 | 6. `WebDriver端口号`:WebDriver端口号,一般情况下是4444端口号,不需要修改
16 | 7. `需要同时测试的浏览器列表`: 需要同时测试的浏览器列表,例如:chrome, firefox, ie 11,可以根据本地或远程执行机集群所支持的浏览器类型自行输入
17 |
18 | 3. 输入`uirecorder`,会要求输入相关的几个参数, 我们这里介绍下几个参数的意义
19 |
20 | 1. `测试脚本文件名`: 录制后保存到哪个文件,支持直接输入目录名,例如:test/aaa.spec.js,我们要求脚本文件名必需是xxx.spec.js,否则会导致无法定位到
21 | 2. `打开同步校验浏览器?`:校验浏览器是用来实现边录边跑,同步的校验录制的步骤是否能够稳定的重现,避免录制后进行大量的调试。从而实现一次录制,即可稳定重现,极大的提高成功率和稳定性。默认值已经是打开,按回车即可。
22 | 3. `浏览器大小`:默认值是1024x768,如果对于浏览器窗口大小没有特殊要求,用默认值回车即可。如果某些场景要求特定大小,那么可以自己指定特定的大小,例如:1024x768
23 |
24 | 当以上4个参数确认后,我们将看到一个黑背影的开始界面。此时,我们仅需输入需要测试的目标网址,即可正式开始我们的录制之旅。
25 |
26 | 常规录制说明:
27 |
28 | 1. 对于常规的网页操作行为,我们没有任何学习门槛,按照正常的网页操作步骤进行即可,UIRecorder会在后台默默的记录并生成自动化脚本
29 | 2. 除了鼠标的悬停操作,我们需要通过录制面板,在目标控件上手工添加一个悬停。例如,二级菜单就需要鼠标移到一级菜单后,才能在二级菜单进行点击操作。
30 | 3. 如何录制三级甚至四级的多级菜单?
31 |
32 | 按住`Ctrl/Command键`,然后点击`添加悬停`按钮,即可进入持续悬停模式。进入此模式后,每次添加悬停后,会持续处于事件拦截状态,不用担心鼠标移动会导致二级菜单缩回,然后就可以持续的添加更多的悬停操作。
33 |
34 | 4. 如何在悬停后断言?
35 |
36 | 进入`持续悬停模式`后,同时也可以解决悬停后的断言问题。如果希望结束`持续悬停模式`,请再次点击`结束悬停`按钮,即可退出此模式。
37 |
38 | 如何添加断言?
39 | -------------------
40 |
41 | 当进行一系列操作后,往往我们需要添加一系列的断言,用来判断操作后的结果是不是正确的。
42 |
43 | 在操作录制后,我们仅需点击录制面板上的`添加断言`按钮即可,点击按钮后,我们可以发现有以下几个选项:
44 |
45 | 1. `延迟时间`:用来设置本次断言延迟一定的时间后才执行,一般用来解决某些异步操作时间不确定的问题。
46 | 2. `断言类型`:我们提供子丰富的断言类型,不同的控件类型要选择合适的断言类型,这里针对每个断言类型进行介绍
47 |
48 | 1. `val`: 断言输入框的值
49 | 2. `text`: 断言文本的内容
50 | 3. `displayed`: 断言控件是否处于显示状态
51 | 4. `enabled`: 断言当前控件是否可用(没有禁用)
52 | 5. `selected`: 断言当前控件是否打勾选中了
53 | 6. `attr`: 断言当前DOM的属性值
54 | 7. `css`: 断言当前DOM的CSS值
55 | 8. `url`: 断言当前网页的URL地址
56 | 9. `title`: 断言当前网页的title标题
57 | 10. `cookie`: 断言当前网页的cookie值
58 | 11. `localStorage`: 断言当前网页的localStorage
59 | 12. `sessionStorage`: 断言当前网页的sessionStorage
60 | 13. `alert`: 断言弹出的alert窗口的提示文本
61 | 14. `jscode`: 在浏览器端执行自定义的JS代码,断言JS代码的返回值
62 | 15. `count`: 断言控件匹配的数量
63 | 16. `imgdiff`: 断言当前控件的图片差异,可以自定义图片差异的百分比
64 |
65 | 3. `断言DOM`:当前DOM控件的PATH,录制时自动生成,也可以自己进行修改,以在某些特殊场景下进行微调
66 | 4. `比较方式`: 针对读取到的值,如何进行断言,我们解释下每个比较方式
67 |
68 | 1. `equal`: 相等
69 | 2. `notEqual`: 不相等
70 | 3. `contain`: 包含,目标值包含另外一个值
71 | 4. `notContain`: 不包含
72 | 5. `above`: 大于,用于断言数值大于某个值
73 | 6. `below`: 小于,用于断言数值小于某个值
74 | 7. `match`: 匹配正则,一般用于高级断言,例如:/aaa\d+bbb/
75 | 8. `notMatch`: 不匹配正则
76 |
77 | 5. `断言结果`: 判断的目标值
78 |
79 | 其它录制功能介绍
80 | --------------------
81 |
82 | 1. `属性开关`:对于某些特殊场景,可以通过灵活的开启或关闭属性项,不同步骤选择不同的PATH生成策略,例如:某些控件不太适合用文本定位,就可以临时先关闭`text`属性项
83 | 2. `属性值黑名单`: 可以即时配置属性值黑名单,立即生效,用来解决随机属性值的问题,修改后记得按一下回车键,让变更立即生效,格式如下:`/aaa_bbb_\d+/`
84 | 3. `执行JS`: 可以用来在浏览器端执行一些扩展功能,例如:`document.title=123;`
85 | 4. `添加延迟`:某些操作后,会发起异步请求,异步请求的完成时间不太确定时,可以通过添加延迟时间提升稳定性,时间单位是毫秒
86 | 5. `脚本跳转`:用来跳到新的URL,或者跳到另外一个脚本,一般情况下用来引用公共脚本,例如登录操作
87 |
88 | 如何部署WebDriver服务?
89 | ----------------
90 |
91 | 1. 本地部署Selenium standalone server:
92 |
93 | 新开控制台窗口然后执行
94 |
95 | > `npm run server`
96 |
97 | 2. Selenium Grid: [https://github.com/SeleniumHQ/selenium/wiki/Grid2](https://github.com/SeleniumHQ/selenium/wiki/Grid2)
98 | 3. F2etest: [https://github.com/alibaba/f2etest](https://github.com/alibaba/f2etest)
99 |
100 | 录制后如何运行脚本?
101 | --------------------
102 |
103 | 1. 运行所有脚本: `source run.sh` ( Linux|Mac ) 或 `run.bat` ( Windows )
104 | 2. 运行单个脚本: `source run.sh sample/test.spec.js` ( Linux|Mac ) 或 `run.bat sample/test.spec.js` ( Windows )
105 |
106 | 如何临时基于本地浏览器调试脚本?
107 | ----------------
108 |
109 | 对于常态基于远程执行机跑的场景下,出现问题时,往往需要在本机进行调试,我们可以通过设置环境变量,临时基于本机的浏览器来调试。
110 |
111 | 1. `export webdriver=127.0.0.1:4444` 或 `set webdriver=127.0.0.1:4444` (Windows)
112 |
113 | 提示:端口号是非必填项,例如:`export webdriver=127.0.0.1`
114 |
--------------------------------------------------------------------------------
/doc/zh-cn/readme.md:
--------------------------------------------------------------------------------
1 | UIRecorder是一款基于WebDriver、Chrome浏览器、NodeJs等方案共同打造的零成本自动化解决方案。
2 |
3 | 基于几乎零成本的录制方案,我们让任何一个完全没有自动化经验的人,可以1分钟录制出可读性高,且强大的自动化脚本。
4 |
5 | 让所有开发和测试能够最低成本的获得自动化测试的能力,把重复又枯燥的测试工作全部交给计算机,彻底的提高测试效率,解放我们的生产力。
6 |
7 | UIRecorder PC使用手册
8 | =================
9 |
10 | 1. [标准入门](pc-standard.md)
11 | 2. [高级使用](pc-advanced.md)
12 | 3. [常见问题](pc-faq.md)
13 |
14 |
15 | UIRecorder 无线使用手册
16 | =================
17 |
18 | 1. [简单配置](https://macacajs.github.io/zh/guide/recorder.html)
19 |
--------------------------------------------------------------------------------
/i18n/en.js:
--------------------------------------------------------------------------------
1 | {
2 | "update_tip": " Update available %s → %s\n Run %s to update",
3 | "please_reinit": "Please reinitialize: uirecorder init",
4 | "webdriver_host": "Webdriver host or ip",
5 | "webdriver_port": "Webdriver port",
6 | "webdriver_browsers": "Browsers list",
7 | "file_saved": "file saved",
8 | "file_created": "file created",
9 | "dir_created": "directory created",
10 | "json_parse_failed": "json parse failed!",
11 | "file_missed": "file search failed, please init first!",
12 | "input_spec_name": "Test spec file name:",
13 | "continue_to_record": "File existed, load and continue to record?",
14 | "mobile_app_path": "App Path (extensions: apk, app, zip):",
15 | "mobile_platform": "App platform:",
16 | "open_checker_browser": "Open checker browser?",
17 | "browser_size": "Browser size (example: 1024 x 768):",
18 | "dom_path_config": "Dom path config, extend: id, name, class",
19 | "attr_black_list": "Black list RegExp for attribute value",
20 | "class_black_list": "Black list RegExp for class value",
21 | "hide_before_expect": "Hide before expect",
22 | "exec_succeed": "execute succeed",
23 | "exec_failed": "execute failed",
24 | "recorder_browser_opened": "Recorder browser opened",
25 | "recorder_browser_title": "Recorder browser",
26 | "checker_browser_opened": "Checker browser opened",
27 | "checker_browser_title": "Checker browser",
28 | "recorder_server_closed": "Recorder server closed",
29 | "recorder_browser_closed": "Recorder browser closed",
30 | "checker_browser_closed": "Checker browser closed",
31 | "test_spec_saved": "Recorded test saved: ",
32 | "raw_cmds_saved": "Raw cmds saved: ",
33 | "config_saved": "Config saved: ",
34 | "check_sumary": "Record %s steps, %s steps check succeed, %s steps check failed",
35 | "no_step_recorded": "No step recorded!",
36 | "nocheck_sumary": "Record %s steps %s",
37 | "nocheck": "no checked",
38 | "recorder_server_listen_on": "Recorder server listen on: %s",
39 | "chrome_init_failed": "Chrome init failed!",
40 | "localhost_hosts_tip": "Please try to add `127.0.0.1 localhost` to your hosts!",
41 | "mobile_open_first": "Please connect your mobile, or open emulator first.",
42 | "mobile_open_failed": "Mobile open failed, please check macaca config.",
43 | "exec_succeed": " execute succeed",
44 | "exec_failed": " execute failed",
45 | "chrome": {
46 | "jump_placeholder": "Please input url or test file name",
47 | "jump_alert": "Please input correct url or test file name!",
48 | "start_button": "Start Record",
49 | "icon_record_tip": "Recording...click for end record",
50 | "icon_end_tip": "Record ended",
51 | "icon_end_msg": "Record is ended, please reopen client!",
52 | "module_start_title": "Test is loading",
53 | "module_start_message": "Test loading: %s, please dont't press any keyboard or mouse when loading!",
54 | "module_end_title": "Test loaded",
55 | "module_end_message": "Test load %s: %s, please continue recording.",
56 | "exec_succeed": "execute succeed",
57 | "exec_failed": "execute failed",
58 | "succeed": "succeed",
59 | "failed": "failed",
60 | "loading": "Loading……",
61 | "attr_switch": "Attr switch: ",
62 | "attr_black": "Attr black: ",
63 | "attr_black_tip": "Please input RegExp for filter attr value, E.g.: /black_val/",
64 | "button_hover_on_text": "Add Hover",
65 | "button_hover_off_text": "End Hover",
66 | "button_sleep_text": "Add Sleep",
67 | "button_expect_text": "Add Expect",
68 | "button_vars_text": "Use Var",
69 | "button_jscode_text": "Run JS",
70 | "button_jump_text": "Jump to",
71 | "button_end_text": "End Record",
72 | "button_text_text": "Input Text",
73 | "button_alert_text": "Alert Cmd",
74 | "button_back_text": "Back Button",
75 | "dialog_ok": "Ok",
76 | "dialog_cancel": "Cancel",
77 | "dialog_expect_title": "Add Expect:",
78 | "dialog_expect_sleep": "Delay Time: ",
79 | "dialog_expect_type": "Expect Type: ",
80 | "dialog_expect_dom": "Expect DOM: ",
81 | "dialog_expect_path": "Expect Path: ",
82 | "dialog_expect_param": "Expect param: ",
83 | "dialog_expect_compare": "Expect compare: ",
84 | "dialog_expect_to": "Expect to: ",
85 | "dialog_vars_title": "Use Var: ",
86 | "dialog_vars_type": "Use type: ",
87 | "dialog_vars_type_insert": "Insert var",
88 | "dialog_vars_type_update": "Update var",
89 | "dialog_vars_name": "Var name: ",
90 | "dialog_vars_addname": "Add var",
91 | "dialog_vars_name_empty": "Name empty!",
92 | "dialog_vars_name_duplicated": "Name duplicated!",
93 | "dialog_vars_value": "Var value: ",
94 | "dialog_vars_template": "Var template: ",
95 | "dialog_vars_template_result": "Template result: ",
96 | "dialog_vars_update_type": "Update type: ",
97 | "dialog_vars_update_dom": "Update DOM: ",
98 | "dialog_vars_update_param": "Update param: ",
99 | "dialog_vars_update_regex": "Update regex: ",
100 | "dialog_jump_title": "Jump to: ",
101 | "dialog_jump_target": "Jump target: ",
102 | "dialog_sleep_title": "Add sleep: ",
103 | "dialog_sleep_time": "Sleep time: ",
104 | "dialog_sleep_time_tip": "Please input sleep time",
105 | "dialog_text_title": "Input Text: ",
106 | "dialog_text_content": "Text Content: ",
107 | "dialog_text_content_tip": "Please input text content",
108 | "dialog_alert_title": "Alert Cmd: ",
109 | "dialog_alert_option": "Alert option: ",
110 | "dialog_alert_option_accpet": "Accpet",
111 | "dialog_alert_option_dismiss": "Dismiss",
112 | "dialog_jscode_title": "Run JS",
113 | "dialog_jscode_code": "JS Code",
114 | "dialog_jscode_tip": "Please input jscode run in browser side,E.g.: document.title='test';"
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/i18n/zh-cn.js:
--------------------------------------------------------------------------------
1 | {
2 | "update_tip": " 发现新版本: %s → %s\n 执行命令升级:%s",
3 | "please_reinit": "请重新初始化: uirecorder init",
4 | "webdriver_host": "WebDriver域名或IP",
5 | "webdriver_port": "WebDriver端口号",
6 | "webdriver_browsers": "需要同时测试的浏览器列表",
7 | "file_saved": "文件保存成功",
8 | "file_created": "文件创建成功",
9 | "dir_created": "文件夹创建成功",
10 | "json_parse_failed": "JSON解析失败!",
11 | "file_missed": "文件查找失败,请先初始化!",
12 | "input_spec_name": "测试脚本文件名:",
13 | "continue_to_record": "文件已存在,加载并继续录制吗?",
14 | "mobile_app_path": "App路径 (扩展名: apk, app, zip):",
15 | "mobile_platform": "App平台:",
16 | "open_checker_browser": "打开同步校验浏览器?",
17 | "browser_size": "浏览器大小 (格式:1024 x 768):",
18 | "dom_path_config": "Path扩展属性配置,除id,name,class之外",
19 | "attr_black_list": "属性值黑名单正则",
20 | "class_black_list": "class值黑名单正则",
21 | "hide_before_expect": "断言前隐藏",
22 | "exec_succeed": "执行成功",
23 | "exec_failed": "执行失败",
24 | "recorder_browser_opened": "录制浏览器已开启",
25 | "recorder_browser_title": "录制浏览器",
26 | "checker_browser_opened": "校验浏览器已开启",
27 | "checker_browser_title": "校验浏览器",
28 | "recorder_server_closed": "录制服务器已关闭",
29 | "recorder_browser_closed": "录制浏览器已关闭",
30 | "checker_browser_closed": "校验浏览器已关闭",
31 | "test_spec_saved": "录制脚本已保存: ",
32 | "raw_cmds_saved": "原数据已保存: ",
33 | "config_saved": "配置已保存: ",
34 | "check_sumary": "共录制%s个步骤,其中校验通过: %s个,校验失败: %s个",
35 | "no_step_recorded": "无步骤录制!",
36 | "nocheck_sumary": "共录制%s个步骤 %s",
37 | "nocheck": "未经过校验",
38 | "recorder_server_listen_on": "录制服务器监听在端口: %s",
39 | "chrome_init_failed": "Chrome打开失败!",
40 | "localhost_hosts_tip": "请尝试添加 `127.0.0.1 localhost` 到您的hosts文件中!",
41 | "mobile_open_first": "请先连接手机或打开模拟器。",
42 | "mobile_open_failed": "手机打开失败,请检查macaca是否配置正确?",
43 | "exec_succeed": " 执行成功",
44 | "exec_failed": " 执行失败",
45 | "chrome": {
46 | "jump_placeholder": "请输入需要测试的URL 或 脚本文件名!",
47 | "jump_alert": "请输入正确的URL 或 脚本文件名!",
48 | "start_button": "开始录制",
49 | "icon_record_tip": "录制中……点击结束录制",
50 | "icon_end_tip": "录制已结束",
51 | "icon_end_msg": "录制已经结束,请从客户端重新初始化!",
52 | "module_start_title": "脚本加载开始",
53 | "module_start_message": "开始加载脚本: %s,加载过程中请勿进行任何键盘和鼠标操作。",
54 | "module_end_title": "脚本加载结束",
55 | "module_end_message": "脚本加载%s: %s,请继续录制操作。",
56 | "exec_succeed": "执行成功",
57 | "exec_failed": "执行失败",
58 | "succeed": "成功",
59 | "failed": "失败",
60 | "loading": "等待加载……",
61 | "attr_switch": "属性开关: ",
62 | "attr_black": "属性黑名单: ",
63 | "attr_black_tip": "请输入过滤属性值的正则表达式, 例如:/black_val/",
64 | "button_hover_on_text": "添加悬停",
65 | "button_hover_off_text": "结束悬停",
66 | "button_sleep_text": "添加延迟",
67 | "button_expect_text": "添加断言",
68 | "button_vars_text": "使用变量",
69 | "button_jscode_text": "执行JS",
70 | "button_jump_text": "脚本跳转",
71 | "button_end_text": "结束录制",
72 | "button_text_text": "输入文字",
73 | "button_alert_text": "Alert命令",
74 | "button_back_text": "后退按键",
75 | "dialog_ok": "确认",
76 | "dialog_cancel": "取消",
77 | "dialog_expect_title": "添加断言: ",
78 | "dialog_expect_sleep": "延迟时间: ",
79 | "dialog_expect_type": "断言类型: ",
80 | "dialog_expect_dom": "断言DOM: ",
81 | "dialog_expect_path": "断言Path: ",
82 | "dialog_expect_param": "断言参数: ",
83 | "dialog_expect_compare": "比较方式: ",
84 | "dialog_expect_to": "断言结果: ",
85 | "dialog_vars_title": "使用变量: ",
86 | "dialog_vars_type": "使用方式: ",
87 | "dialog_vars_type_insert": "插入变量",
88 | "dialog_vars_type_update": "更新变量",
89 | "dialog_vars_name": "变量名: ",
90 | "dialog_vars_addname": "添加变量",
91 | "dialog_vars_name_empty": "变量名不能为空!",
92 | "dialog_vars_name_duplicated": "变量名重复!",
93 | "dialog_vars_value": "变量值: ",
94 | "dialog_vars_template": "变量模板: ",
95 | "dialog_vars_template_result": "模板结果: ",
96 | "dialog_vars_update_type": "取值方式: ",
97 | "dialog_vars_update_dom": "取值DOM: ",
98 | "dialog_vars_update_param": "取值参数: ",
99 | "dialog_vars_update_regex": "取值正则: ",
100 | "dialog_jump_title": "脚本跳转: ",
101 | "dialog_jump_target": "跳转目标: ",
102 | "dialog_sleep_title": "添加延迟: ",
103 | "dialog_sleep_time": "延迟时间: ",
104 | "dialog_sleep_time_tip": "请输入延迟时间,单位毫秒",
105 | "dialog_text_title": "输入文字: ",
106 | "dialog_text_content": "文字内容: ",
107 | "dialog_text_content_tip": "请输入文字",
108 | "dialog_alert_title": "Alert命令: ",
109 | "dialog_alert_option": "Alert选项: ",
110 | "dialog_alert_option_accpet": "接受",
111 | "dialog_alert_option_dismiss": "拒绝",
112 | "dialog_jscode_title": "执行JS",
113 | "dialog_jscode_code": "JS代码",
114 | "dialog_jscode_tip": "请输入浏览器端执行的JS代码,例如: document.title='test';"
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/i18n/zh-tw.js:
--------------------------------------------------------------------------------
1 | {
2 | "update_tip": " 發現新版本: %s → %s\n 執行命令升級:%s",
3 | "please_reinit": "請重新初始化: uirecorder init",
4 | "webdriver_host": "WebDriver域名或IP",
5 | "webdriver_port": "WebDriver端口號",
6 | "webdriver_browsers": "需要同時測試的瀏覽器列表",
7 | "file_saved": "文件保存成功",
8 | "file_created": "文件創建成功",
9 | "dir_created": "文件夾創建成功",
10 | "json_parse_failed": "JSON解析失敗!",
11 | "file_missed": "文件查找失敗,請先初始化!",
12 | "input_spec_name": "測試腳本文件名:",
13 | "continue_to_record": "文件已存在,加載並繼續錄制嗎?",
14 | "mobile_app_path": "App路徑 (擴展名: apk, app, zip):",
15 | "mobile_platform": "App平台:",
16 | "open_checker_browser": "打開同步校驗瀏覽器?",
17 | "browser_size": "瀏覽器大小 (格式:1024 x 768):",
18 | "dom_path_config": "Path擴展屬性配置,除id,name,class之外",
19 | "attr_black_list": "屬性值黑名單正則",
20 | "class_black_list": "class值黑名單正則",
21 | "hide_before_expect": "斷言前隱藏",
22 | "exec_succeed": "執行成功",
23 | "exec_failed": "執行失敗",
24 | "recorder_browser_opened": "錄制瀏覽器已開啓",
25 | "recorder_browser_title": "錄制瀏覽器",
26 | "checker_browser_opened": "校驗瀏覽器已開啓",
27 | "checker_browser_title": "校驗瀏覽器",
28 | "recorder_server_closed": "錄制服務器已關閉",
29 | "recorder_browser_closed": "錄制瀏覽器已關閉",
30 | "checker_browser_closed": "校驗瀏覽器已關閉",
31 | "test_spec_saved": "錄制腳本已保存: ",
32 | "raw_cmds_saved": "原數據已保存: ",
33 | "config_saved": "配置已保存: ",
34 | "check_sumary": "共錄制%s個步驟,其中校驗通過: %s個,校驗失敗: %s個",
35 | "no_step_recorded": "無步驟錄制!",
36 | "nocheck_sumary": "共錄制%s個步驟 %s",
37 | "nocheck": "未經過校驗",
38 | "recorder_server_listen_on": "錄制服務器監聽在端口: %s",
39 | "chrome_init_failed": "Chrome打開失敗!",
40 | "localhost_hosts_tip": "請嘗試添加 `127.0.0.1 localhost` 到您的hosts文件中!",
41 | "mobile_open_first": "請先連接手機或打開模擬器。",
42 | "mobile_open_failed": "手機打開失敗,請檢查macaca是否配置正確?",
43 | "exec_succeed": " 執行成功",
44 | "exec_failed": " 執行失敗",
45 | "chrome": {
46 | "jump_placeholder": "請輸入需要測試的URL 或 腳本文件名!",
47 | "jump_alert": "請輸入正確的URL 或 腳本文件名!",
48 | "start_button": "開始錄制",
49 | "icon_record_tip": "錄制中……點擊結束錄制",
50 | "icon_end_tip": "錄制已結束",
51 | "icon_end_msg": "錄制已經結束,請從客戶端重新初始化!",
52 | "module_start_title": "腳本加載開始",
53 | "module_start_message": "開始加載腳本: %s,加載過程中請勿進行任何鍵盤和鼠標操作。",
54 | "module_end_title": "腳本加載結束",
55 | "module_end_message": "腳本加載%s: %s,請繼續錄制操作。",
56 | "exec_succeed": "執行成功",
57 | "exec_failed": "執行失敗",
58 | "succeed": "成功",
59 | "failed": "失敗",
60 | "loading": "等待加載……",
61 | "attr_switch": "屬性開關: ",
62 | "attr_black": "屬性黑名單: ",
63 | "attr_black_tip": "請輸入過濾屬性值的正則表達式, 例如:/black_val/",
64 | "button_hover_on_text": "添加懸停",
65 | "button_hover_off_text": "結束懸停",
66 | "button_sleep_text": "添加延遲",
67 | "button_expect_text": "添加斷言",
68 | "button_vars_text": "使用變量",
69 | "button_jscode_text": "執行JS",
70 | "button_jump_text": "腳本跳轉",
71 | "button_end_text": "結束錄制",
72 | "button_text_text": "輸入文字",
73 | "button_alert_text": "Alert命令",
74 | "button_back_text": "後退按鍵",
75 | "dialog_ok": "確認",
76 | "dialog_cancel": "取消",
77 | "dialog_expect_title": "添加斷言: ",
78 | "dialog_expect_sleep": "延遲時間: ",
79 | "dialog_expect_type": "斷言類型: ",
80 | "dialog_expect_dom": "斷言DOM: ",
81 | "dialog_expect_path": "斷言Path: ",
82 | "dialog_expect_param": "斷言參數: ",
83 | "dialog_expect_compare": "比較方式: ",
84 | "dialog_expect_to": "斷言結果: ",
85 | "dialog_vars_title": "使用變量: ",
86 | "dialog_vars_type": "使用方式: ",
87 | "dialog_vars_type_insert": "插入變量",
88 | "dialog_vars_type_update": "更新變量",
89 | "dialog_vars_name": "變量名: ",
90 | "dialog_vars_addname": "添加變量",
91 | "dialog_vars_name_empty": "變量名不能為空!",
92 | "dialog_vars_name_duplicated": "變量名重復!",
93 | "dialog_vars_value": "變量值: ",
94 | "dialog_vars_template": "變量模板: ",
95 | "dialog_vars_template_result": "模板結果: ",
96 | "dialog_vars_update_type": "取值方式: ",
97 | "dialog_vars_update_dom": "取值DOM: ",
98 | "dialog_vars_update_param": "取值參數: ",
99 | "dialog_vars_update_regex": "取值正則: ",
100 | "dialog_jump_title": "腳本跳轉: ",
101 | "dialog_jump_target": "跳轉目標: ",
102 | "dialog_sleep_title": "添加延遲: ",
103 | "dialog_sleep_time": "延遲時間: ",
104 | "dialog_sleep_time_tip": "請輸入延遲時間,單位毫秒",
105 | "dialog_text_title": "輸入文字: ",
106 | "dialog_text_content": "文字內容: ",
107 | "dialog_text_content_tip": "請輸入文字",
108 | "dialog_alert_title": "Alert命令: ",
109 | "dialog_alert_option": "Alert選項: ",
110 | "dialog_alert_option_accpet": "接受",
111 | "dialog_alert_option_dismiss": "拒絕",
112 | "dialog_jscode_title": "執行JS",
113 | "dialog_jscode_code": "JS代碼",
114 | "dialog_jscode_tip": "請輸入瀏覽器端執行的JS代碼,例如: document.title='test';"
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var initConfig = require('./lib/init.js');
2 | var startRecorder = require('./lib/start.js');
3 | var checkUpdate = require('./lib/update.js');
4 |
5 | module.exports = {
6 | init: initConfig,
7 | start: startRecorder,
8 | checkUpdate: checkUpdate
9 | };
10 |
--------------------------------------------------------------------------------
/lib/builder/java.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | function getJavaTemplateContent(rootPath) {
5 | var templateName = 'javaTemplate.java';
6 | var tempalteFilePath = path.resolve(__dirname, '../../template/' + templateName);
7 | var customTemplateFilePath = path.join(rootPath, '../template/', templateName);
8 | if (fs.existsSync(customTemplateFilePath)) {
9 | tempalteFilePath = customTemplateFilePath;
10 | }
11 | return fs.readFileSync(tempalteFilePath).toString();
12 | }
13 |
14 | function getArrRawCmdsTarget(arrRawCmdsTarget, browserSize, arrRawCmds) {
15 | if (browserSize) {
16 | arrRawCmdsTarget[0] = 'Configuration.browserSize = "' + browserSize[0] + 'x' + browserSize[1] + '";';
17 | } else {
18 | arrRawCmdsTarget[0] = 'Configuration.startMaximized=true;';
19 | }
20 | for (jj = 0, len = arrRawCmds.length; jj < len; jj++) {
21 | // console.log('arrRawCmds[jj].type:'+arrRawCmds[jj].type);
22 | switch (arrRawCmds[jj].type) {
23 | case 'url':
24 | arrRawCmds[jj].data = arrRawCmds[jj].data.replace(/\"/g, "'");
25 | arrRawCmdsTarget[jj + 1] = 'actions.openUrl("' + arrRawCmds[jj].data + '");' + '//打开URL';
26 | console.log('lion arrRawCmdsTarget[' + jj + 1 + ']:' + arrRawCmdsTarget[jj + 1]);
27 | break;
28 | case 'dblClick':
29 | arrRawCmds[jj].data.path = arrRawCmds[jj].data.path.replace(/\"/g, "'");
30 | arrRawCmdsTarget[jj + 1] = 'actions.click("' + arrRawCmds[jj].data.path + '");' + '//点击:' + arrRawCmds[jj].data.text;
31 | console.log('lion arrRawCmdsTarget[' + jj + 1 + ']:' + arrRawCmdsTarget[jj + 1]);
32 | break;
33 | case 'click':
34 | arrRawCmds[jj].data.path = arrRawCmds[jj].data.path.replace(/\"/g, "'");
35 | if (arrRawCmds[jj].data.path.indexOf('radio') != -1 || arrRawCmds[jj].data.path.indexOf('checkbox') != -1 || arrRawCmds[jj].data.path.indexOf('combobox') != -1) {
36 | arrRawCmdsTarget[jj + 1] = 'actions.clickByJS("' + arrRawCmds[jj].data.path + '");' + '//点击:' + arrRawCmds[jj].data.text;
37 | } else {
38 | arrRawCmdsTarget[jj + 1] = 'actions.click("' + arrRawCmds[jj].data.path + '");' + '//点击:' + arrRawCmds[jj].data.text;
39 | }
40 | console.log('lion arrRawCmdsTarget[' + jj + 1 + ']:' + arrRawCmdsTarget[jj + 1]);
41 | break;
42 | case 'switchWindow':
43 | //arrRawCmds[jj].path = arrRawCmds[jj].data.replace(/\"/g, "'");
44 | arrRawCmdsTarget[jj + 1] = 'actions.swtichToWindow(' + arrRawCmds[jj].data + ');' + '//切换窗口';
45 | console.log('lion arrRawCmdsTarget[' + jj + 1 + ']:' + arrRawCmdsTarget[jj + 1]);
46 | break;
47 | case 'sendKeys':
48 | // arrRawCmds[jj - 1].data.path = arrRawCmds[jj - 1].data.path.replace(/\"/g, "'");
49 | // arrRawCmdsTarget[jj + 1] = 'actions.input("' + arrRawCmds[jj - 1].data.path + '","' + arrRawCmds[jj].data.keys + '");'+'//文本框'+arrRawCmds[jj - 1].data.text+'输入:'+arrRawCmds[jj].data.keys;
50 | arrRawCmdsTarget[jj + 1] = 'actions.sendKeys("' + arrRawCmds[jj].data.keys + '");' + '//输入:' + arrRawCmds[jj].data.keys;
51 | console.log('lion arrRawCmdsTarget[' + jj + 1 + ']:' + arrRawCmdsTarget[jj + 1]);
52 | break;
53 | case 'waitBody':
54 | arrRawCmdsTarget[jj + 1] = 'actions.sleep(3000);';
55 | console.log('lion arrRawCmdsTarget[' + jj + 1 + ']:' + arrRawCmdsTarget[jj + 1]) + '//waitBody等待页面加载完成';
56 | break;
57 | // case 'switchFrame':
58 | // arrRawCmds[jj].data = arrRawCmds[jj].data.replace(/\"/g,"'");
59 | // arrRawCmdsTarget[jj]='actions.switchToFrame("'+arrRawCmds[jj].data+'");';
60 | // console.log('lion arrRawCmdsTarget['+jj+']:'+arrRawCmdsTarget[jj]);
61 | // break;
62 | case 'switchFrame':
63 | if (null == arrRawCmds[jj].data) {
64 | arrRawCmdsTarget[jj + 1] = 'actions.switchToDefaultContent();' + '//退出frame';
65 | } else {
66 | arrRawCmds[jj].data = arrRawCmds[jj].data.replace(/#/g, "");
67 | arrRawCmdsTarget[jj + 1] = 'actions.switchToFrame("' + arrRawCmds[jj].data + '");' + '//切换frame';
68 | }
69 | console.log('lion arrRawCmdsTarget[' + jj + 1 + ']:' + arrRawCmdsTarget[jj + 1]);
70 | break;
71 | case 'scrollTo':
72 | arrRawCmdsTarget[jj + 1] = 'actions.scrollTo(' + arrRawCmds[jj].data.x + ',' + arrRawCmds[jj].data.y + ');' + '//滚动页面';
73 | console.log('lion scrollTo[' + jj + 1 + ']:' + arrRawCmdsTarget[jj + 1]);
74 | break;
75 | case 'scrollElementTo':
76 | arrRawCmdsTarget[jj + 1] = 'actions.scrollElementTo("' + arrRawCmds[jj].data.path + '",' + arrRawCmds[jj].data.x + ',' + arrRawCmds[jj].data.y + ');' + '//滚动元素';
77 | console.log('lion scrollTo[' + jj + 1 + ']:' + arrRawCmdsTarget[jj + 1]);
78 | break;
79 | case 'closeWindow':
80 | arrRawCmdsTarget[jj + 1] = 'actions.closeCurrentWindow();' + '//关闭当前窗口'
81 | console.log('lion scrollTo[' + jj + 1 + ']:' + arrRawCmdsTarget[jj + 1]);
82 | break;
83 | case 'sleep':
84 | arrRawCmdsTarget[jj + 1] = 'actions.sleep(' + arrRawCmds[jj].data + ');' + '//sleep';
85 | console.log('lion scrollTo[' + jj + 1 + ']:' + arrRawCmdsTarget[jj + 1]);
86 | break;
87 | case 'mouseMove':
88 | arrRawCmdsTarget[jj + 1] = 'actions.hover("' + arrRawCmds[jj].data.path + '");' + '//鼠标hover:' + arrRawCmds[jj].data.text;
89 | console.log('lion scrollTo[' + jj + 1 + ']:' + arrRawCmdsTarget[jj + 1]);
90 | break;
91 | case 'expect':
92 | switch (arrRawCmds[jj].data.type) {
93 | case 'displayed':
94 | arrRawCmds[jj].data.params[0] = arrRawCmds[jj].data.params[0].replace(/\"/g, "'");
95 | if ((arrRawCmds[jj].data.compare == 'equal') && (arrRawCmds[jj].data.to == 'true')) {
96 | arrRawCmdsTarget[jj + 1] = 'Assert.assertTrue(actions.waitElementFound("' + arrRawCmds[jj].data.params[0] + '"));' + '//校验元素存在'
97 | console.log('lion scrollTo[' + jj + 1 + ']:' + arrRawCmdsTarget[jj + 1]);
98 | } else {
99 | arrRawCmdsTarget[jj + 1] = 'Assert.assertFalse(actions.waitElementDisapear("' + arrRawCmds[jj].data.params[0] + '"));' + '//校验元素不存在'
100 | console.log('lion scrollTo[' + jj + 1 + ']:' + arrRawCmdsTarget[jj + 1]);
101 | }
102 | break;
103 | case 'text':
104 | arrRawCmds[jj].data.params[0] = arrRawCmds[jj].data.params[0].replace(/\"/g, "'");
105 | if (arrRawCmds[jj].data.compare == 'equal') {
106 | arrRawCmdsTarget[jj + 1] = 'Assert.assertTrue(actions.getText("' + arrRawCmds[jj].data.params[0] + '").equals("' + arrRawCmds[jj].data.to + '"));' + '//校验元素的文本值满足期望'
107 | console.log('lion scrollTo[' + jj + 1 + ']:' + arrRawCmdsTarget[jj + 1]);
108 | } else {
109 | arrRawCmdsTarget[jj + 1] = 'Assert.assertFalse(actions.getText("' + arrRawCmds[jj].data.params[0] + '").equals("' + arrRawCmds[jj].data.to + '"));' + '//校验元素的文本值不满足期望'
110 | console.log('lion scrollTo[' + jj + 1 + ']:' + arrRawCmdsTarget[jj + 1]);
111 | }
112 | break;
113 | }
114 | break;
115 | }
116 | }
117 | return arrRawCmdsTarget;
118 | }
119 |
120 | module.exports = {
121 | getJavaTemplateContent,
122 | getArrRawCmdsTarget,
123 | };
--------------------------------------------------------------------------------
/lib/i18n.js:
--------------------------------------------------------------------------------
1 | var i18n = require('i18n');
2 | var path = require('path');
3 | var osLocale = require('os-locale');
4 | i18n.configure({
5 | locales:['en', 'zh-cn', 'zh-tw'],
6 | defaultLocale: 'en',
7 | directory: path.resolve(__dirname, '../i18n/'),
8 | updateFiles: false,
9 | extension: '.js',
10 | register: global
11 | });
12 |
13 | var loc = osLocale.sync();
14 | loc = loc.toLowerCase().replace(/_/g, '-');
15 | i18n.setLocale(loc);
16 | module.exports = i18n;
17 |
--------------------------------------------------------------------------------
/lib/init.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs-extra')
2 | var path = require('path');
3 | var cp = require('child_process');
4 | var inquirer = require('inquirer');
5 | var npminstall = require('npminstall');
6 | var i18n = require('./i18n');
7 | var co = require('co');
8 |
9 | var projectPath = path.resolve(__dirname, '../project');
10 |
11 | function initConfig(options){
12 | var locale = options.locale;
13 | var mobile = options.mobile;
14 | if(locale){
15 | i18n.setLocale(locale);
16 | }
17 | var configPath = 'config.json';
18 | var configFile = path.resolve(configPath);
19 | var config = {};
20 | if(fs.existsSync(configFile)){
21 | var content = fs.readFileSync(configFile).toString();
22 | try{
23 | config = JSON.parse(content);
24 | }
25 | catch(e){}
26 | }
27 | var recorder;
28 | config.webdriver = config.webdriver || {};
29 | config.vars = config.vars || {};
30 | config.reporter = config.reporter || { distDir: '' };
31 | config.screenshots = config.screenshots || { captureAll: true };
32 | var webdriver = config.webdriver;
33 | webdriver.host = webdriver.host || '127.0.0.1';
34 | webdriver.port = webdriver.port || '4444';
35 | webdriver.chromeOptions = webdriver.chromeOptions || { w3c: false };
36 | if(!mobile){
37 | config.recorder = config.recorder || {};
38 | recorder = config.recorder;
39 | recorder.pathAttrs = recorder.pathAttrs || 'data-id,data-name,type,data-type,role,data-role,data-value';
40 | recorder.attrValueBlack = recorder.attrValueBlack || '';
41 | recorder.classValueBlack = recorder.classValueBlack || '';
42 | recorder.hideBeforeExpect = recorder.hideBeforeExpect || '';
43 | webdriver.browsers = webdriver.browsers || 'chrome, ie 11';
44 | }
45 | var questions = [];
46 | if(mobile){
47 | questions = [
48 | {
49 | 'type': 'input',
50 | 'name': 'host',
51 | 'message': __('webdriver_host'),
52 | 'default': webdriver.host,
53 | 'filter': function(input){
54 | return input.replace(/(^\s+|\s+$)/g, '');
55 | },
56 | 'validate': function(input){
57 | return input !== '' && /^https?:\/\//.test(input) === false;
58 | }
59 | },
60 | {
61 | 'type': 'input',
62 | 'name': 'port',
63 | 'message': __('webdriver_port'),
64 | 'default': webdriver.port,
65 | 'filter': function(input){
66 | return input.replace(/(^\s+|\s+$)/g, '');
67 | },
68 | 'validate': function(input){
69 | return input !== '';
70 | }
71 | }
72 | ];
73 | }
74 | else{
75 | questions = [
76 | {
77 | 'type': 'input',
78 | 'name': 'pathAttrs',
79 | 'message': __('dom_path_config'),
80 | 'default': recorder.pathAttrs,
81 | 'filter': function(input){
82 | return input.replace(/(^\s+|\s+$)/g, '');
83 | }
84 | },
85 | {
86 | 'type': 'input',
87 | 'name': 'attrValueBlack',
88 | 'message': __('attr_black_list'),
89 | 'default': recorder.attrValueBlack,
90 | 'filter': function(input){
91 | return input.replace(/(^\s+|\s+$)/g, '');
92 | }
93 | },
94 | {
95 | 'type': 'input',
96 | 'name': 'classValueBlack',
97 | 'message': __('class_black_list'),
98 | 'default': recorder.classValueBlack,
99 | 'filter': function(input){
100 | return input.replace(/(^\s+|\s+$)/g, '');
101 | }
102 | },
103 | {
104 | 'type': 'input',
105 | 'name': 'hideBeforeExpect',
106 | 'message': __('hide_before_expect'),
107 | 'default': recorder.hideBeforeExpect,
108 | 'filter': function(input){
109 | return input.replace(/(^\s+|\s+$)/g, '');
110 | }
111 | },
112 | {
113 | 'type': 'input',
114 | 'name': 'host',
115 | 'message': __('webdriver_host'),
116 | 'default': webdriver.host,
117 | 'filter': function(input){
118 | return input.replace(/(^\s+|\s+$)/g, '');
119 | },
120 | 'validate': function(input){
121 | return input !== '' && /^https?:\/\//.test(input) === false;
122 | }
123 | },
124 | {
125 | 'type': 'input',
126 | 'name': 'port',
127 | 'message': __('webdriver_port'),
128 | 'default': webdriver.port,
129 | 'filter': function(input){
130 | return input.replace(/(^\s+|\s+$)/g, '');
131 | },
132 | 'validate': function(input){
133 | return input !== '';
134 | }
135 | },
136 | {
137 | 'type': 'input',
138 | 'name': 'browsers',
139 | 'message': __('webdriver_browsers'),
140 | 'default': webdriver.browsers,
141 | 'filter': function(input){
142 | return input.replace(/(^\s+|\s+$)/g, '');
143 | },
144 | 'validate': function(input){
145 | return input !== '';
146 | }
147 | }
148 | ];
149 | }
150 | inquirer.prompt(questions).then(function(anwsers){
151 | webdriver.host = String(anwsers.host).replace(/^\s+|\s+$/g, '');
152 | webdriver.port = String(anwsers.port).replace(/^\s+|\s+$/g, '');
153 | if(!mobile){
154 | recorder.pathAttrs = String(anwsers.pathAttrs).replace(/^\s+|\s+$/g, '');
155 | recorder.attrValueBlack = String(anwsers.attrValueBlack).replace(/^\s+|\s+$/g, '');
156 | recorder.classValueBlack = String(anwsers.classValueBlack).replace(/^\s+|\s+$/g, '');
157 | recorder.hideBeforeExpect = String(anwsers.hideBeforeExpect).replace(/^\s+|\s+$/g, '');
158 | webdriver.browsers = String(anwsers.browsers).replace(/^\s+|\s+$/g, '');
159 | }
160 | fs.writeFileSync(configFile, JSON.stringify(config, null, 4));
161 | console.log('');
162 | console.log(configPath.bold+' '+__('file_saved').green);
163 | if(mobile){
164 | initProject({
165 | 'package.json':'',
166 | 'README.md':'',
167 | 'screenshots':'',
168 | 'commons':'',
169 | '.editorconfig':'',
170 | '.gitignore1':'.gitignore',
171 | 'install.sh':'',
172 | 'run.bat':'',
173 | 'run.sh':'',
174 | 'vslaunch.json':'.vscode/launch.json'
175 | });
176 | }
177 | else{
178 | var hostsPath = 'hosts';
179 | initProject({
180 | 'package.json':'',
181 | 'README.md':'',
182 | 'screenshots':'',
183 | 'commons':'',
184 | 'uploadfiles':'',
185 | '.editorconfig':'',
186 | '.gitignore1':'.gitignore',
187 | 'install.sh':'',
188 | 'run.bat':'',
189 | 'run.sh':'',
190 | 'hosts': hostsPath,
191 | 'vslaunch.json':'.vscode/launch.json'
192 | });
193 | }
194 | co(function* () {
195 | console.log('');
196 | console.log('Start install project dependencies...'.cyan);
197 | console.log('--------------------------------------------');
198 | console.log('');
199 | var npminstallOptions = {
200 | root: process.cwd(),
201 | registry: 'https://registry.npm.taobao.org'
202 | };
203 | var proxy;
204 | try{
205 | var cnpmout = cp.execSync('cnpm config get proxy', {stdio:['pipe', 'pipe', 'ignore']});
206 | cnpmout = cnpmout.toString().trim();
207 | if(/^http:\/\//.test(cnpmout)){
208 | proxy = cnpmout;
209 | }
210 | }
211 | catch(e){}
212 | if(proxy){
213 | npminstallOptions.proxy = proxy;
214 | console.log('Find proxy from cnpm:', proxy);
215 | }
216 | yield npminstall(npminstallOptions);
217 | if(!mobile){
218 | console.log('');
219 | console.log('Start install webdriver dependencies...'.cyan);
220 | console.log('--------------------------------------------');
221 | console.log('');
222 | var installdriver = cp.exec('npm run installdriver');
223 | installdriver.stdout.on('data', function(data){
224 | console.log(data);
225 | });
226 | }
227 | });
228 | }).catch(function(err){
229 | console.log(err)
230 | });
231 |
232 | }
233 |
234 | function initProject(mapFiles){
235 | for(var key in mapFiles){
236 | initProjectFileOrDir(key, mapFiles[key]);
237 | }
238 | }
239 |
240 | function initProjectFileOrDir(srcName, descName){
241 | descName = descName || srcName;
242 | var srcFile = projectPath + '/' + srcName;
243 | var destFile = path.resolve(descName);
244 | if(fs.existsSync(destFile) === false){
245 | fs.copySync(srcFile, destFile);
246 | console.log(descName.bold+' '+__(fs.statSync(srcFile).isDirectory()?'dir_created':'file_created').green);
247 | }
248 | }
249 |
250 | module.exports = initConfig;
251 |
--------------------------------------------------------------------------------
/lib/update.js:
--------------------------------------------------------------------------------
1 | var latestVersion = require('latest-version');
2 | var semver = require('semver');
3 | var pkg = require('../package.json');
4 | var i18n = require('./i18n');
5 |
6 | function checkUpdate(locale){
7 | if(locale){
8 | i18n.setLocale(locale);
9 | }
10 | var pkgName = pkg.name;
11 | latestVersion(pkgName).then(function(newVersion){
12 | var oldVersion = pkg.version;
13 | var isNew = semver.gt(newVersion, oldVersion)
14 | if(isNew){
15 | process.on('exit', function(){
16 | var updateCmd = 'npm update '+pkgName+' -g';
17 | console.log('');
18 | console.log('------------------------------------------------------------------'.yellow);
19 | console.log('');
20 | console.log(__('update_tip', oldVersion.gray, newVersion.green.bold, updateCmd.cyan.bold));
21 | console.log('');
22 | console.log('------------------------------------------------------------------'.yellow);
23 | });
24 | process.on('SIGINT', function () {
25 | console.error('');
26 | process.exit();
27 | });
28 | }
29 | });
30 | }
31 |
32 | module.exports = checkUpdate;
33 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/logo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "uirecorder",
3 | "version": "4.0.2-beta.2",
4 | "description": "Tool for record ui test case",
5 | "main": "./index",
6 | "bin": {
7 | "uirecorder": "./bin/uirecorder"
8 | },
9 | "dependencies": {
10 | "jquery": "3.x",
11 | "async": "2.1.2",
12 | "chai": "3.5.0",
13 | "chromedriver": "latest",
14 | "co": "4.6.0",
15 | "colors": "1.1.2",
16 | "commander": "2.9.0",
17 | "detect-port": "^1.3.0",
18 | "fs-extra": "1.0.0",
19 | "i18n": "0.8.3",
20 | "inquirer": "3.0.1",
21 | "jwebdriver": "2.2.6",
22 | "latest-version": "2.0.0",
23 | "npminstall": "3.1.1",
24 | "os-locale": "1.4.0",
25 | "resemblejs-node": "1.0.0",
26 | "semver": "5.3.0",
27 | "websocket": "1.0.22"
28 | },
29 | "devDependencies": {
30 | "git-contributor": "^1.0.8"
31 | },
32 | "scripts": {
33 | "contributor": "git-contributor"
34 | },
35 | "repository": {
36 | "type": "git",
37 | "url": "git@github.com:alibaba/uirecorder.git"
38 | },
39 | "bugs": {
40 | "url": "https://github.com/alibaba/uirecorder/issues"
41 | },
42 | "keywords": [
43 | "uirecorder",
44 | "webdriver",
45 | "test",
46 | "recorder"
47 | ],
48 | "author": "Yanis Wang, Stngle",
49 | "license": "MIT"
50 | }
51 |
--------------------------------------------------------------------------------
/project/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | # Apply for all files
8 | [*]
9 |
10 | charset = utf-8
11 |
12 | indent_style = space
13 | indent_size = 4
14 |
15 | end_of_line = lf
16 | insert_final_newline = true
17 | trim_trailing_whitespace = true
18 |
--------------------------------------------------------------------------------
/project/.gitignore1:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | node_modules
4 | npm-debug.log
5 | uirecorder.log
6 | reports
7 | screenshots/**/*.png
8 | screenshots/**/*.html
9 | screenshots/**/*.json
10 |
--------------------------------------------------------------------------------
/project/README.md:
--------------------------------------------------------------------------------
1 | UI Recorder test sample project
2 | ================
3 |
4 | It's a UI Recorder test sample project.
5 |
6 | Save your test code here.
7 |
8 | Get more info: [http://uirecorder.com/](http://uirecorder.com/)
9 |
10 | How to run test case?
11 | ================
12 |
13 | 1. npm install
14 | 2. source run.sh ( Linux|Mac ) or run.bat ( Windows )
15 |
16 | How to dock jenkins?
17 | ================
18 |
19 | 1. Add commands
20 |
21 | source ./install.sh
22 | source ./run.sh
23 |
24 | 2. Add reports
25 |
26 | > JUnit: reports/index.xml
27 |
28 | > HTML: reports/
29 |
--------------------------------------------------------------------------------
/project/commons/commons.md:
--------------------------------------------------------------------------------
1 | Please save common test case here.
--------------------------------------------------------------------------------
/project/hosts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/project/hosts
--------------------------------------------------------------------------------
/project/install.sh:
--------------------------------------------------------------------------------
1 | ls ~/nvm || git clone https://github.com/creationix/nvm.git ~/nvm
2 | source ~/nvm/nvm.sh
3 | nvm install v7.10.0
4 | npm install
5 |
--------------------------------------------------------------------------------
/project/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "uirecorderTest",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "",
6 | "dependencies": {
7 | "chai": "3.5.0",
8 | "jwebdriver": "2.3.4",
9 | "mocha": "5",
10 | "macaca-mocha-parallel-tests": "2.x",
11 | "macaca-reporter": "^1.3.24",
12 | "resemblejs-node": "1.0.0",
13 | "selenium-standalone": "6.x.x",
14 | "jquery": "3.x"
15 | },
16 | "devDependencies": {
17 | },
18 | "scripts": {
19 | "installdriver": "selenium-standalone install --drivers.firefox.baseURL=http://npm.taobao.org/mirrors/geckodriver --baseURL=http://npm.taobao.org/mirrors/selenium --drivers.chrome.baseURL=http://npm.taobao.org/mirrors/chromedriver --drivers.ie.baseURL=http://npm.taobao.org/mirrors/selenium",
20 | "server": "selenium-standalone start",
21 | "test": "mocha \"!(node_modules)/**/*.spec.js\" --reporter macaca-reporter --reporter-options reportJSONFilename=index,processAlwaysExitWithZero=true --bail",
22 | "singletest": "mocha --reporter macaca-reporter --reporter-options reportJSONFilename=index,processAlwaysExitWithZero=true --bail",
23 | "paralleltest": "macaca-mocha-parallel-tests \"!(node_modules)/**/*.spec.js\" --reporter macaca-reporter --reporter-options reportJSONFilename=index,processAlwaysExitWithZero=true --max-parallel 5 --bail",
24 | "moduletest": "macaca-mocha-parallel-tests --reporter macaca-reporter --reporter-options reportJSONFilename=index,processAlwaysExitWithZero=true --max-parallel 5 --bail"
25 | },
26 | "author": ""
27 | }
28 |
--------------------------------------------------------------------------------
/project/run.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | if "%1" neq "" (
4 | npm run singletest %1 %2
5 | ) else (
6 | npm run paralleltest
7 | )
8 |
--------------------------------------------------------------------------------
/project/run.sh:
--------------------------------------------------------------------------------
1 | if [ "$1" = "" ]; then
2 | npm run paralleltest
3 | else
4 | npm run singletest $1 $2
5 | fi
6 |
--------------------------------------------------------------------------------
/project/screenshots/screenshots.md:
--------------------------------------------------------------------------------
1 | Test screenshots saved here.
--------------------------------------------------------------------------------
/project/uploadfiles/uploadfiles.md:
--------------------------------------------------------------------------------
1 | Please save upload files here.
2 |
--------------------------------------------------------------------------------
/project/vslaunch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible Node.js debug attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Debug UIRecorder Local",
11 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
12 | "cwd": "${workspaceRoot}",
13 | "args": ["--reporter", "macaca-reporter", "${file}"],
14 | "env": {
15 | "webdriver": "127.0.0.1"
16 | }
17 | },
18 | {
19 | "type": "node",
20 | "request": "launch",
21 | "name": "Debug UIRecorder Default",
22 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
23 | "cwd": "${workspaceRoot}",
24 | "args": ["--reporter", "macaca-reporter", "${file}"]
25 | }
26 | ]
27 | }
--------------------------------------------------------------------------------
/screenshot/shot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/screenshot/shot1.png
--------------------------------------------------------------------------------
/screenshot/shot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/screenshot/shot2.png
--------------------------------------------------------------------------------
/screenshot/shot3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/screenshot/shot3.png
--------------------------------------------------------------------------------
/screenshot/shot4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/screenshot/shot4.png
--------------------------------------------------------------------------------
/template/javaTemplate.java:
--------------------------------------------------------------------------------
1 | package uitest.tc;
2 |
3 | import org.testng.annotations.Test;
4 | import uitest.BaseTest;
5 | import org.testng.Assert;
6 | import com.codeborne.selenide.Configuration;
7 |
8 | public class JavaUITest extends BaseTest {
9 | @Test
10 | public void test(){
11 | {$testCodes}
12 | //java code ending
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/template/jwebdriver-mobile.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const cp = require('child_process');
4 | const chai = require("chai");
5 | const should = chai.should();
6 | const JWebDriver = require('jwebdriver');
7 | chai.use(JWebDriver.chaiSupportChainPromise);
8 | const resemble = require('resemblejs-node');
9 | resemble.outputSettings({
10 | errorType: 'flatDifferenceIntensity'
11 | });
12 |
13 | const rootPath = getRootPath();
14 | const appPath = '{$appPath}';
15 | const platformName = '{$platformName}';
16 |
17 | module.exports = function(){
18 |
19 | var driver, testVars;
20 |
21 | before(function(){
22 | var self = this;
23 | driver = self.driver;
24 | testVars = self.testVars;
25 | });
26 |
27 | {$testCodes}
28 | function _(str){
29 | if(typeof str === 'string'){
30 | return str.replace(/\{\{(.+?)\}\}/g, function(all, key){
31 | return testVars[key] || '';
32 | });
33 | }
34 | else{
35 | return str;
36 | }
37 | }
38 |
39 | };
40 |
41 | if(module.parent && /mocha\.js/.test(module.parent.id)){
42 | runThisSpec();
43 | }
44 |
45 | function runThisSpec(){
46 | // read config
47 | let config = require(rootPath + '/config.json');
48 | let webdriverConfig = Object.assign({},config.webdriver);
49 | let host = webdriverConfig.host;
50 | let port = webdriverConfig.port || 4444;
51 | let testVars = config.vars;
52 |
53 | let specName = path.relative(rootPath, __filename).replace(/\\/g,'/').replace(/\.js$/,'');
54 |
55 | let arrDeviceList = getEnvList() || getDeviceList(platformName);
56 | if(arrDeviceList.length ===0 ){
57 | console.log('Search mobile device failed!');
58 | process.exit(1);
59 | }
60 |
61 | arrDeviceList.forEach(function(device){
62 | let caseName = specName + ' : ' + (device.name?device.name+' ['+device.udid+']':device.udid);
63 |
64 | describe(caseName, function(){
65 |
66 | this.timeout(600000);
67 | this.slow(1000);
68 |
69 | before(function(){
70 | let self = this;
71 | let driver = new JWebDriver({
72 | 'host': host,
73 | 'port': port
74 | });
75 | self.driver = driver.session({
76 | 'platformName': platformName,
77 | 'udid': device.udid,
78 | 'app': /^(\/|[a-z]:\\|https?:\/\/)/i.test(appPath) ? appPath : rootPath + '/' + appPath
79 | });
80 | self.testVars = testVars;
81 | let casePath = path.dirname(caseName);
82 | self.screenshotPath = rootPath + '/screenshots/' + casePath;
83 | self.diffbasePath = rootPath + '/diffbase/' + casePath;
84 | self.caseName = caseName.replace(/.*\//g, '').replace(/\s*[:\.\:\-\s]\s*/g, '_');
85 | mkdirs(self.screenshotPath);
86 | mkdirs(self.diffbasePath);
87 | self.stepId = 0;
88 | return self.driver;
89 | });
90 |
91 | module.exports();
92 |
93 | beforeEach(function(){
94 | let self = this;
95 | self.stepId ++;
96 | });
97 |
98 | afterEach(async function(){
99 | let self = this;
100 | let filepath = self.screenshotPath + '/' + self.caseName + '_' + self.stepId;
101 | let driver = self.driver;
102 | await driver.getScreenshot(filepath + '.png');
103 | let json = await driver.source();
104 | fs.writeFileSync(filepath + '.json', json);
105 | });
106 |
107 | after(function(){
108 | return this.driver.close();
109 | });
110 |
111 | });
112 | });
113 | }
114 |
115 | function getRootPath(){
116 | let rootPath = path.resolve(__dirname);
117 | while(rootPath){
118 | if(fs.existsSync(rootPath + '/config.json')){
119 | break;
120 | }
121 | rootPath = rootPath.substring(0, rootPath.lastIndexOf(path.sep));
122 | }
123 | return rootPath;
124 | }
125 |
126 | function mkdirs(dirname){
127 | if(fs.existsSync(dirname)){
128 | return true;
129 | }else{
130 | if(mkdirs(path.dirname(dirname))){
131 | fs.mkdirSync(dirname);
132 | return true;
133 | }
134 | }
135 | }
136 |
137 | function callSpec(name){
138 | try{
139 | require(rootPath + '/' + name)();
140 | }
141 | catch(e){
142 | console.log(e)
143 | process.exit(1);
144 | }
145 | }
146 |
147 | function getEnvList(){
148 | let strEnvList = process.env.devices;
149 | if(strEnvList){
150 | return strEnvList.split(/\s*,\s*/).map(function(udid){
151 | return {udid: udid};
152 | });
153 | }
154 | }
155 |
156 | function getDeviceList(platformName){
157 | let arrDeviceList = [];
158 | let strText, match;
159 | if(platformName === 'Android')
160 | {
161 | // for android
162 | strText = cp.execSync('adb devices').toString();
163 | strText.replace(/(.+?)\s+device\r?\n/g, function(all, deviceName){
164 | arrDeviceList.push({
165 | udid: deviceName
166 | });
167 | });
168 | }
169 | else{
170 | // ios real device
171 | strText = cp.execSync('idevice_id -l').toString();
172 | strText.replace(/(.+)\r?\n/g, function(all, udid){
173 | let deviceName = cp.execSync('idevice_id '+udid).toString();
174 | deviceName = deviceName.replace(/\r?\n/g, '');
175 | arrDeviceList.push({
176 | name: deviceName,
177 | udid: udid
178 | });
179 | });
180 | // ios simulator
181 | strText = cp.execSync('xcrun simctl list devices').toString();
182 | strText.replace(/\r?\n\s*(.+?)\s+\((.+?)\) \(Booted\)/g, function(all, deviceName, udid){
183 | arrDeviceList.push({
184 | name: deviceName,
185 | udid: udid
186 | });
187 | });
188 | }
189 | return arrDeviceList;
190 | }
191 |
--------------------------------------------------------------------------------
/template/jwebdriver.js:
--------------------------------------------------------------------------------
1 | const $ = require('jquery');
2 | const fs = require('fs');
3 | const path = require('path');
4 | const chai = require("chai");
5 | const should = chai.should();
6 | const JWebDriver = require('jwebdriver');
7 | chai.use(JWebDriver.chaiSupportChainPromise);
8 | const resemble = require('resemblejs-node');
9 | resemble.outputSettings({
10 | errorType: 'flatDifferenceIntensity'
11 | });
12 |
13 | const rootPath = getRootPath();
14 |
15 | module.exports = function(){
16 |
17 | let driver, testVars;
18 |
19 | before(function(){
20 | let self = this;
21 | driver = self.driver;
22 | testVars = self.testVars;
23 | });
24 |
25 | {$testCodes}
26 | function _(str){
27 | if(typeof str === 'string'){
28 | return str.replace(/\{\{(.+?)\}\}/g, function(all, key){
29 | return testVars[key] || '';
30 | });
31 | }
32 | else{
33 | return str;
34 | }
35 | }
36 |
37 | };
38 |
39 | if(module.parent && /mocha\.js/.test(module.parent.id)){
40 | runThisSpec();
41 | }
42 |
43 | function runThisSpec(){
44 | // read config
45 | let webdriver = process.env['webdriver'] || '';
46 | let proxy = process.env['wdproxy'] || '';
47 | let config = require(rootPath + '/config.json');
48 | let webdriverConfig = Object.assign({},config.webdriver);
49 | let host = webdriverConfig.host;
50 | let port = webdriverConfig.port || 4444;
51 | let group = webdriverConfig.group || 'default';
52 | let match = webdriver.match(/([^\:]+)(?:\:(\d+))?/);
53 | if(match){
54 | host = match[1] || host;
55 | port = match[2] || port;
56 | }
57 | let testVars = config.vars;
58 | let browsers = webdriverConfig.browsers;
59 | browsers = browsers.replace(/^\s+|\s+$/g, '');
60 | delete webdriverConfig.host;
61 | delete webdriverConfig.port;
62 | delete webdriverConfig.group;
63 | delete webdriverConfig.browsers;
64 |
65 | // read hosts
66 | let hostsPath = rootPath + '/hosts';
67 | let hosts = '';
68 | if(fs.existsSync(hostsPath)){
69 | hosts = fs.readFileSync(hostsPath).toString();
70 | }
71 | let specName = path.relative(rootPath, __filename).replace(/\\/g,'/').replace(/\.js$/,'');
72 |
73 | browsers.split(/\s*,\s*/).forEach(function(browserName){
74 | let caseName = specName + ' : ' + browserName;
75 |
76 | let browserInfo = browserName.split(' ');
77 | browserName = browserInfo[0];
78 | let browserVersion = browserInfo[1];
79 |
80 | describe(caseName, function(){
81 |
82 | this.timeout(600000);
83 | this.slow(1000);
84 |
85 | let driver;
86 | before(function(){
87 | let self = this;
88 | let driver = new JWebDriver({
89 | 'host': host,
90 | 'port': port
91 | });
92 | let sessionConfig = Object.assign({}, webdriverConfig, {
93 | 'group': group,
94 | 'browserName': browserName,
95 | 'version': browserVersion,
96 | 'ie.ensureCleanSession': true,
97 | });
98 | if(proxy){
99 | sessionConfig.proxy = {
100 | 'proxyType': 'manual',
101 | 'httpProxy': proxy,
102 | 'sslProxy': proxy
103 | }
104 | }
105 | else if(hosts){
106 | sessionConfig.hosts = hosts;
107 | }
108 |
109 | try {
110 | self.driver = driver.session(sessionConfig){$sizeCode}.config({
111 | pageloadTimeout: 30000, // page onload timeout
112 | scriptTimeout: 5000, // sync script timeout
113 | asyncScriptTimeout: 10000 // async script timeout
114 | });
115 | } catch (e) {
116 | console.log(e);
117 | }
118 |
119 | self.testVars = testVars;
120 | let casePath = path.dirname(caseName);
121 | if (config.reporter && config.reporter.distDir) {
122 | self.screenshotPath = config.reporter.distDir + '/reports/screenshots/' + casePath;
123 | self.diffbasePath = config.reporter.distDir + '/reports/diffbase/' + casePath;
124 | } else {
125 | self.screenshotPath = rootPath + '/reports/screenshots/' + casePath;
126 | self.diffbasePath = rootPath + '/reports/diffbase/' + casePath;
127 | }
128 | self.caseName = caseName.replace(/.*\//g, '').replace(/\s*[:\.\:\-\s]\s*/g, '_');
129 | mkdirs(self.screenshotPath);
130 | mkdirs(self.diffbasePath);
131 | self.stepId = 0;
132 | return self.driver;
133 | });
134 |
135 | module.exports();
136 |
137 | beforeEach(function(){
138 | let self = this;
139 | self.stepId ++;
140 | if(self.skipAll){
141 | self.skip();
142 | }
143 | });
144 |
145 | afterEach(async function(){
146 | let self = this;
147 | let currentTest = self.currentTest;
148 | let title = currentTest.title;
149 | if(currentTest.state === 'failed' && /^(url|waitBody|switchWindow|switchFrame):/.test(title)){
150 | self.skipAll = true;
151 | }
152 |
153 | if ((config.screenshots && config.screenshots.captureAll && !/^(closeWindow):/.test(title)) || currentTest.state === 'failed') {
154 | const casePath = path.dirname(caseName);
155 | const filepath = `${self.screenshotPath}/${self.caseName}_${self.stepId}`;
156 | const relativeFilePath = `./screenshots/${casePath}/${self.caseName}_${self.stepId}`;
157 | let driver = self.driver;
158 | try{
159 | // catch error when get alert msg
160 | await driver.getScreenshot(filepath + '.png');
161 | let url = await driver.url();
162 | let html = await driver.source();
163 | html = '\n' + html;
164 | fs.writeFileSync(filepath + '.html', html);
165 | let cookies = await driver.cookies();
166 | fs.writeFileSync(filepath + '.cookie', JSON.stringify(cookies));
167 | appendToContext(self, relativeFilePath + '.png');
168 | }
169 | catch(e){}
170 | }
171 | });
172 |
173 | after(function(){
174 | return this.driver.close();
175 | });
176 |
177 | });
178 | });
179 | }
180 |
181 | function getRootPath(){
182 | let rootPath = path.resolve(__dirname);
183 | while(rootPath){
184 | if(fs.existsSync(rootPath + '/config.json')){
185 | break;
186 | }
187 | rootPath = rootPath.substring(0, rootPath.lastIndexOf(path.sep));
188 | }
189 | return rootPath;
190 | }
191 |
192 | function mkdirs(dirname){
193 | if(fs.existsSync(dirname)){
194 | return true;
195 | }else{
196 | if(mkdirs(path.dirname(dirname))){
197 | fs.mkdirSync(dirname);
198 | return true;
199 | }
200 | }
201 | }
202 |
203 | function callSpec(name){
204 | try{
205 | require(rootPath + '/' + name)();
206 | }
207 | catch(e){
208 | console.log(e)
209 | process.exit(1);
210 | }
211 | }
212 |
213 | function isPageError(code){
214 | return code == '' || / jscontent="errorCode" jstcache="\d+"|diagnoseConnectionAndRefresh|dnserror_unavailable_header|id="reportCertificateErrorRetry"|400 Bad Request|403 Forbidden|404 Not Found|500 Internal Server Error|502 Bad Gateway|503 Service Temporarily Unavailable|504 Gateway Time-out/i.test(code);
215 | }
216 |
217 | function appendToContext(mocha, content) {
218 | try {
219 | const test = mocha.currentTest || mocha.test;
220 |
221 | if (!test.context) {
222 | test.context = content;
223 | } else if (Array.isArray(test.context)) {
224 | test.context.push(content);
225 | } else {
226 | test.context = [test.context];
227 | test.context.push(content);
228 | }
229 | } catch (e) {
230 | console.log('error', e);
231 | }
232 | };
233 |
234 | function catchError(error){
235 |
236 | }
237 |
--------------------------------------------------------------------------------
/tool/uirecorder.crx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/uirecorder/72d08a133545b0cb34b424aa835529fd813d673d/tool/uirecorder.crx
--------------------------------------------------------------------------------
/uirecorder.log:
--------------------------------------------------------------------------------
1 | __ ______ ____ __
2 | / / / / _/ / __ \___ _________ _________/ /__ _____
3 | / / / // / / /_/ / _ \/ ___/ __ \/ ___/ __ / _ \/ ___/
4 | / /_/ // / / _, _/ __/ /__/ /_/ / / / /_/ / __/ /
5 | \____/___/ /_/ |_|\___/\___/\____/_/ \__,_/\___/_/ v3.3.0
6 |
7 | Official Site: http://uirecorder.com
8 | ------------------------------------------------------------------
9 |
10 | config.json 文件查找失败,请先初始化!
11 |
--------------------------------------------------------------------------------
/uirecorder.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCxTFJyhnu84tZq
3 | /RuCrmdiUK6AcdhiucN0Sn7b7Bts+cL9ncbp7HWWp7bNkNM4Gue0F2ZPuN1eAYVz
4 | d3zSuv450SvnaFQQjoVmFj4ysHw/78iLMpSMiXY+SA/4mwPHrOK16UctcyIJoTDL
5 | e57HsEVKFFwVRKV36Di2Z7gd6dtCmXCvrQR7Xmos5CblxAWSI7hI4/mKxMrnH0A7
6 | Swq0stJ4IrlJel8GEDrWYMH+roWJeJqlgI0f0PrhBWWw1QpnHub/Yc4N2GYIqqoY
7 | xHY3tm0Q5B1bkvC5tkwsO+Y+sfbjtUfHoQX1e4ksT1W9ZruA1zppfjj1PgcGNj7+
8 | kptfDFi/AgMBAAECggEAakR1smE670cC/5N/lr9UBhCX1zLlYJ85MI2qJcUJ1zKI
9 | lhyoafMps8gIgIPKpfkyYbYYw7XpMPw2cbPvpBsiX6Mo7oWQxW+3My4nz5gKkQP2
10 | rr/9W5LUxZXJxNec12SfaitNV0eH4j+0EHKjA8t6bGFxo+nGR+1veJ0INR3DJtHz
11 | 0gzsKnKd/Ta6z3fVuwKB8wOcYEuIZ5uhJFZWCMyheCcPeKqAkrV15SfP05wDpeko
12 | zvPf0E+IOvC3X55v0MeKQoML+mdKjxGhKrQrVHWMcq0CVn/JWB3U6VCB95t8lDFs
13 | R1fajIFQagBWG4WwQHkklou3+PkB0MChVLZWSNu2MQKBgQDZII99deSgLnF5ATOQ
14 | kaKQ1Jas1CDi0TuzEDaDZ+J8KcY8z/j3Z8n8fTPqtjmJecH9IL0sOtAwqrf1+Nlx
15 | GmlX/5T4fBDeO+WJp5riGeJUtdM4u9WgLZK+gWMegq2ysnGePWizhW2BBRg5xGUk
16 | QBw1ZR2e01iSz+GXKAgbq0+diQKBgQDRCk3ZxGWdmamCAvgVupSbqiZEFC5zNMKn
17 | jnGpoamn96QDrLKlkaGDIf8T4hsyO32B5EYnLxt3sgmQlsd4zBdNb7eSd0q/NnvB
18 | Unbnf9IzAmdcylJc3zmOGmKQWdo6xU7KXJ5hmCEvryNz7dL5pCJNjGa2N4AyzRvE
19 | VCrsxz46BwKBgGTZYsx7Pb3I1JvHmxPDEScEFxgfT0cKuBfrp+ZREjlpjdIhJxqC
20 | 8qZ74Olbyk24aAoScstgZeK06M0u0JBgHB3rcF4aAhu25l6RorbyHtYJvhnT5N2J
21 | TWd+4XMCb3tYtr0w+LipeLs8iowKVJAJ1xBV7vQeZj2KoNV8mod/gnNBAoGAM1Cs
22 | O8ESkNWf3uKLtAnRYUUrj5rErFNPVYKKNHITC8Cm6qACWtKdK2u1ClR/CJ3B+Zjn
23 | /8Z4n7F815mr7eNr9P5vuey+1KGzT4nG1p1yJEN6zDR+c334ywF/IKBuCe9VoCeM
24 | WbjWrLX5pgPDvrSkFxVYQXLubYocPt3Ki8V9aRECgYAc9pbtUAy2M+iHdsT65h9O
25 | q0v25qVq6R7pZCFOjAPFVcUCyN5nqEw9eR5kjMfKd2z3IWpSn2+6gzexnH7bLzFz
26 | 4TleaeyL31BrhkJo2OpJCq1yQa4zhEnn1nlaxqar726NaRl4wF69cv63AdI2eg5x
27 | PWT96evUQ38+4D63THSVew==
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------