├── LICENSE ├── README.md ├── devtools.html ├── devtools.js ├── icon128.png ├── icon16.png ├── icon48.png ├── inject.js ├── manifest.json ├── panel.html ├── panel.js ├── prism.css └── prism.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 hanthomas 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 | # Protractor Recorder 2 | Chrome extension to record Protractor E2E test scripts 3 | 4 | ## How to Install 5 | 1. Download ZIP and unzip to folder on your local machine 6 | 2. Start Chrome browser and navigate to *chrome://extensions* 7 | 3. Click on **Load unpacked extension** and select location of unzipped folder 8 | 4. Enable the Protractor Recorder extension 9 | 10 | ## How to Use 11 | 1. Open your web site in Chrome 12 | 2. Launch **Developer Tools** by pressing F12 13 | 3. Select **Protractor** tab 14 | 4. Interact with your web site. Each action is recorded in the Protractor Recorder console. 15 | 5. When you're done, click **Copy to Clipboard** to copy the recorded script. 16 | 17 | ## Should I disable automatic synchronization? 18 | Check this box if the recorded scripts are timing out when run in Protractor. Protractor has known issues with AngularJS applications that use **$timeout**. See Protractor issues [#169](https://github.com/angular/protractor/issues/169) and [#2950](https://github.com/angular/protractor/issues/2950). 19 | -------------------------------------------------------------------------------- /devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /devtools.js: -------------------------------------------------------------------------------- 1 | chrome.devtools.panels.create('Protractor', 'icon48.png', 'panel.html', function (panel) { 2 | var xhr = new XMLHttpRequest(); 3 | 4 | xhr.open('GET', chrome.runtime.getURL('/inject.js'), false); 5 | xhr.send(); 6 | 7 | var script = xhr.responseText; 8 | 9 | if (script) { 10 | chrome.devtools.inspectedWindow.eval(script.toString()); 11 | 12 | var interval; 13 | 14 | panel.onShown.addListener(function (o) { 15 | interval = window.setInterval(function () { 16 | chrome.devtools.inspectedWindow.eval('(function () { return window.protractor.logs; })()', function () { 17 | if (!arguments[1]) 18 | o.update(arguments[0]); 19 | else 20 | chrome.devtools.inspectedWindow.eval(script.toString()); 21 | }); 22 | }, 300); 23 | }); 24 | 25 | panel.onHidden.addListener(function () { 26 | if (interval) 27 | window.clearInterval(interval); 28 | }); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanthomas/protractor-recorder/308d6c24f3aada915836ba917ee1342a6c65c0ac/icon128.png -------------------------------------------------------------------------------- /icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanthomas/protractor-recorder/308d6c24f3aada915836ba917ee1342a6c65c0ac/icon16.png -------------------------------------------------------------------------------- /icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanthomas/protractor-recorder/308d6c24f3aada915836ba917ee1342a6c65c0ac/icon48.png -------------------------------------------------------------------------------- /inject.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | window.protractor = {}; 3 | window.protractor.logs = []; 4 | window.protractor.ignoreSynchronization = false; 5 | 6 | Event.prototype.stopPropagation = function () { }; 7 | 8 | var url = ''; 9 | var time = null; 10 | var mouse = []; 11 | 12 | document.addEventListener('click', function (e) { 13 | if (e.target.tagName.toLowerCase() != 'canvas') 14 | log('element(by.css(\'' + selector(e.target).replace(/\\\"/g, '\\\\\\"') + '\'))' + '.click();'); 15 | }); 16 | 17 | document.addEventListener('mousedown', function (e) { 18 | if (e.target.tagName.toLowerCase() == 'canvas') 19 | mouse = [e]; 20 | }); 21 | 22 | document.addEventListener('mousemove', function (e) { 23 | if (e.target.tagName.toLowerCase() == 'canvas' && mouse && mouse.length > 0) 24 | mouse.push(e); 25 | }); 26 | 27 | document.addEventListener('mouseup', function (e) { 28 | if (e.target.tagName.toLowerCase() == 'canvas' && mouse && mouse.length > 0 && mouse[0].target == e.target) { 29 | if (mouse.reduce(function (a, b) { return a.clientX - b.clientX; }, mouse[0]) <= 1 && mouse.reduce(function (a, b) { return a.clientY - b.clientY; }, mouse[0]) <= 1) 30 | log('browser.driver.actions().mouseMove(element(by.css(\'' + selector(mouse[0].target).replace(/\\\"/g, '\\\\\\"') + '\')), {x: ' + mouse[0].clientX.toString() + ', y:' + mouse[0].clientY.toString() + '}).click().perform();'); 31 | else 32 | log('browser.driver.actions().mouseMove(element(by.css(\'' + selector(mouse[0].target).replace(/\\\"/g, '\\\\\\"') + '\')), {x: ' + mouse[0].clientX.toString() + ', y:' + mouse[0].clientY.toString() + '}).mouseDown()' + mouse.reduce(function (a, b, i) { return i > 0 ? a + '.mouseMove({x: ' + (b.clientX - mouse[i - 1].clientX).toString() + ', y:' + (b.clientY - mouse[i - 1].clientY).toString() + '})' : ''; }, '') + '.mouseUp().perform();'); 33 | } 34 | }); 35 | 36 | document.addEventListener('touchstart', function (e) { 37 | if (e.target.tagName.toLowerCase() == 'canvas' && e.targetTouches && e.targetTouches.length > 0) 38 | mouse = [{ target: e.target, clientX: Math.floor(e.targetTouches[0].clientX), clientY: Math.floor(e.targetTouches[0].clientY) }]; 39 | }); 40 | 41 | document.addEventListener('touchmove', function (e) { 42 | if (e.target.tagName.toLowerCase() == 'canvas' && e.targetTouches && e.targetTouches.length > 0 && mouse && mouse.length > 0) 43 | mouse.push({ target: e.target, clientX: Math.floor(e.targetTouches[0].clientX), clientY: Math.floor(e.targetTouches[0].clientY) }); 44 | }); 45 | 46 | document.addEventListener('touchend', function (e) { 47 | if (e.target.tagName.toLowerCase() == 'canvas' && mouse && mouse.length > 0 && mouse[0].target == e.target) { 48 | if (mouse.reduce(function (a, b) { return a.clientX - b.clientX; }, mouse[0]) <= 1 && mouse.reduce(function (a, b) { return a.clientY - b.clientY; }, mouse[0]) <= 1) 49 | log('browser.driver.actions().mouseMove(element(by.css(\'' + selector(mouse[0].target).replace(/\\\"/g, '\\\\\\"') + '\')), {x: ' + mouse[0].clientX.toString() + ', y:' + mouse[0].clientY.toString() + '}).click().perform();'); 50 | else 51 | log('browser.driver.actions().mouseMove(element(by.css(\'' + selector(mouse[0].target).replace(/\\\"/g, '\\\\\\"') + '\')), {x: ' + mouse[0].clientX.toString() + ', y:' + mouse[0].clientY.toString() + '}).mouseDown()' + mouse.reduce(function (a, b, i) { return i > 0 ? a + '.mouseMove({x: ' + (b.clientX - mouse[i - 1].clientX).toString() + ', y:' + (b.clientY - mouse[i - 1].clientY).toString() + '})' : ''; }, '') + '.mouseUp().perform();'); 52 | } 53 | }); 54 | 55 | document.addEventListener('change', function (e) { 56 | if (e.target.tagName.toLowerCase() == 'input' && ['text', 'password', 'number'].indexOf(e.target.getAttribute('type').toLowerCase()) != -1) 57 | log('element(by.css(\'' + selector(e.target).replace(/\\\"/g, '\\\\\\"') + '\'))' + '.clear().sendKeys(\'' + e.target.value + '\');'); 58 | else if (e.target.tagName.toLowerCase() == 'textarea') 59 | log('element(by.css(\'' + selector(e.target).replace(/\\\"/g, '\\\\\\"') + '\'))' + '.clear().sendKeys(\'' + e.target.value + '\');'); 60 | }); 61 | 62 | var selector = function (target) { 63 | var query = ''; 64 | 65 | if (target == document) 66 | query = 'body'; 67 | else { 68 | var attr = ['ng-model', 'ng-href', 'name', 'aria-label'].reduce(function (a, b) { return a || (target.getAttribute(b) ? b : null); }, null); 69 | if (attr) 70 | query = target.tagName.toLowerCase() + '[' + attr + '="' + target.getAttribute(attr).replace(/\\/g, '\\\\').replace(/\'/g, '\\\'').replace(/\"/g, '\\"').replace(/\0/g, '\\0') + '"]'; 71 | else 72 | query = target.tagName.toLowerCase(); 73 | 74 | var nodes = target.parentNode.querySelectorAll(query); 75 | if (nodes && nodes.length > 1) 76 | query += ':nth-of-type(' + (Array.prototype.slice.call(nodes).indexOf(target) + 1).toString() + ')'; 77 | 78 | query = query.replace(/\s/g, ' '); 79 | } 80 | 81 | if (document.querySelectorAll(query).length > 1 && target.parentNode) 82 | query = selector(target.parentNode) + '>' + query; 83 | 84 | return query; 85 | }; 86 | 87 | var log = function (action) { 88 | if (window.protractor.logs.length == 0) 89 | window.protractor.logs.push('browser.driver.manage().window().setSize(' + window.outerWidth + ', ' + window.outerHeight + ');'); 90 | 91 | if (window.protractor.ignoreSynchronization && time) 92 | window.protractor.logs.push('browser.sleep(' + (new Date() - time).toString() + ');'); 93 | 94 | time = new Date(); 95 | 96 | if (!url || url != window.location.hash) 97 | window.protractor.logs.push('// URL: ' + window.location.hash); 98 | 99 | url = window.location.hash; 100 | 101 | window.protractor.logs.push(action); 102 | }; 103 | 104 | return window.protractor.logs; 105 | })(); 106 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Protractor Recorder", 3 | "version": "1.0.0", 4 | "description": "Records Protractor E2E test scripts", 5 | "devtools_page": "devtools.html", 6 | "icons": { 7 | "16": "icon16.png", 8 | "48": "icon48.png", 9 | "128": "icon128.png" 10 | }, 11 | "manifest_version": 2 12 | } -------------------------------------------------------------------------------- /panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 51 | 52 | 53 | 54 |
68 |
69 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/panel.js:
--------------------------------------------------------------------------------
1 | document.addEventListener('DOMContentLoaded', function () {
2 | document.querySelector('#copy').addEventListener('click', function () {
3 | var code = document.createRange();
4 | code.selectNode(document.querySelector('code'));
5 | window.getSelection().addRange(code);
6 |
7 | try {
8 | if (document.execCommand('copy'))
9 | document.querySelector('#message').innerText = 'Copied to the clipboard on ' + (new Date()).toLocaleString();
10 | else
11 | document.querySelector('#message').innerText = 'Problem copying test script to the clipboard.';
12 | }
13 | catch (e) {
14 | document.querySelector('#message').innerText = 'Problem copying test script to the clipboard.';
15 | }
16 |
17 | window.getSelection().removeAllRanges();
18 | }, false);
19 |
20 | document.querySelector('#clear').addEventListener('click', function () {
21 | chrome.devtools.inspectedWindow.eval('(function () { window.protractor.logs = []; })()');
22 | update([]);
23 | document.querySelector('#message').innerText = '';
24 | }, false);
25 |
26 | document.querySelector('#nosync').addEventListener('change', function () {
27 | if (document.querySelector('#nosync').checked)
28 | chrome.devtools.inspectedWindow.eval('(function () { window.protractor.ignoreSynchronization = true; window.protractor.logs.push(\'browser.ignoreSynchronization = true;\'); })()');
29 | else
30 | chrome.devtools.inspectedWindow.eval('(function () { window.protractor.ignoreSynchronization = false; window.protractor.logs.push(\'browser.ignoreSynchronization = false; })()');
31 | }, false);
32 |
33 | document.querySelector('code').addEventListener('focus', function () {
34 | editing = true;
35 | }, false);
36 |
37 | document.querySelector('code').addEventListener('blur', function () {
38 | chrome.devtools.inspectedWindow.eval('(function () { window.protractor.logs = ' + JSON.stringify(document.querySelector('code').innerText.split('\n')) + '; })()');
39 | editing = false;
40 | }, false);
41 | }, false);
42 |
43 | var editing = false;
44 |
45 | function update(logs) {
46 | if (!editing) {
47 | var code = document.querySelector('code');
48 | if (code)
49 | code.innerText = logs.join('\n');
50 |
51 | Prism.highlightAll();
52 |
53 | if (logs && logs.length > 0) {
54 | document.querySelector('#copy').style.display = '';
55 | document.querySelector('#clear').style.display = '';
56 | }
57 | else {
58 | document.querySelector('#copy').style.display = 'none';
59 | document.querySelector('#clear').style.display = 'none';
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/prism.css:
--------------------------------------------------------------------------------
1 | /* http://prismjs.com/download.html?themes=prism-okaidia&languages=markup+css+clike+javascript */
2 | /**
3 | * okaidia theme for JavaScript, CSS and HTML
4 | * Loosely based on Monokai textmate theme by http://www.monokai.nl/
5 | * @author ocodia
6 | */
7 |
8 | code[class*="language-"],
9 | pre[class*="language-"] {
10 | color: #f8f8f2;
11 | background: none;
12 | text-shadow: 0 1px rgba(0, 0, 0, 0.3);
13 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
14 | text-align: left;
15 | white-space: pre;
16 | word-spacing: normal;
17 | word-break: normal;
18 | word-wrap: normal;
19 | line-height: 1.5;
20 |
21 | -moz-tab-size: 4;
22 | -o-tab-size: 4;
23 | tab-size: 4;
24 |
25 | -webkit-hyphens: none;
26 | -moz-hyphens: none;
27 | -ms-hyphens: none;
28 | hyphens: none;
29 | }
30 |
31 | /* Code blocks */
32 | pre[class*="language-"] {
33 | padding: 1em;
34 | margin: .5em 0;
35 | overflow: auto;
36 | border-radius: 0.3em;
37 | }
38 |
39 | :not(pre) > code[class*="language-"],
40 | pre[class*="language-"] {
41 | background: #272822;
42 | }
43 |
44 | /* Inline code */
45 | :not(pre) > code[class*="language-"] {
46 | padding: .1em;
47 | border-radius: .3em;
48 | white-space: normal;
49 | }
50 |
51 | .token.comment,
52 | .token.prolog,
53 | .token.doctype,
54 | .token.cdata {
55 | color: slategray;
56 | }
57 |
58 | .token.punctuation {
59 | color: #f8f8f2;
60 | }
61 |
62 | .namespace {
63 | opacity: .7;
64 | }
65 |
66 | .token.property,
67 | .token.tag,
68 | .token.constant,
69 | .token.symbol,
70 | .token.deleted {
71 | color: #f92672;
72 | }
73 |
74 | .token.boolean,
75 | .token.number {
76 | color: #ae81ff;
77 | }
78 |
79 | .token.selector,
80 | .token.attr-name,
81 | .token.string,
82 | .token.char,
83 | .token.builtin,
84 | .token.inserted {
85 | color: #a6e22e;
86 | }
87 |
88 | .token.operator,
89 | .token.entity,
90 | .token.url,
91 | .language-css .token.string,
92 | .style .token.string,
93 | .token.variable {
94 | color: #f8f8f2;
95 | }
96 |
97 | .token.atrule,
98 | .token.attr-value,
99 | .token.function {
100 | color: #e6db74;
101 | }
102 |
103 | .token.keyword {
104 | color: #66d9ef;
105 | }
106 |
107 | .token.regex,
108 | .token.important {
109 | color: #fd971f;
110 | }
111 |
112 | .token.important,
113 | .token.bold {
114 | font-weight: bold;
115 | }
116 | .token.italic {
117 | font-style: italic;
118 | }
119 |
120 | .token.entity {
121 | cursor: help;
122 | }
123 |
124 |
--------------------------------------------------------------------------------
/prism.js:
--------------------------------------------------------------------------------
1 | /* http://prismjs.com/download.html?themes=prism-okaidia&languages=markup+css+clike+javascript */
2 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(){var e=/\blang(?:uage)?-(\w+)\b/i,t=0,n=_self.Prism={util:{encode:function(e){return e instanceof a?new a(e.type,n.util.encode(e.content),e.alias):"Array"===n.util.type(e)?e.map(n.util.encode):e.replace(/&/g,"&").replace(/e.length)break e;if(!(m instanceof a)){u.lastIndex=0;var v=u.exec(m),y=1;if(!v&&h&&p!=r.length-1){var b=r[p+1].matchedStr||r[p+1],k=m+b;if(p