├── .gitignore ├── LICENSE ├── README.md ├── app.js ├── package-lock.json ├── package.json ├── static ├── cube.png ├── fork.png ├── icon.svg ├── prism.css ├── prism.js ├── style.css ├── upload.css ├── upload.html └── upload.js └── views ├── Ajax.pug ├── Alerts.pug ├── Animation.pug ├── AutoWait.pug ├── ClassAttr.pug ├── Click.pug ├── ClientDelay.pug ├── DisabledInput.pug ├── DynamicID.pug ├── DynamicTable.pug ├── HiddenLayers.pug ├── Home.pug ├── LoadDelay.pug ├── MouseOver.pug ├── Nbsp.pug ├── Overlapped.pug ├── ProgressBar.pug ├── Resources.pug ├── SampleApp.pug ├── Scrollbars.pug ├── ShadowDom.pug ├── TextInput.pug ├── Upload.pug ├── VerifyText.pug ├── Visibility.pug ├── footer.html ├── head.pug ├── navbar.pug └── text.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Inflectra Corporation. All rights reserved. 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "[]" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright 2017, The TensorFlow Authors. 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ui-test-automation-playground 2 | 3 | The purpose of this website is to provide a platform for sharpening UI test automation skills. Use it to practice with your test automation tool. Use it to learn test automation techniques. 4 | 5 | ## Live Version 6 | 7 | Latest version of this website is always available at [uitestingplayground.com](http://uitestingplayground.com). 8 | 9 | ## Prerequisites 10 | - [Node.js](https://nodejs.org) 11 | - [npm](https://www.npmjs.com/get-npm) 12 | 13 | ## Usage 14 | 15 | 1. Clone the repository 16 | 2. In the package folder run 17 | ```bash 18 | npm install 19 | ``` 20 | 3. Launch with 21 | ```bash 22 | node app.js 23 | ``` 24 | 4. In a browser navigate to 25 | ``` 26 | http://localhost:3000 27 | ``` 28 | 29 | ## Software Stack 30 | - [Node.js](https://github.com/nodejs/node) 31 | - [Express](https://github.com/expressjs/express/) 32 | - [Pug](https://github.com/pugjs/pug) 33 | - [Bootstrap](https://github.com/twbs/bootstrap) 34 | - [jQuery](https://github.com/jquery/jquery) 35 | 36 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const app = express() 3 | const shuffle = require('shuffle-array') 4 | 5 | function guid() { 6 | function s4() { 7 | return Math.floor((1 + Math.random()) * 0x10000) 8 | .toString(16) 9 | .substring(1); 10 | } 11 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + 12 | s4() + '-' + s4() + s4() + s4(); 13 | } 14 | 15 | // Routing and launch 16 | app.set('view engine', 'pug') 17 | 18 | app.use('/static', express.static('static')) 19 | 20 | app.get('/', function (req, res) { 21 | res.render('Home', { 22 | title: 'UI Test Automation Playground', 23 | homeActive: true 24 | }) 25 | }) 26 | 27 | app.get('/home', function (req, res) { 28 | res.render('Home', { 29 | title: 'UI Test Automation Playground', 30 | homeActive: true 31 | }) 32 | }) 33 | 34 | app.get('/resources', function (req, res) { 35 | res.render('Resources', { 36 | title: 'Resources', 37 | resourceActive: true 38 | }) 39 | }) 40 | 41 | app.get('/dynamicid', function (req, res) { 42 | res.render('DynamicID', { 43 | title: 'Dynamic ID', 44 | buttonId: guid() 45 | }) 46 | }) 47 | 48 | app.get('/loaddelay', function (req, res) { 49 | function render() { 50 | res.render('LoadDelay', { 51 | title: 'Load Delays' 52 | }) 53 | } 54 | setTimeout(render, 5000); 55 | }) 56 | 57 | app.get('/click', function (req, res) { 58 | res.render('Click', { 59 | title: 'Click' 60 | }) 61 | }) 62 | 63 | app.get('/textinput', function (req, res) { 64 | res.render('TextInput', { 65 | title: 'Text Input' 66 | }) 67 | }) 68 | 69 | app.get('/ajax', function (req, res) { 70 | res.render('Ajax', { 71 | title: 'AJAX Data' 72 | }) 73 | }) 74 | 75 | app.get('/ajaxdata', function (req, res) { 76 | function render() { 77 | res.send('Data loaded with AJAX get request.') 78 | } 79 | setTimeout(render, 15000); 80 | }) 81 | 82 | app.get('/clientdelay', function (req, res) { 83 | res.render('ClientDelay', { 84 | title: 'Client Side Delay' 85 | }) 86 | }) 87 | 88 | app.get('/progressbar', function (req, res) { 89 | res.render('ProgressBar', { 90 | title: 'Progress Bar' 91 | }) 92 | }) 93 | 94 | app.get('/classattr', function (req, res) { 95 | 96 | var collection = ['btn-primary', 'btn-success', 'btn-warning']; 97 | shuffle(collection); 98 | 99 | res.render('ClassAttr', { 100 | title: 'Class Attribute', 101 | button1Class: collection[0], 102 | button2Class: collection[1], 103 | button3Class: collection[2] 104 | }) 105 | }) 106 | 107 | app.get('/verifytext', function (req, res) { 108 | res.render('VerifyText', { 109 | title: 'Verify Text' 110 | }) 111 | }) 112 | 113 | app.get('/hiddenlayers', function (req, res) { 114 | res.render('HiddenLayers', { 115 | title: 'Hidden Layers' 116 | }) 117 | }) 118 | 119 | app.get('/scrollbars', function (req, res) { 120 | res.render('Scrollbars', { 121 | title: 'Scrollbars' 122 | }) 123 | }) 124 | 125 | app.get('/visibility', function (req, res) { 126 | res.render('Visibility', { 127 | title: 'Visibility' 128 | }) 129 | }) 130 | 131 | app.get('/sampleapp', function (req, res) { 132 | res.render('SampleApp', { 133 | title: 'Sample App' 134 | }) 135 | }) 136 | 137 | app.get('/mouseover', function (req, res) { 138 | res.render('MouseOver', { 139 | title: 'Mouse Over' 140 | }) 141 | }) 142 | 143 | app.get('/nbsp', function (req, res) { 144 | res.render('Nbsp', { 145 | title: 'Non-Breaking Space' 146 | }) 147 | }) 148 | 149 | app.get('/overlapped', function (req, res) { 150 | res.render('Overlapped', { 151 | title: 'Overlapped Element' 152 | }) 153 | }) 154 | 155 | app.get('/shadowdom', function (req, res) { 156 | res.render('ShadowDom', { 157 | title: 'Shadow DOM' 158 | }) 159 | }) 160 | 161 | app.get('/alerts', function (req, res) { 162 | res.render('Alerts', { 163 | title: 'Alerts' 164 | }) 165 | }) 166 | 167 | app.get('/upload', function (req, res) { 168 | res.render('Upload', { 169 | title: 'File Upload' 170 | }) 171 | }) 172 | 173 | app.get('/animation', function (req, res) { 174 | res.render('Animation', { 175 | title: 'Animated Button' 176 | }) 177 | }) 178 | 179 | app.get('/disabledinput', function (req, res) { 180 | res.render('DisabledInput', { 181 | title: 'Disabled Input' 182 | }) 183 | }) 184 | 185 | app.get('/autowait', function (req, res) { 186 | res.render('AutoWait', { 187 | title: 'Auto Wait' 188 | }) 189 | }) 190 | 191 | app.get('/dynamictable', function (req, res) { 192 | 193 | function genmetric(m) 194 | { 195 | switch(m) 196 | { 197 | case "CPU": 198 | return Math.floor(Math.random()*100)/10.0 + "%"; 199 | case "Memory": 200 | return Math.floor(Math.random()*1000)/10.0 + " MB"; 201 | case "Disk": 202 | return Math.floor(Math.random()*10)/10.0 + " MB/s"; 203 | case "Network": 204 | return Math.floor(Math.random()*100)/10.0 + " Mbps"; 205 | default: 206 | } 207 | return ""; 208 | } 209 | 210 | var tasks = ["System", "Firefox", "Chrome", "Internet Explorer"]; 211 | shuffle(tasks); 212 | 213 | var metrics = ["CPU", "Memory", "Disk", "Network"]; 214 | shuffle(metrics); 215 | 216 | var rows = []; 217 | for(var t in tasks) 218 | { 219 | var row = [tasks[t]]; 220 | for(var m in metrics) 221 | { 222 | row.push(genmetric(metrics[m])); 223 | } 224 | rows.push(row); 225 | } 226 | 227 | var columns = ["Name"]; 228 | columns.push.apply(columns, metrics); 229 | 230 | var cpuColumnIndex = columns.indexOf("CPU"); 231 | var chromeRowIndex = tasks.indexOf("Chrome"); 232 | var chromeCPU = rows[chromeRowIndex][cpuColumnIndex]; 233 | 234 | var table = { 235 | name: "Tasks", 236 | desc: "Task Manager", 237 | columns: columns, 238 | rows: rows, 239 | chromeCPU: chromeCPU 240 | }; 241 | 242 | res.render('DynamicTable', { 243 | title: 'Dynamic Table', table: table 244 | }) 245 | }) 246 | 247 | const port = process.env.PORT || 3000; 248 | app.listen(port, () => console.log('UI Test Automation Playground is listening on port ' + port)) -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui-test-automation-playground", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/babel-types": { 8 | "version": "7.0.4", 9 | "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.4.tgz", 10 | "integrity": "sha512-WiZhq3SVJHFRgRYLXvpf65XnV6ipVHhnNaNvE8yCimejrGglkg38kEj0JcizqwSHxmPSjcTlig/6JouxLGEhGw==" 11 | }, 12 | "@types/babylon": { 13 | "version": "6.16.3", 14 | "resolved": "https://registry.npmjs.org/@types/babylon/-/babylon-6.16.3.tgz", 15 | "integrity": "sha512-lyJ8sW1PbY3uwuvpOBZ9zMYKshMnQpXmeDHh8dj9j2nJm/xrW0FgB5gLSYOArj5X0IfaXnmhFoJnhS4KbqIMug==", 16 | "requires": { 17 | "@types/babel-types": "*" 18 | } 19 | }, 20 | "accepts": { 21 | "version": "1.3.5", 22 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", 23 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", 24 | "requires": { 25 | "mime-types": "~2.1.18", 26 | "negotiator": "0.6.1" 27 | } 28 | }, 29 | "acorn": { 30 | "version": "3.3.0", 31 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", 32 | "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" 33 | }, 34 | "acorn-globals": { 35 | "version": "3.1.0", 36 | "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", 37 | "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", 38 | "requires": { 39 | "acorn": "^4.0.4" 40 | }, 41 | "dependencies": { 42 | "acorn": { 43 | "version": "4.0.13", 44 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", 45 | "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" 46 | } 47 | } 48 | }, 49 | "align-text": { 50 | "version": "0.1.4", 51 | "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", 52 | "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", 53 | "requires": { 54 | "kind-of": "^3.0.2", 55 | "longest": "^1.0.1", 56 | "repeat-string": "^1.5.2" 57 | } 58 | }, 59 | "array-flatten": { 60 | "version": "1.1.1", 61 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 62 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 63 | }, 64 | "asap": { 65 | "version": "2.0.6", 66 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", 67 | "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" 68 | }, 69 | "babel-runtime": { 70 | "version": "6.26.0", 71 | "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", 72 | "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", 73 | "requires": { 74 | "core-js": "^2.4.0", 75 | "regenerator-runtime": "^0.11.0" 76 | } 77 | }, 78 | "babel-types": { 79 | "version": "6.26.0", 80 | "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", 81 | "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", 82 | "requires": { 83 | "babel-runtime": "^6.26.0", 84 | "esutils": "^2.0.2", 85 | "lodash": "^4.17.4", 86 | "to-fast-properties": "^1.0.3" 87 | } 88 | }, 89 | "babylon": { 90 | "version": "6.18.0", 91 | "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", 92 | "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" 93 | }, 94 | "body-parser": { 95 | "version": "1.18.2", 96 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", 97 | "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", 98 | "requires": { 99 | "bytes": "3.0.0", 100 | "content-type": "~1.0.4", 101 | "debug": "2.6.9", 102 | "depd": "~1.1.1", 103 | "http-errors": "~1.6.2", 104 | "iconv-lite": "0.4.19", 105 | "on-finished": "~2.3.0", 106 | "qs": "6.5.1", 107 | "raw-body": "2.3.2", 108 | "type-is": "~1.6.15" 109 | } 110 | }, 111 | "bytes": { 112 | "version": "3.0.0", 113 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 114 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 115 | }, 116 | "camelcase": { 117 | "version": "1.2.1", 118 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", 119 | "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" 120 | }, 121 | "center-align": { 122 | "version": "0.1.3", 123 | "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", 124 | "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", 125 | "requires": { 126 | "align-text": "^0.1.3", 127 | "lazy-cache": "^1.0.3" 128 | } 129 | }, 130 | "character-parser": { 131 | "version": "2.2.0", 132 | "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", 133 | "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=", 134 | "requires": { 135 | "is-regex": "^1.0.3" 136 | } 137 | }, 138 | "clean-css": { 139 | "version": "4.1.11", 140 | "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", 141 | "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", 142 | "requires": { 143 | "source-map": "0.5.x" 144 | } 145 | }, 146 | "cliui": { 147 | "version": "2.1.0", 148 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", 149 | "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", 150 | "requires": { 151 | "center-align": "^0.1.1", 152 | "right-align": "^0.1.1", 153 | "wordwrap": "0.0.2" 154 | } 155 | }, 156 | "constantinople": { 157 | "version": "3.1.2", 158 | "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz", 159 | "integrity": "sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==", 160 | "requires": { 161 | "@types/babel-types": "^7.0.0", 162 | "@types/babylon": "^6.16.2", 163 | "babel-types": "^6.26.0", 164 | "babylon": "^6.18.0" 165 | } 166 | }, 167 | "content-disposition": { 168 | "version": "0.5.2", 169 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 170 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 171 | }, 172 | "content-type": { 173 | "version": "1.0.4", 174 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 175 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 176 | }, 177 | "cookie": { 178 | "version": "0.3.1", 179 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 180 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 181 | }, 182 | "cookie-signature": { 183 | "version": "1.0.6", 184 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 185 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 186 | }, 187 | "core-js": { 188 | "version": "2.5.7", 189 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", 190 | "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" 191 | }, 192 | "debug": { 193 | "version": "2.6.9", 194 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 195 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 196 | "requires": { 197 | "ms": "2.0.0" 198 | } 199 | }, 200 | "decamelize": { 201 | "version": "1.2.0", 202 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", 203 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" 204 | }, 205 | "depd": { 206 | "version": "1.1.2", 207 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 208 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 209 | }, 210 | "destroy": { 211 | "version": "1.0.4", 212 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 213 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 214 | }, 215 | "doctypes": { 216 | "version": "1.1.0", 217 | "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", 218 | "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" 219 | }, 220 | "ee-first": { 221 | "version": "1.1.1", 222 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 223 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 224 | }, 225 | "encodeurl": { 226 | "version": "1.0.2", 227 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 228 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 229 | }, 230 | "escape-html": { 231 | "version": "1.0.3", 232 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 233 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 234 | }, 235 | "esutils": { 236 | "version": "2.0.2", 237 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", 238 | "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" 239 | }, 240 | "etag": { 241 | "version": "1.8.1", 242 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 243 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 244 | }, 245 | "express": { 246 | "version": "4.16.3", 247 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", 248 | "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", 249 | "requires": { 250 | "accepts": "~1.3.5", 251 | "array-flatten": "1.1.1", 252 | "body-parser": "1.18.2", 253 | "content-disposition": "0.5.2", 254 | "content-type": "~1.0.4", 255 | "cookie": "0.3.1", 256 | "cookie-signature": "1.0.6", 257 | "debug": "2.6.9", 258 | "depd": "~1.1.2", 259 | "encodeurl": "~1.0.2", 260 | "escape-html": "~1.0.3", 261 | "etag": "~1.8.1", 262 | "finalhandler": "1.1.1", 263 | "fresh": "0.5.2", 264 | "merge-descriptors": "1.0.1", 265 | "methods": "~1.1.2", 266 | "on-finished": "~2.3.0", 267 | "parseurl": "~1.3.2", 268 | "path-to-regexp": "0.1.7", 269 | "proxy-addr": "~2.0.3", 270 | "qs": "6.5.1", 271 | "range-parser": "~1.2.0", 272 | "safe-buffer": "5.1.1", 273 | "send": "0.16.2", 274 | "serve-static": "1.13.2", 275 | "setprototypeof": "1.1.0", 276 | "statuses": "~1.4.0", 277 | "type-is": "~1.6.16", 278 | "utils-merge": "1.0.1", 279 | "vary": "~1.1.2" 280 | } 281 | }, 282 | "finalhandler": { 283 | "version": "1.1.1", 284 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", 285 | "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", 286 | "requires": { 287 | "debug": "2.6.9", 288 | "encodeurl": "~1.0.2", 289 | "escape-html": "~1.0.3", 290 | "on-finished": "~2.3.0", 291 | "parseurl": "~1.3.2", 292 | "statuses": "~1.4.0", 293 | "unpipe": "~1.0.0" 294 | } 295 | }, 296 | "forwarded": { 297 | "version": "0.1.2", 298 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 299 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 300 | }, 301 | "fresh": { 302 | "version": "0.5.2", 303 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 304 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 305 | }, 306 | "function-bind": { 307 | "version": "1.1.1", 308 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 309 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 310 | }, 311 | "has": { 312 | "version": "1.0.3", 313 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 314 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 315 | "requires": { 316 | "function-bind": "^1.1.1" 317 | } 318 | }, 319 | "http-errors": { 320 | "version": "1.6.3", 321 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", 322 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", 323 | "requires": { 324 | "depd": "~1.1.2", 325 | "inherits": "2.0.3", 326 | "setprototypeof": "1.1.0", 327 | "statuses": ">= 1.4.0 < 2" 328 | } 329 | }, 330 | "iconv-lite": { 331 | "version": "0.4.19", 332 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", 333 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" 334 | }, 335 | "inherits": { 336 | "version": "2.0.3", 337 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 338 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 339 | }, 340 | "ipaddr.js": { 341 | "version": "1.6.0", 342 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", 343 | "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=" 344 | }, 345 | "is-buffer": { 346 | "version": "1.1.6", 347 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 348 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" 349 | }, 350 | "is-expression": { 351 | "version": "3.0.0", 352 | "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", 353 | "integrity": "sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=", 354 | "requires": { 355 | "acorn": "~4.0.2", 356 | "object-assign": "^4.0.1" 357 | }, 358 | "dependencies": { 359 | "acorn": { 360 | "version": "4.0.13", 361 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", 362 | "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" 363 | } 364 | } 365 | }, 366 | "is-promise": { 367 | "version": "2.1.0", 368 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", 369 | "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" 370 | }, 371 | "is-regex": { 372 | "version": "1.0.4", 373 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", 374 | "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", 375 | "requires": { 376 | "has": "^1.0.1" 377 | } 378 | }, 379 | "js-stringify": { 380 | "version": "1.0.2", 381 | "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", 382 | "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" 383 | }, 384 | "jstransformer": { 385 | "version": "1.0.0", 386 | "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", 387 | "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=", 388 | "requires": { 389 | "is-promise": "^2.0.0", 390 | "promise": "^7.0.1" 391 | } 392 | }, 393 | "kind-of": { 394 | "version": "3.2.2", 395 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", 396 | "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", 397 | "requires": { 398 | "is-buffer": "^1.1.5" 399 | } 400 | }, 401 | "lazy-cache": { 402 | "version": "1.0.4", 403 | "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", 404 | "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" 405 | }, 406 | "lodash": { 407 | "version": "4.17.21", 408 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 409 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 410 | }, 411 | "longest": { 412 | "version": "1.0.1", 413 | "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", 414 | "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" 415 | }, 416 | "media-typer": { 417 | "version": "0.3.0", 418 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 419 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 420 | }, 421 | "merge-descriptors": { 422 | "version": "1.0.1", 423 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 424 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 425 | }, 426 | "methods": { 427 | "version": "1.1.2", 428 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 429 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 430 | }, 431 | "mime": { 432 | "version": "1.4.1", 433 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 434 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 435 | }, 436 | "mime-db": { 437 | "version": "1.35.0", 438 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", 439 | "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" 440 | }, 441 | "mime-types": { 442 | "version": "2.1.19", 443 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", 444 | "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", 445 | "requires": { 446 | "mime-db": "~1.35.0" 447 | } 448 | }, 449 | "ms": { 450 | "version": "2.0.0", 451 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 452 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 453 | }, 454 | "negotiator": { 455 | "version": "0.6.1", 456 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 457 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 458 | }, 459 | "object-assign": { 460 | "version": "4.1.1", 461 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 462 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 463 | }, 464 | "on-finished": { 465 | "version": "2.3.0", 466 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 467 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 468 | "requires": { 469 | "ee-first": "1.1.1" 470 | } 471 | }, 472 | "parseurl": { 473 | "version": "1.3.2", 474 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 475 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 476 | }, 477 | "path-parse": { 478 | "version": "1.0.5", 479 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", 480 | "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" 481 | }, 482 | "path-to-regexp": { 483 | "version": "0.1.7", 484 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 485 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 486 | }, 487 | "promise": { 488 | "version": "7.3.1", 489 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", 490 | "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", 491 | "requires": { 492 | "asap": "~2.0.3" 493 | } 494 | }, 495 | "proxy-addr": { 496 | "version": "2.0.3", 497 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", 498 | "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", 499 | "requires": { 500 | "forwarded": "~0.1.2", 501 | "ipaddr.js": "1.6.0" 502 | } 503 | }, 504 | "pug": { 505 | "version": "2.0.3", 506 | "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.3.tgz", 507 | "integrity": "sha1-ccuoJTfJWl6rftBGluQiH1Oqh44=", 508 | "requires": { 509 | "pug-code-gen": "^2.0.1", 510 | "pug-filters": "^3.1.0", 511 | "pug-lexer": "^4.0.0", 512 | "pug-linker": "^3.0.5", 513 | "pug-load": "^2.0.11", 514 | "pug-parser": "^5.0.0", 515 | "pug-runtime": "^2.0.4", 516 | "pug-strip-comments": "^1.0.3" 517 | } 518 | }, 519 | "pug-code-gen": { 520 | "version": "2.0.3", 521 | "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-2.0.3.tgz", 522 | "integrity": "sha512-r9sezXdDuZJfW9J91TN/2LFbiqDhmltTFmGpHTsGdrNGp3p4SxAjjXEfnuK2e4ywYsRIVP0NeLbSAMHUcaX1EA==", 523 | "requires": { 524 | "constantinople": "^3.1.2", 525 | "doctypes": "^1.1.0", 526 | "js-stringify": "^1.0.1", 527 | "pug-attrs": "^2.0.4", 528 | "pug-error": "^1.3.3", 529 | "pug-runtime": "^2.0.5", 530 | "void-elements": "^2.0.1", 531 | "with": "^5.0.0" 532 | }, 533 | "dependencies": { 534 | "pug-attrs": { 535 | "version": "2.0.4", 536 | "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-2.0.4.tgz", 537 | "integrity": "sha512-TaZ4Z2TWUPDJcV3wjU3RtUXMrd3kM4Wzjbe3EWnSsZPsJ3LDI0F3yCnf2/W7PPFF+edUFQ0HgDL1IoxSz5K8EQ==", 538 | "requires": { 539 | "constantinople": "^3.0.1", 540 | "js-stringify": "^1.0.1", 541 | "pug-runtime": "^2.0.5" 542 | } 543 | }, 544 | "pug-error": { 545 | "version": "1.3.3", 546 | "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.3.tgz", 547 | "integrity": "sha512-qE3YhESP2mRAWMFJgKdtT5D7ckThRScXRwkfo+Erqga7dyJdY3ZquspprMCj/9sJ2ijm5hXFWQE/A3l4poMWiQ==" 548 | }, 549 | "pug-runtime": { 550 | "version": "2.0.5", 551 | "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.5.tgz", 552 | "integrity": "sha512-P+rXKn9un4fQY77wtpcuFyvFaBww7/91f3jHa154qU26qFAnOe6SW1CbIDcxiG5lLK9HazYrMCCuDvNgDQNptw==" 553 | } 554 | } 555 | }, 556 | "pug-error": { 557 | "version": "1.3.2", 558 | "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.2.tgz", 559 | "integrity": "sha1-U659nSm7A89WRJOgJhCfVMR/XyY=" 560 | }, 561 | "pug-filters": { 562 | "version": "3.1.0", 563 | "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-3.1.0.tgz", 564 | "integrity": "sha1-JxZVVbwEwjbkqisDZiRt+gIbYm4=", 565 | "requires": { 566 | "clean-css": "^4.1.11", 567 | "constantinople": "^3.0.1", 568 | "jstransformer": "1.0.0", 569 | "pug-error": "^1.3.2", 570 | "pug-walk": "^1.1.7", 571 | "resolve": "^1.1.6", 572 | "uglify-js": "^2.6.1" 573 | } 574 | }, 575 | "pug-lexer": { 576 | "version": "4.0.0", 577 | "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-4.0.0.tgz", 578 | "integrity": "sha1-IQwYRX7y4XYCQnQMXmR715TOwng=", 579 | "requires": { 580 | "character-parser": "^2.1.1", 581 | "is-expression": "^3.0.0", 582 | "pug-error": "^1.3.2" 583 | } 584 | }, 585 | "pug-linker": { 586 | "version": "3.0.5", 587 | "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-3.0.5.tgz", 588 | "integrity": "sha1-npp65ABWgtAn3uuWsAD4juuDoC8=", 589 | "requires": { 590 | "pug-error": "^1.3.2", 591 | "pug-walk": "^1.1.7" 592 | } 593 | }, 594 | "pug-load": { 595 | "version": "2.0.11", 596 | "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-2.0.11.tgz", 597 | "integrity": "sha1-5kjlftET/iwfRdV4WOorrWvAFSc=", 598 | "requires": { 599 | "object-assign": "^4.1.0", 600 | "pug-walk": "^1.1.7" 601 | } 602 | }, 603 | "pug-parser": { 604 | "version": "5.0.0", 605 | "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-5.0.0.tgz", 606 | "integrity": "sha1-45Stmz/KkxI5QK/4hcBuRKt+aOQ=", 607 | "requires": { 608 | "pug-error": "^1.3.2", 609 | "token-stream": "0.0.1" 610 | } 611 | }, 612 | "pug-runtime": { 613 | "version": "2.0.4", 614 | "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.4.tgz", 615 | "integrity": "sha1-4XjhvaaKsujArPybztLFT9iM61g=" 616 | }, 617 | "pug-strip-comments": { 618 | "version": "1.0.3", 619 | "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.3.tgz", 620 | "integrity": "sha1-8VWVkiBu3G+FMQ2s9K+0igJa9Z8=", 621 | "requires": { 622 | "pug-error": "^1.3.2" 623 | } 624 | }, 625 | "pug-walk": { 626 | "version": "1.1.7", 627 | "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.7.tgz", 628 | "integrity": "sha1-wA1cUSi6xYBr7BXSt+fNq+QlMfM=" 629 | }, 630 | "qs": { 631 | "version": "6.5.1", 632 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 633 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 634 | }, 635 | "range-parser": { 636 | "version": "1.2.0", 637 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 638 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 639 | }, 640 | "raw-body": { 641 | "version": "2.3.2", 642 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", 643 | "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", 644 | "requires": { 645 | "bytes": "3.0.0", 646 | "http-errors": "1.6.2", 647 | "iconv-lite": "0.4.19", 648 | "unpipe": "1.0.0" 649 | }, 650 | "dependencies": { 651 | "depd": { 652 | "version": "1.1.1", 653 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", 654 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" 655 | }, 656 | "http-errors": { 657 | "version": "1.6.2", 658 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", 659 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", 660 | "requires": { 661 | "depd": "1.1.1", 662 | "inherits": "2.0.3", 663 | "setprototypeof": "1.0.3", 664 | "statuses": ">= 1.3.1 < 2" 665 | } 666 | }, 667 | "setprototypeof": { 668 | "version": "1.0.3", 669 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", 670 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" 671 | } 672 | } 673 | }, 674 | "regenerator-runtime": { 675 | "version": "0.11.1", 676 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", 677 | "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" 678 | }, 679 | "repeat-string": { 680 | "version": "1.6.1", 681 | "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", 682 | "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" 683 | }, 684 | "resolve": { 685 | "version": "1.8.1", 686 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", 687 | "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", 688 | "requires": { 689 | "path-parse": "^1.0.5" 690 | } 691 | }, 692 | "right-align": { 693 | "version": "0.1.3", 694 | "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", 695 | "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", 696 | "requires": { 697 | "align-text": "^0.1.1" 698 | } 699 | }, 700 | "safe-buffer": { 701 | "version": "5.1.1", 702 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 703 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 704 | }, 705 | "send": { 706 | "version": "0.16.2", 707 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", 708 | "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", 709 | "requires": { 710 | "debug": "2.6.9", 711 | "depd": "~1.1.2", 712 | "destroy": "~1.0.4", 713 | "encodeurl": "~1.0.2", 714 | "escape-html": "~1.0.3", 715 | "etag": "~1.8.1", 716 | "fresh": "0.5.2", 717 | "http-errors": "~1.6.2", 718 | "mime": "1.4.1", 719 | "ms": "2.0.0", 720 | "on-finished": "~2.3.0", 721 | "range-parser": "~1.2.0", 722 | "statuses": "~1.4.0" 723 | } 724 | }, 725 | "serve-static": { 726 | "version": "1.13.2", 727 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", 728 | "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", 729 | "requires": { 730 | "encodeurl": "~1.0.2", 731 | "escape-html": "~1.0.3", 732 | "parseurl": "~1.3.2", 733 | "send": "0.16.2" 734 | } 735 | }, 736 | "setprototypeof": { 737 | "version": "1.1.0", 738 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 739 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 740 | }, 741 | "shuffle-array": { 742 | "version": "1.0.1", 743 | "resolved": "https://registry.npmjs.org/shuffle-array/-/shuffle-array-1.0.1.tgz", 744 | "integrity": "sha1-xP88/nTRb5NzBZIwGyXmV3sSiYs=" 745 | }, 746 | "source-map": { 747 | "version": "0.5.7", 748 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", 749 | "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" 750 | }, 751 | "statuses": { 752 | "version": "1.4.0", 753 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 754 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 755 | }, 756 | "to-fast-properties": { 757 | "version": "1.0.3", 758 | "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", 759 | "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" 760 | }, 761 | "token-stream": { 762 | "version": "0.0.1", 763 | "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz", 764 | "integrity": "sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=" 765 | }, 766 | "type-is": { 767 | "version": "1.6.16", 768 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 769 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", 770 | "requires": { 771 | "media-typer": "0.3.0", 772 | "mime-types": "~2.1.18" 773 | } 774 | }, 775 | "uglify-js": { 776 | "version": "2.8.29", 777 | "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", 778 | "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", 779 | "requires": { 780 | "source-map": "~0.5.1", 781 | "uglify-to-browserify": "~1.0.0", 782 | "yargs": "~3.10.0" 783 | } 784 | }, 785 | "uglify-to-browserify": { 786 | "version": "1.0.2", 787 | "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", 788 | "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", 789 | "optional": true 790 | }, 791 | "unpipe": { 792 | "version": "1.0.0", 793 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 794 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 795 | }, 796 | "utils-merge": { 797 | "version": "1.0.1", 798 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 799 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 800 | }, 801 | "vary": { 802 | "version": "1.1.2", 803 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 804 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 805 | }, 806 | "void-elements": { 807 | "version": "2.0.1", 808 | "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", 809 | "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" 810 | }, 811 | "window-size": { 812 | "version": "0.1.0", 813 | "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", 814 | "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" 815 | }, 816 | "with": { 817 | "version": "5.1.1", 818 | "resolved": "https://registry.npmjs.org/with/-/with-5.1.1.tgz", 819 | "integrity": "sha1-+k2qktrzLE6pTtRTyB8EaGtXXf4=", 820 | "requires": { 821 | "acorn": "^3.1.0", 822 | "acorn-globals": "^3.0.0" 823 | } 824 | }, 825 | "wordwrap": { 826 | "version": "0.0.2", 827 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", 828 | "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" 829 | }, 830 | "yargs": { 831 | "version": "3.10.0", 832 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", 833 | "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", 834 | "requires": { 835 | "camelcase": "^1.0.2", 836 | "cliui": "^2.1.0", 837 | "decamelize": "^1.0.0", 838 | "window-size": "0.1.0" 839 | } 840 | } 841 | } 842 | } 843 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui-test-automation-playground", 3 | "version": "1.0.0", 4 | "description": "UI Test Automation Playground", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Inflectra Corporation", 10 | "license": "Apache License 2.0", 11 | "dependencies": { 12 | "express": "^4.16.3", 13 | "pug": "^2.0.3", 14 | "shuffle-array": "^1.0.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /static/cube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inflectra/ui-test-automation-playground/dc1c4adbcd571107042d10256d3a684e9c70b160/static/cube.png -------------------------------------------------------------------------------- /static/fork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Inflectra/ui-test-automation-playground/dc1c4adbcd571107042d10256d3a684e9c70b160/static/fork.png -------------------------------------------------------------------------------- /static/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 39 | 40 | 44 | 47 | 51 | 55 | 59 | 63 | 67 | 71 | 76 | 81 | 82 | 83 | 89 | 90 | -------------------------------------------------------------------------------- /static/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.15.0 2 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */ 3 | /** 4 | * prism.js default theme for JavaScript, CSS and HTML 5 | * Based on dabblet (http://dabblet.com) 6 | * @author Lea Verou 7 | */ 8 | 9 | code[class*="language-"], 10 | pre[class*="language-"] { 11 | color: black; 12 | background: none; 13 | text-shadow: 0 1px white; 14 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 15 | text-align: left; 16 | white-space: pre; 17 | word-spacing: normal; 18 | word-break: normal; 19 | word-wrap: normal; 20 | line-height: 1.5; 21 | 22 | -moz-tab-size: 4; 23 | -o-tab-size: 4; 24 | tab-size: 4; 25 | 26 | -webkit-hyphens: none; 27 | -moz-hyphens: none; 28 | -ms-hyphens: none; 29 | hyphens: none; 30 | } 31 | 32 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, 33 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { 34 | text-shadow: none; 35 | background: #b3d4fc; 36 | } 37 | 38 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection, 39 | code[class*="language-"]::selection, code[class*="language-"] ::selection { 40 | text-shadow: none; 41 | background: #b3d4fc; 42 | } 43 | 44 | @media print { 45 | code[class*="language-"], 46 | pre[class*="language-"] { 47 | text-shadow: none; 48 | } 49 | } 50 | 51 | /* Code blocks */ 52 | pre[class*="language-"] { 53 | padding: 1em; 54 | margin: .5em 0; 55 | overflow: auto; 56 | } 57 | 58 | :not(pre) > code[class*="language-"], 59 | pre[class*="language-"] { 60 | background: #f5f2f0; 61 | } 62 | 63 | /* Inline code */ 64 | :not(pre) > code[class*="language-"] { 65 | padding: .1em; 66 | border-radius: .3em; 67 | white-space: normal; 68 | } 69 | 70 | .token.comment, 71 | .token.prolog, 72 | .token.doctype, 73 | .token.cdata { 74 | color: slategray; 75 | } 76 | 77 | .token.punctuation { 78 | color: #999; 79 | } 80 | 81 | .namespace { 82 | opacity: .7; 83 | } 84 | 85 | .token.property, 86 | .token.tag, 87 | .token.boolean, 88 | .token.number, 89 | .token.constant, 90 | .token.symbol, 91 | .token.deleted { 92 | color: #905; 93 | } 94 | 95 | .token.selector, 96 | .token.attr-name, 97 | .token.string, 98 | .token.char, 99 | .token.builtin, 100 | .token.inserted { 101 | color: #690; 102 | } 103 | 104 | .token.operator, 105 | .token.entity, 106 | .token.url, 107 | .language-css .token.string, 108 | .style .token.string { 109 | color: #9a6e3a; 110 | background: hsla(0, 0%, 100%, .5); 111 | } 112 | 113 | .token.atrule, 114 | .token.attr-value, 115 | .token.keyword { 116 | color: #07a; 117 | } 118 | 119 | .token.function, 120 | .token.class-name { 121 | color: #DD4A68; 122 | } 123 | 124 | .token.regex, 125 | .token.important, 126 | .token.variable { 127 | color: #e90; 128 | } 129 | 130 | .token.important, 131 | .token.bold { 132 | font-weight: bold; 133 | } 134 | .token.italic { 135 | font-style: italic; 136 | } 137 | 138 | .token.entity { 139 | cursor: help; 140 | } 141 | 142 | -------------------------------------------------------------------------------- /static/prism.js: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.15.0 2 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */ 3 | 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={manual:_self.Prism&&_self.Prism.manual,disableWorkerMessageHandler:_self.Prism&&_self.Prism.disableWorkerMessageHandler,util:{encode:function(e){return e instanceof r?new r(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)return;if(!(w instanceof s)){if(m&&b!=t.length-1){h.lastIndex=k;var _=h.exec(e);if(!_)break;for(var j=_.index+(d?_[1].length:0),P=_.index+_[0].length,A=b,x=k,O=t.length;O>A&&(P>x||!t[A].type&&!t[A-1].greedy);++A)x+=t[A].length,j>=x&&(++b,k=x);if(t[b]instanceof s)continue;I=A-b,w=e.slice(k,x),_.index-=k}else{h.lastIndex=0;var _=h.exec(w),I=1}if(_){d&&(p=_[1]?_[1].length:0);var j=_.index+p,_=_[0].slice(p),P=j+_.length,N=w.slice(0,j),S=w.slice(P),C=[b,I];N&&(++b,k+=N.length,C.push(N));var E=new s(u,f?n.tokenize(_,f):_,y,_,m);if(C.push(E),S&&C.push(S),Array.prototype.splice.apply(t,C),1!=I&&n.matchGrammar(e,t,r,b,k,!0,u),i)break}else if(i)break}}}}},tokenize:function(e,t){var r=[e],a=t.rest;if(a){for(var l in a)t[l]=a[l];delete t.rest}return n.matchGrammar(e,r,t,0,0,!1),r},hooks:{all:{},add:function(e,t){var r=n.hooks.all;r[e]=r[e]||[],r[e].push(t)},run:function(e,t){var r=n.hooks.all[e];if(r&&r.length)for(var a,l=0;a=r[l++];)a(t)}}},r=n.Token=function(e,t,n,r,a){this.type=e,this.content=t,this.alias=n,this.length=0|(r||"").length,this.greedy=!!a};if(r.stringify=function(e,t,a){if("string"==typeof e)return e;if("Array"===n.util.type(e))return e.map(function(n){return r.stringify(n,t,e)}).join("");var l={type:e.type,content:r.stringify(e.content,t,a),tag:"span",classes:["token",e.type],attributes:{},language:t,parent:a};if(e.alias){var i="Array"===n.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(l.classes,i)}n.hooks.run("wrap",l);var o=Object.keys(l.attributes).map(function(e){return e+'="'+(l.attributes[e]||"").replace(/"/g,""")+'"'}).join(" ");return"<"+l.tag+' class="'+l.classes.join(" ")+'"'+(o?" "+o:"")+">"+l.content+""},!_self.document)return _self.addEventListener?(n.disableWorkerMessageHandler||_self.addEventListener("message",function(e){var t=JSON.parse(e.data),r=t.language,a=t.code,l=t.immediateClose;_self.postMessage(n.highlight(a,n.languages[r],r)),l&&_self.close()},!1),_self.Prism):_self.Prism;var a=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();return a&&(n.filename=a.src,n.manual||a.hasAttribute("data-manual")||("loading"!==document.readyState?window.requestAnimationFrame?window.requestAnimationFrame(n.highlightAll):window.setTimeout(n.highlightAll,16):document.addEventListener("DOMContentLoaded",n.highlightAll))),_self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); 4 | Prism.languages.markup={comment://,prolog:/<\?[\s\S]+?\?>/,doctype://i,cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|[^\s'">=]+))?)*\s*\/?>/i,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=(?:("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|[^\s'">=]+)/i,inside:{punctuation:[/^=/,{pattern:/(^|[^\\])["']/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/&#?[\da-z]{1,8};/i},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.hooks.add("wrap",function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))}),Prism.languages.xml=Prism.languages.markup,Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup; 5 | Prism.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-]+?.*?(?:;|(?=\s*\{))/i,inside:{rule:/@[\w-]+/}},url:/url\((?:(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,selector:/[^{}\s][^{};]*?(?=\s*\{)/,string:{pattern:/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},property:/[-_a-z\xA0-\uFFFF][-\w\xA0-\uFFFF]*(?=\s*:)/i,important:/\B!important\b/i,"function":/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:]/},Prism.languages.css.atrule.inside.rest=Prism.languages.css,Prism.languages.markup&&(Prism.languages.insertBefore("markup","tag",{style:{pattern:/()[\s\S]*?(?=<\/style>)/i,lookbehind:!0,inside:Prism.languages.css,alias:"language-css",greedy:!0}}),Prism.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|')(?:\\[\s\S]|(?!\1)[^\\])*\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:Prism.languages.markup.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:Prism.languages.css}},alias:"language-css"}},Prism.languages.markup.tag)); 6 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(?:true|false)\b/,"function":/[a-z0-9_]+(?=\()/i,number:/\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/}; 7 | Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/,number:/\b(?:0[xX][\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|NaN|Infinity)\b|(?:\b\d+\.?\d*|\B\.\d+)(?:[Ee][+-]?\d+)?/,"function":/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*\()/i,operator:/-[-=]?|\+[+=]?|!=?=?|<>?>?=?|=(?:==?|>)?|&[&=]?|\|[|=]?|\*\*?=?|\/=?|~|\^=?|%=?|\?|\.{3}/}),Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s])\s*)\/(\[[^\]\r\n]+]|\\.|[^\/\\\[\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})\]]))/,lookbehind:!0,greedy:!0},"function-variable":{pattern:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*=\s*(?:function\b|(?:\([^()]*\)|[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)\s*=>))/i,alias:"function"},constant:/\b[A-Z][A-Z\d_]*\b/}),Prism.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\[\s\S]|\${[^}]+}|[^\\`])*`/,greedy:!0,inside:{interpolation:{pattern:/\${[^}]+}/,inside:{"interpolation-punctuation":{pattern:/^\${|}$/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}}}),Prism.languages.javascript["template-string"].inside.interpolation.inside.rest=Prism.languages.javascript,Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/()[\s\S]*?(?=<\/script>)/i,lookbehind:!0,inside:Prism.languages.javascript,alias:"language-javascript",greedy:!0}}),Prism.languages.js=Prism.languages.javascript; 8 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | section { 2 | margin-top: 50px; 3 | } 4 | 5 | h1#title { 6 | font-size: 3.5em; 7 | } 8 | 9 | blockquote#citation { 10 | margin-top: 30px; 11 | margin-bottom: 30px; 12 | } 13 | 14 | li.active { 15 | font-weight: 700; 16 | } 17 | 18 | #content { 19 | margin-top: 30px; 20 | } 21 | 22 | #spinner { 23 | font-size:24px; 24 | margin-left:10px; 25 | } 26 | 27 | .btn-test { 28 | margin-left:20px; 29 | } 30 | 31 | #footer { 32 | font-size: 11px; 33 | margin: 60px 10px 30px; 34 | padding-left: 5%; 35 | max-width: 1090px; 36 | } 37 | 38 | #footer-content { 39 | display: flex; 40 | flex-wrap: wrap; 41 | justify-content: center; 42 | align-items: center; 43 | margin: 20px auto 0; 44 | } 45 | 46 | #footer-content div { 47 | float: left; 48 | margin-right: 20px; 49 | background-repeat: no-repeat; 50 | background-position: 0 1px; 51 | padding-bottom: 4px; 52 | } 53 | 54 | #github { 55 | margin-right: 5px !important; 56 | } 57 | 58 | #license { 59 | text-align: center; 60 | margin-top: -5px; 61 | } 62 | 63 | #fork { 64 | background-image: url(/static/fork.png); 65 | background-size: 11px 16px; 66 | padding-left: 15px; 67 | } 68 | 69 | #spa { 70 | position: relative; 71 | top: 0; 72 | bottom: 0; 73 | width: 100%; 74 | height: 100%; 75 | } 76 | 77 | .spa-view { 78 | position: absolute; 79 | top: 0; 80 | bottom: 0; 81 | width: 100%; 82 | height: 100%; 83 | } 84 | 85 | /* Dynamic table */ 86 | .annotate{ 87 | font-style: italic; 88 | color: #366ED4; 89 | } 90 | 91 | [role="table"] { 92 | display: table; 93 | } 94 | 95 | [role="table"] > div[id] { 96 | display: table-caption; 97 | font-style: italic; 98 | } 99 | 100 | [role="table"] [role="row"] { 101 | display: table-row; 102 | } 103 | 104 | [role="table"] [role="cell"], 105 | [role="table"] [role="columnheader"] { 106 | display: table-cell; 107 | padding: 0.125em 0.25em; 108 | width: 8em; 109 | } 110 | 111 | [role="table"] [role="columnheader"] { 112 | font-weight: bold; 113 | border-bottom: thin solid #888; 114 | } 115 | 116 | [role="table"] [role="rowgroup"]:last-child [role="row"]:nth-child(odd) { 117 | background-color: #ddd; 118 | } 119 | 120 | .zerowidth { 121 | width: 0px; 122 | min-width: 0px; 123 | padding-left: 0; 124 | padding-right: 0; 125 | border-left-width: 0; 126 | border-right-width: 0; 127 | white-space: nowrap; 128 | overflow: hidden; 129 | } 130 | 131 | .offscreen { 132 | position: absolute; 133 | top: -9999px; 134 | left: -9999px; 135 | } 136 | 137 | #movingTarget { 138 | margin-left: 100px; 139 | } 140 | 141 | .spin { 142 | position:absolute; 143 | animation: orbit 5s forwards; 144 | animation-timing-function: linear; 145 | } 146 | 147 | ul.indented-right { 148 | padding-left: 20px; 149 | } 150 | 151 | @-webkit-keyframes orbit { 152 | from { -webkit-transform: rotate(0deg) translateX(150px) rotate(0deg); } 153 | to { -webkit-transform: rotate(360deg) translateX(150px) rotate(-360deg); } 154 | } 155 | -------------------------------------------------------------------------------- /static/upload.css: -------------------------------------------------------------------------------- 1 | #root{max-width:1280px;margin:0 auto;padding:2rem;text-align:center}.logo{height:6em;padding:1.5em;will-change:filter;transition:filter .3s}.logo:hover{filter:drop-shadow(0 0 2em #646cffaa)}.logo.react:hover{filter:drop-shadow(0 0 2em #61dafbaa)}@keyframes logo-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@media (prefers-reduced-motion: no-preference){a:nth-of-type(2) .logo{animation:logo-spin infinite 20s linear}}.card{padding:2em}.read-the-docs{color:#888}.drag-drop{background:#fff;border:1px solid var(--border-color);border-radius:8px}.document-uploader{border:2px dashed #4282fe;background-color:#f4fbff;padding:10px;display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative;border-radius:8px;cursor:pointer}.document-uploader.active{border-color:#6dc24b}.document-uploader .upload-info{display:flex;align-items:center;margin-bottom:1rem}.document-uploader .upload-info svg{font-size:36px;margin-right:1rem}.document-uploader .upload-info div p{margin:0;font-size:16px}.document-uploader .upload-info div p:first-child{font-weight:700}.document-uploader .file-list{display:flex;flex-direction:column;gap:.5rem;width:100%;height:30vh;&__container{width:100%;height:100%;overflow:auto}}.document-uploader .file-item{display:flex;justify-content:space-between;align-items:center;padding:.5rem;border:1px solid var(--border-color);border-radius:8px}.document-uploader .file-item .file-info{display:flex;flex-direction:column;gap:.25rem;flex:1}.document-uploader .file-item .file-info p{margin:0;font-size:14px;color:#333}.document-uploader .file-item .file-actions{cursor:pointer}.document-uploader .file-item .file-actions svg{font-size:18px;color:#888}.document-uploader .file-item .file-actions:hover svg{color:#d44}.document-uploader .browse-btn{display:flex;align-items:center;justify-content:center;padding:.5rem 1rem;border:1px solid var(--border-color);border-radius:8px;cursor:pointer;background-color:var(--primary-color)}.document-uploader .browse-btn:hover{background-color:transparent}.document-uploader .success-file{display:flex;align-items:center;color:#6dc24b}.document-uploader .success-file p{margin:0;font-size:14px;font-weight:700}.document-uploader input[type=file]{display:none}:root{font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}body{margin:0;display:flex;place-items:center;min-width:320px;min-height:100vh}h1{font-size:3.2em;line-height:1.1}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}@media (prefers-color-scheme: light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}} 2 | -------------------------------------------------------------------------------- /static/upload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | UI Test Automation Playground 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /views/Ajax.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p An element may appaear on a page after processing of an AJAX request to a web server. A test should be able to wait for an element to show up. 11 | h4 Scenario 12 | ul 13 | li Record the following steps. Press the button below and wait for data to appear (15 seconds), click on text of the loaded label. 14 | li Then execute your test to make sure it waits for label text to appear. 15 | h4 Playground 16 | button( 17 | type="button" 18 | class="btn btn-primary" 19 | id="ajaxButton" 20 | onclick="LoadLabel()" 21 | ) Button Triggering AJAX Request 22 | i( 23 | id="spinner" 24 | class="fa fa-spinner fa-spin" 25 | style="display:none" 26 | ) 27 |
28 |
29 | 30 | script. 31 | function LoadLabel() 32 | { 33 | $('#spinner').show(); 34 | $.get( "/ajaxdata", function( data ) 35 | { 36 | var label = document.createElement("p"); 37 | label.className = "bg-success"; 38 | label.innerHTML = data; 39 | document.getElementById("content").appendChild(label); 40 | $('#spinner').hide(); 41 | }); 42 | } 43 | 44 | include footer.html 45 | 46 | -------------------------------------------------------------------------------- /views/Alerts.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p Dealing with standard alerts, prompts and confirmations is an important skill in test automation. 11 | h4 Scenario 12 | ul 13 | li Record clicks on `Alert`, `Confirm` and `Prompt` buttons. Click `OK` to confirm, answer with non-default value to the prompt. 14 | li Then execute your test to make sure that it passes completely without manual steps. 15 | h4 Playground 16 | button( 17 | id="alertButton" 18 | type="button" 19 | class="btn btn-primary" 20 | ) Alert 21 | span   22 | button( 23 | id="confirmButton" 24 | type="button" 25 | class="btn btn-primary" 26 | ) Confirm 27 | span   28 | button( 29 | id="promptButton" 30 | type="button" 31 | class="btn btn-primary" 32 | ) Prompt 33 | 34 | script. 35 | function ClickEventHandler(event) 36 | { 37 | if (event.target.id == "alertButton") 38 | { 39 | alert("Today is a working day.\nOr less likely a holiday."); 40 | console.log("Alert closed"); 41 | } 42 | else if (event.target.id == "confirmButton") 43 | { 44 | if (confirm("Today is Friday.\nDo you agree?")) 45 | { 46 | setTimeout(function () { alert("Yes") }, 1000); 47 | } 48 | else 49 | { 50 | setTimeout(function () { alert("No") }, 1000); 51 | } 52 | } 53 | else if (event.target.id == "promptButton") 54 | { 55 | var res = prompt("Choose \"cats\" or 'dogs'.\nEnter your value:", "cats"); 56 | if (res == null) 57 | { 58 | res = "no answer"; 59 | } 60 | console.log("Prompt answer: " + res); 61 | setTimeout(function () { alert("User value: " + res) }, 1000); 62 | } 63 | } 64 | document.body.addEventListener('click', ClickEventHandler, true); 65 | 66 | 67 | include footer.html 68 | -------------------------------------------------------------------------------- /views/Animation.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p Before clicking a button we may need to wait for it to become stable (not moving). 11 | h4 Scenario 12 | ul 13 | li Record `Start Animation` button click. Wait for animation to complete and record click on `Moving Target`. 14 | li Then execute your test to make sure that when Moving Target is clicked, it's class does not contain 'spin'. The class is printed on the status label below the buttons. 15 | h4 Playground 16 | button( 17 | id="animationButton" 18 | type="button" 19 | class="btn btn-secondary" 20 | onclick="startAnimation()" 21 | ) Start Animation 22 | span   23 | button( 24 | id="movingTarget" 25 | type="button" 26 | class="btn btn-primary" 27 | onclick="movingTargetClicked()" 28 | onanimationend="animationDone()" 29 | ) Moving Target 30 | div( 31 | id="opstatus" 32 | ) --- 33 | 34 | script. 35 | function startAnimation() 36 | { 37 | setStatus("Animating the button..."); 38 | byid("movingTarget").className = "btn btn-primary spin"; 39 | } 40 | 41 | function animationDone() 42 | { 43 | byid("movingTarget").className = "btn btn-primary"; 44 | setStatus("Animation done"); 45 | } 46 | 47 | function movingTargetClicked() 48 | { 49 | setStatus("Moving Target clicked. It's class name is '" + byid("movingTarget").className + "'"); 50 | } 51 | 52 | include footer.html 53 | 54 | -------------------------------------------------------------------------------- /views/AutoWait.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p Before clicking an element or entering text we may need to wait until the element becomes interactable. 11 | h4 Scenario 12 | ul 13 | li Choose an element type from the combobox. 14 | li Check the checkboxes to set the element's properties. 15 | li Then click one of the Apply buttons to immediately apply the settings and restore interactable state of the element after a delay. 16 | li Interact with the element in the Playground section (click, select item, enter text). 17 | li Observe the status messages. 18 | div.row 19 | div.col-6 20 | h4 Settings 21 | form 22 | // ComboBox 23 | div(class="mb-3") 24 | label(for="element-type" class="form-label") Choose an element type:  25 | select(id="element-type" name="element-type" class="form-select") 26 | option(value="button") Button 27 | option(value="input") Input 28 | option(value="textarea") Textarea 29 | option(value="select") Select 30 | option(value="label") Label 31 | 32 | // Checkboxes (No bullets in the list) 33 | ul(class="list-unstyled indented-right") 34 | li(class="form-check") 35 | input(type="checkbox" id="visible" name="visible" class="form-check-input" checked) 36 | label(for="visible" class="form-check-label") Visible 37 | li(class="form-check") 38 | input(type="checkbox" id="enabled" name="enabled" class="form-check-input" checked) 39 | label(for="enabled" class="form-check-label") Enabled 40 | li(class="form-check") 41 | input(type="checkbox" id="editable" name="editable" class="form-check-input" checked) 42 | label(for="editable" class="form-check-label") Editable 43 | li(class="form-check") 44 | input(type="checkbox" id="ontop" name="ontop" class="form-check-input" checked) 45 | label(for="ontop" class="form-check-label") On Top 46 | li(class="form-check") 47 | input(type="checkbox" id="nonzero" name="nonzero" class="form-check-input" checked) 48 | label(for="nonzero" class="form-check-label") Non Zero Size 49 | button( 50 | id="applyButton3" 51 | type="button" 52 | class="btn btn-secondary" 53 | onclick="applySettings(3)" 54 | ) Apply 3s 55 | span   56 | button( 57 | id="applyButton5" 58 | type="button" 59 | class="btn btn-secondary" 60 | onclick="applySettings(5)" 61 | ) Apply 5s 62 | span   63 | button( 64 | id="applyButton10" 65 | type="button" 66 | class="btn btn-secondary" 67 | onclick="applySettings(10)" 68 | ) Apply 10s 69 | div.col-6 70 | h4 Playground 71 | div(id="element-container") 72 | button( 73 | id="target" 74 | type="button" 75 | class="btn btn-primary" 76 | onclick="targetClicked()" 77 | ) Button 78 | div( 79 | id="opstatus" 80 | ) --- 81 | 82 | script. 83 | var options = { 84 | visible: true, 85 | enabled: true, 86 | editable: true, 87 | ontop: true 88 | }; 89 | 90 | // Process checkbox clicks 91 | document.querySelectorAll('.form-check-input').forEach(checkbox => { 92 | checkbox.addEventListener('change', function() { 93 | console.log(this.id + ' checkbox clicked. Checked: ' + this.checked); 94 | options[this.id] = this.checked; 95 | }); 96 | }); 97 | 98 | // Handle combobox change and check all checkboxes 99 | document.getElementById('element-type').addEventListener('change', function() { 100 | console.log('Combobox selection changed. Checking all checkboxes.'); 101 | document.querySelectorAll('.form-check-input').forEach(checkbox => { 102 | checkbox.checked = true; 103 | options[checkbox.id] = true; 104 | }); 105 | 106 | var parentElement = document.getElementById('element-container'); 107 | var childElement = document.getElementById('target'); 108 | parentElement.removeChild(childElement); 109 | 110 | if (this.value == "input") 111 | { 112 | var input = document.createElement('input'); 113 | input.type = 'text'; 114 | input.id = 'target'; 115 | input.className = 'form-control'; 116 | input.onclick = targetClicked; 117 | parentElement.appendChild(input); 118 | input.addEventListener('change', function(event) { 119 | setStatus("Text: " + this.value); 120 | }); 121 | } 122 | else if (this.value == "textarea") 123 | { 124 | var textarea = document.createElement('textarea'); 125 | textarea.id = 'target'; 126 | textarea.className = 'form-control'; 127 | textarea.onclick = targetClicked; 128 | parentElement.appendChild(textarea); 129 | textarea.addEventListener('change', function(event) { 130 | setStatus("Text: " + this.value); 131 | }); 132 | } 133 | else if (this.value == "select") 134 | { 135 | var select = document.createElement('select'); 136 | select.id = 'target'; 137 | select.className = 'form-select'; 138 | var optionItems = ['Item 1', 'Item 2', 'Item 3']; 139 | optionItems.forEach(function(item) { 140 | var option = document.createElement('option'); 141 | option.value = item; 142 | option.textContent = item; 143 | select.appendChild(option); 144 | }); 145 | parentElement.appendChild(select); 146 | select.addEventListener('change', function(event) { 147 | setStatus("Selected: " + this.value); 148 | }); 149 | select.addEventListener('click', function(event) { 150 | if (event.detail == 1) 151 | { 152 | setStatus("Target clicked."); 153 | } 154 | }); 155 | } 156 | else if (this.value == "label") 157 | { 158 | var label = document.createElement('label'); 159 | label.id = 'target'; 160 | label.className = 'form-label'; 161 | label.onclick = targetClicked; 162 | label.innerHTML = "This is a Label"; 163 | parentElement.appendChild(label); 164 | } 165 | else 166 | { 167 | var button = document.createElement('button'); 168 | button.id = 'target'; 169 | button.className = 'btn btn-primary'; 170 | button.onclick = targetClicked; 171 | button.innerHTML = 'Button'; 172 | parentElement.appendChild(button); 173 | } 174 | setStatus(""); 175 | }); 176 | 177 | var lastWidth; 178 | var lastHeight; 179 | var lastPadding; 180 | var lastBorder; 181 | 182 | function applyOptions() 183 | { 184 | var element = document.getElementById('target'); 185 | element.style.visibility = options.visible ? 'visible' : 'hidden'; 186 | if (element.tagName != 'LABEL') 187 | { 188 | element.disabled = !options.enabled; 189 | } 190 | if (element.tagName == 'INPUT' || element.tagName == 'TEXTAREA') 191 | { 192 | element.readOnly = !options.editable; 193 | } 194 | 195 | if (!options.ontop) 196 | { 197 | createOverlayElement(); 198 | } 199 | else 200 | { 201 | deleteOverlayElement(); 202 | } 203 | 204 | if (!options.nonzero) 205 | { 206 | const computedStyle = window.getComputedStyle(element); 207 | lastWidth = computedStyle.width; 208 | lastHeight = computedStyle.height; 209 | lastPadding = computedStyle.padding; 210 | lastBorder = computedStyle.border; 211 | element.style.width = "0px"; 212 | element.style.height = "0px"; 213 | element.style.padding = "0px"; 214 | element.style.border = "none"; 215 | if (element.tagName == 'LABEL') 216 | { 217 | element.style.overflow = 'hidden'; 218 | } 219 | else if (element.tagName == 'BUTTON') 220 | { 221 | element.style.pointerEvents = 'none'; 222 | } 223 | } 224 | else 225 | { 226 | if (element.style.width == "0px") 227 | { 228 | element.style.width = lastWidth; 229 | element.style.height = lastHeight; 230 | element.style.padding = lastPadding; 231 | element.style.border = lastBorder; 232 | if (element.tagName == 'LABEL') 233 | { 234 | element.style.overflow = 'auto'; 235 | } 236 | else if (element.tagName == 'BUTTON') 237 | { 238 | element.style.pointerEvents = 'auto'; 239 | } 240 | } 241 | } 242 | } 243 | 244 | function applySettings(timeout) 245 | { 246 | setStatus(`Target element settings applied for ${timeout} seconds.`); 247 | applyOptions(); 248 | 249 | setTimeout(function() { 250 | 251 | setStatus("Target element state restored."); 252 | document.querySelectorAll('.form-check-input').forEach(checkbox => { 253 | checkbox.checked = true; 254 | options[checkbox.id] = true; 255 | }); 256 | applyOptions(); 257 | }, timeout * 1000); 258 | } 259 | 260 | function createOverlayElement() 261 | { 262 | var targetElement = document.getElementById('target'); 263 | var targetRect = targetElement.getBoundingClientRect(); 264 | var overlayElement = document.createElement('div'); 265 | 266 | overlayElement.id = 'overlay'; 267 | 268 | overlayElement.style.position = 'absolute'; 269 | overlayElement.style.top = targetRect.top + 'px'; 270 | overlayElement.style.left = targetRect.left + 'px'; 271 | overlayElement.style.width = targetRect.width + 'px'; 272 | overlayElement.style.height = targetRect.height + 'px'; 273 | 274 | overlayElement.style.zIndex = '1000'; // Higher than the target's z-index 275 | overlayElement.style.backgroundColor = 'rgba(255, 0, 0, 0.5)'; // Semi-transparent red 276 | 277 | document.body.appendChild(overlayElement); 278 | } 279 | 280 | function deleteOverlayElement() 281 | { 282 | var overlayElement = document.getElementById('overlay'); 283 | if (overlayElement) 284 | { 285 | overlayElement.remove(); 286 | } 287 | } 288 | 289 | function targetClicked() 290 | { 291 | setStatus("Target clicked."); 292 | } 293 | 294 | include footer.html 295 | 296 | -------------------------------------------------------------------------------- /views/ClassAttr.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p Class attribute of an element may contain more than one class reference. E.g. 11 |
<button class="btn btn-primary btn-test">
12 | p XPath selector relying on a class must be well formed. For example, the following will not work: 13 |
//button[@class='btn-primary']
14 | p Correct variant is 15 |
//button[contains(concat(' ', normalize-space(@class), ' '), ' btn-primary ')]
16 | h4 Scenario 17 | ul 18 | li Record primary (blue) button click and press ok in alert popup. 19 | li Then execute your test to make sure that it can identify the button using btn-primary class. 20 | h4 Playground 21 | button( 22 | type="button" 23 | class="btn class1 " + button1Class + " btn-test" 24 | ) Button 25 | button( 26 | type="button" 27 | class="btn class2 " + button2Class + " btn-test" 28 | ) Button 29 | button( 30 | type="button" 31 | class="btn class3 " + button3Class + " btn-test" 32 | ) Button 33 | 34 | script. 35 | function ClickEventHandler(event) 36 | { 37 | if (("" + event.target.className).indexOf("btn-primary") != -1) 38 | { 39 | alert("Primary button pressed"); 40 | } 41 | } 42 | document.body.addEventListener('click', ClickEventHandler, true); 43 | 44 | include footer.html 45 | 46 | -------------------------------------------------------------------------------- /views/Click.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p Physical mouse click and DOM event emulated click are differently handled by browsers. There are still cases, with sometimes hardly identifiable reasons, when an event based click does not work. The solution for this problem is emulating physical mouse click. This page is specifically designed to ignore event based click. 11 | h4 Scenario 12 | ul 13 | li Record button click. The button becomes green after clicking. 14 | li Then execute your test to make sure that it is able to click the button. 15 | h4 Playground 16 | button( 17 | id="badButton" 18 | type="button" 19 | class="btn btn-primary" 20 | ) Button That Ignores DOM Click Event 21 | 22 | script. 23 | function ClickEventHandler(event) 24 | { 25 | if (event.target.id == "badButton") 26 | { 27 | if (event.screenX > 0) 28 | { 29 | event.target.className = 'btn btn-success'; 30 | } 31 | } 32 | } 33 | document.body.addEventListener('click', ClickEventHandler, true); 34 | 35 | include footer.html 36 | 37 | -------------------------------------------------------------------------------- /views/ClientDelay.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p An element may appaear on a page after heavy JavaScript processing on a client side. A test should be able to wait for an element to show up. 11 | h4 Scenario 12 | ul 13 | li Record the following steps. Press the button below and wait for data to appear (15 seconds), click on text of the loaded label. 14 | li Then execute your test to make sure it waits for label text to appear. 15 | h4 Playground 16 | button( 17 | type="button" 18 | class="btn btn-primary" 19 | id="ajaxButton" 20 | onclick="CreateLabel()" 21 | ) Button Triggering Client Side Logic 22 | i( 23 | id="spinner" 24 | class="fa fa-spinner fa-spin" 25 | style="display:none" 26 | ) 27 |
28 |
29 | 30 | script. 31 | function CreateLabel() 32 | { 33 | $('#spinner').show(); 34 | setTimeout(function() 35 | { 36 | var label = document.createElement("p"); 37 | label.className = "bg-success"; 38 | label.innerHTML = "Data calculated on the client side."; 39 | document.getElementById("content").appendChild(label); 40 | $('#spinner').hide(); 41 | }, 15000); 42 | } 43 | 44 | include footer.html 45 | 46 | -------------------------------------------------------------------------------- /views/DisabledInput.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p Sometimes elements become enabled after some time they are rendered on the page. A test should be able to wait for an element to become enabled. 11 | h4 Scenario 12 | ul 13 | li Record button click. Also record text input into an edit field. 14 | li Make a test that enters text as soon as the edit field becomes enabled. 15 | h4 Playground 16 |
17 | input( 18 | id="inputField" 19 | title="Edit Field" 20 | onchange="setStatus('Value changed to: ' + this.value)" 21 | class="form-control" 22 | placeholder="Change me..." 23 | ) 24 | br 25 | button( 26 | id="enableButton" 27 | class="btn btn-primary" 28 | onclick="enableEditFieldWithDelay()" 29 | ) Enable Edit Field with 5 seconds delay 30 | div( 31 | id="opstatus" 32 | ) --- 33 | 34 | script. 35 | function enableEditFieldWithDelay() 36 | { 37 | byid('inputField').disabled = true; 38 | setStatus('Input Disabled...'); 39 | setTimeout(()=>{ 40 | byid('inputField').disabled = false; 41 | setStatus('Input Enabled...'); 42 | },5000); 43 | } 44 | 45 | include footer.html 46 | 47 | -------------------------------------------------------------------------------- /views/DynamicID.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p Modern applications often generate dynamic IDs for elements. In this case ID is not a reliable attribute for using in element selector. By default many UI automation tools record IDs and this results in tests broken from the very beginning. An automation tool needs a way to instruct it to skip dynamic IDs when XPath is generated for an element. 11 | h4 Scenario 12 | ul 13 | li Record button click. 14 | li Then execute your test to make sure that ID is not used for button identification. 15 | h4 Playground 16 | button( 17 | type="button" 18 | class="btn btn-primary" 19 | id=buttonId 20 | ) Button with Dynamic ID 21 | 22 | include footer.html 23 | 24 | -------------------------------------------------------------------------------- /views/DynamicTable.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p Below you see a table where columns and rows change their position upon page reload. Values in cells are random. The table is based on DIVs with ARIA attributes. See WAI-ARIA table design pattern for details. 11 | h4 Scenario 12 | ul 13 | li For Chrome process get value of CPU load. 14 | li Compare it with value in the yellow label. 15 | h4 Playground 16 | div( 17 | role="table" 18 | aria-label=table.name 19 | aria-describedby="table_desc" 20 | ) 21 | div(id="table_desc") 22 | =table.desc 23 | div(role="rowgroup") 24 | div(role="row") 25 | - for (var c = 0; c < table.columns.length; c++) 26 | span(role="columnheader") 27 | =table.columns[c] 28 | div(role="rowgroup") 29 | - for (var r = 0; r < table.rows.length; r++) 30 | div(role="row") 31 | - for (var v = 0; v < table.rows[r].length; v++) 32 | span(role="cell") 33 | =table.rows[r][v] 34 | 35 | br 36 | p(class='bg-warning')="Chrome CPU: " + table.chromeCPU 37 | include footer.html 38 | 39 | -------------------------------------------------------------------------------- /views/HiddenLayers.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p Some applications use DOM caching techniques. For example, if a user follows a multi step process and each step requires filling data into a form then forms may be cached at the client side along the way. It allows to quickly navigate back and forward through the steps without requesting data from a server. When form is cached it just pushed on-top of z-order stack. It means that an element may be still present in the DOM tree but overlapped with another layer of elements. In this case it is important that a test does not interact with inactive elements becasue they are invisible to a user. 11 | h4 Scenario 12 | ul 13 | li Record button click and then duplicate the button click step in your test. 14 | li Execute the test to make sure that green button can not be hit twice. 15 | h4 Playground 16 | div( 17 | id="spa" 18 | ) 19 | div( 20 | class="spa-view" 21 | style="z-index: 1;" 22 | ) 23 | button( 24 | type="button" 25 | class="btn btn-success" 26 | id="greenButton" 27 | ) Button 28 | div 29 | br 30 | 31 | script. 32 | function ClickEventHandler(event) 33 | { 34 | if (event.target.id == "greenButton") 35 | { 36 | console.log("Green button pressed"); 37 | if ($("#blueButton").length) 38 | { 39 | $("#blueButton").parent().append("

User can not click green button in the current application state!

"); 40 | } 41 | $("#spa").append('
'); 42 | } 43 | else if (event.target.id == "blueButton") 44 | { 45 | console.log("Blue button pressed"); 46 | } 47 | } 48 | document.body.addEventListener('click', ClickEventHandler, true); 49 | 50 | include footer.html 51 | 52 | -------------------------------------------------------------------------------- /views/Home.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section(id="description") 8 | div(class="container") 9 | div(class="row") 10 | div(class="col-sm") 11 | h1(id="title") UI Test Automation
Playground 12 | blockquote( 13 | class="blockquote" 14 | id="citation" 15 | ) 16 | p(class="mb-0") Quality is not an act, it is a habit. 17 | footer(class="blockquote-footer") Aristotle 18 | div( 19 | class="alert alert-warning" 20 | role="alert" 21 | ) 22 | span The purpose of this website is to provide a platform for sharpening UI test automation skills. Use it to practice with your test automation tool. Use it to learn test automation techniques. 23 | p Different automation pitfalls appearing in modern web applications are described and emulated below. 24 | div(class="col-sm") 25 | img( 26 | src="/static/cube.png" 27 | class="img-fluid" 28 | alt="Responsive image" 29 | ) 30 | p(class="text-center") 31 | 32 | Rubik's Cube is licensed under 33 | CC 4.0 BY-NC 34 | 35 | 36 | 37 | section(id="overview") 38 | div(class="container") 39 | div(class="row") 40 | div(class="col-sm") 41 | h3 42 | a(href="/dynamicid") Dynamic ID 43 | p Make sure you are not recording dynamic IDs of elements 44 | div(class="col-sm") 45 | h3 46 | a(href="/classattr") Class Attribute 47 | p Check that class attribute based XPath is well formed 48 | div(class="col-sm") 49 | h3 50 | a(href="/hiddenlayers") Hidden Layers 51 | p Verify that your test does not interact with elements invisible because of z-order 52 | div(class="col-sm") 53 | h3 54 | a( 55 | href="/loaddelay" 56 | onclick="$('#spinner').show(); return true;" 57 | ) Load Delay 58 | i( 59 | id="spinner" 60 | class="fa fa-spinner fa-spin" 61 | style="display:none" 62 | ) 63 | p Ensure that a test is capable of waiting for a page to load 64 | div(class="row") 65 | div(class="col-sm") 66 | h3 67 | a(href="/ajax") AJAX Data 68 | p Some elements may appear on a page after loading data with AJAX request 69 | div(class="col-sm") 70 | h3 71 | a(href="/clientdelay") Client Side Delay 72 | p Some elements may appear after client-side time consuming JavaScript calculations 73 | div(class="col-sm") 74 | h3 75 | a(href="/click") Click 76 | p Event based click on an element may not always work 77 | div(class="col-sm") 78 | h3 79 | a(href="/textinput") Text Input 80 | p Entering text into an edit field may not have effect 81 | div(class="row") 82 | div(class="col-sm") 83 | h3 84 | a(href="/scrollbars") Scrollbars 85 | p Scrolling an element into view may be a tricky task 86 | div(class="col-sm") 87 | h3 88 | a(href="/dynamictable") Dynamic Table 89 | p Verify cell value in a dynamic table 90 | div(class="col-sm") 91 | h3 92 | a(href="/verifytext") Verify Text 93 | p Finding an element by displayed text has nuances 94 | div(class="col-sm") 95 | h3 96 | a(href="/progressbar") Progress Bar 97 | p Follow the progress of a lengthy process and continue upon completion 98 | div(class="row") 99 | div(class="col-sm") 100 | h3 101 | a(href="/visibility") Visibility 102 | p Check if element is visible on screen 103 | div(class="col-sm") 104 | h3 105 | a(href="/sampleapp") Sample App 106 | p Demo application with dynamically generated element attributes 107 | div(class="col-sm") 108 | h3 109 | a(href="/mouseover") Mouse Over 110 | p Placing mouse over an element may change DOM and make the element unavailable 111 | div(class="col-sm") 112 | h3 113 | a(href="/nbsp") Non-Breaking Space 114 | p Non-breaking space looks like a normal one on screen. It may lead to confusion when building XPath 115 | div(class="row") 116 | div(class="col-sm") 117 | h3 118 | a(href="/overlapped") Overlapped Element 119 | p Make element visible to enter text 120 | div(class="col-sm") 121 | h3 122 | a(href="/shadowdom") Shadow DOM 123 | p Look inside Shadow DOM component 124 | div(class="col-sm") 125 | h3 126 | a(href="/alerts") Alerts 127 | p Accept alerts, confirmations and prompts 128 | div(class="col-sm") 129 | h3 130 | a(href="/upload") File Upload 131 | p Upload files 132 | div(class="row") 133 | div(class="col-sm") 134 | h3 135 | a(href="/animation") Animated Button 136 | p Wait for animation to stop before clicking a button 137 | div(class="col-sm") 138 | h3 139 | a(href="/disabledinput") Disabled Input 140 | p Wait for edit field to become enabled 141 | div(class="col-sm") 142 | h3 143 | a(href="/autowait") Auto Wait 144 | p Wait for an element to become interactable 145 | div(class="col-sm") 146 | p   147 | include footer.html -------------------------------------------------------------------------------- /views/LoadDelay.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p Server response may often come with an unpredictable delay. So a test must be able to patiently wait for page loaded event from a browser. 11 | h4 Scenario 12 | ul 13 | li Navigate to Home page and record Load Delays link click and button click on this page. 14 | li Then play the test. It should wait until page is loaded. 15 | h4 Playground 16 | button( 17 | type="button" 18 | class="btn btn-primary" 19 | ) Button Appearing After Delay 20 | 21 | include footer.html -------------------------------------------------------------------------------- /views/MouseOver.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p Placing mouse over an element may lead to changes in the DOM tree. For example the element may be modified or replaced. It means if you keep a reference to the original element and will try to click on it - it may not work (stale element problem). 11 | p This puzzle complicates both recording and playback of a test. 12 | 13 | h4 Scenario 14 | ul 15 | li Record 2 consecutive link clicks. 16 | li Execute the test and make sure that click count is increasing by 2. 17 | h4 Playground 18 | div 19 | p The link below will be replaced and new title assigned to it when you place mouse over it. Click on it to increase the click count. 20 | a( 21 | title="Click me" 22 | class="text-primary" 23 | onmouseenter="linkActive(this)" 24 | ) Click me 25 | div 26 | p The link above clicked 0 times. 27 | div 28 | p The link below will be replaced with identical one when you place mouse over it. Click on it to increase the click count. 29 | a( 30 | title="Link Button" 31 | class="text-primary" 32 | onmouseenter="linkButtonActive(this)" 33 | ) Link Button 34 | div 35 | p The link above clicked 0 times. 36 | 37 | script. 38 | 39 | function linkActive(el) 40 | { 41 | var newEl = el.cloneNode(true); 42 | newEl.setAttribute("title", "Active Link"); 43 | newEl.setAttribute("onmouseenter", ""); 44 | newEl.setAttribute("onmouseleave", "linkInactive(this)"); 45 | newEl.setAttribute("onclick", "linkClicked(this)") 46 | newEl.setAttribute("class", "text-warning"); 47 | 48 | var parentNode = el.parentNode; 49 | parentNode.removeChild(el); 50 | parentNode.appendChild(newEl); 51 | } 52 | 53 | function linkInactive(el) 54 | { 55 | var newEl = el.cloneNode(true); 56 | newEl.setAttribute("title", el.innerText); 57 | newEl.setAttribute("onmouseenter", "linkActive(this)"); 58 | newEl.setAttribute("onmouseleave", ""); 59 | newEl.setAttribute("onclick", "") 60 | newEl.setAttribute("class", "text-primary"); 61 | 62 | var parentNode = el.parentNode; 63 | parentNode.removeChild(el); 64 | parentNode.appendChild(newEl); 65 | } 66 | 67 | function linkClicked(el) 68 | { 69 | var badge = document.getElementById("clickCount"); 70 | badge.innerText = parseInt(badge.innerText) + 1; 71 | } 72 | 73 | function linkButtonActive(el) 74 | { 75 | var newEl = el.cloneNode(true); 76 | newEl.setAttribute("onmouseenter", ""); 77 | newEl.setAttribute("onmouseleave", "linkButtonInactive(this)"); 78 | newEl.setAttribute("onclick", "linkButtonClicked(this)") 79 | newEl.setAttribute("class", "text-warning"); 80 | 81 | var parentNode = el.parentNode; 82 | parentNode.removeChild(el); 83 | parentNode.appendChild(newEl); 84 | } 85 | 86 | function linkButtonInactive(el) 87 | { 88 | var newEl = el.cloneNode(true); 89 | newEl.setAttribute("onmouseenter", "linkButtonActive(this)"); 90 | newEl.setAttribute("onmouseleave", ""); 91 | newEl.setAttribute("onclick", "") 92 | newEl.setAttribute("class", "text-primary"); 93 | 94 | var parentNode = el.parentNode; 95 | parentNode.removeChild(el); 96 | parentNode.appendChild(newEl); 97 | } 98 | 99 | function linkButtonClicked(el) 100 | { 101 | var badge = document.getElementById("clickButtonCount"); 102 | badge.innerText = parseInt(badge.innerText) + 1; 103 | } 104 | 105 | 106 | include footer.html 107 | -------------------------------------------------------------------------------- /views/Nbsp.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p There are cases in test automation when something should obviously work but for some reason it does not. Searching for an element by its text is one of those cases. Text caption may contain non-breaking spaces that have no visual difference from generic spaces. 11 | h4 Scenario 12 | ul 13 | li Use the following xpath to find the button in your test: 14 | br 15 | br 16 | pre //button[text()='My Button'] 17 | li Notice that the XPath does not work. Change the space between 'My' and 'Button' to a non-breaking space. This time the XPath should be valid. 18 | h4 Playground 19 | button( 20 | type="button" 21 | class="btn btn-primary" 22 | id=buttonId 23 | ) My Button 24 | 25 | include footer.html 26 | 27 | -------------------------------------------------------------------------------- /views/Overlapped.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p Entering text to a partially visible element may require scrolling it into view. 11 | h4 Scenario 12 | ul 13 | li Record setting text into the Name input field (scroll element before entering the text). 14 | li Then execute your test to make sure that the text was entered correctly. 15 | h4 Playground 16 |
17 |
18 | 19 |

20 | 21 |

22 | 23 |
24 |
25 |
26 | 27 | 28 | script. 29 | var nameElement = document.querySelector('#name'); 30 | 31 | nameElement.addEventListener('input', (event) => { 32 | 33 | var crect = nameElement.getBoundingClientRect(); 34 | 35 | var centerX = crect.left + (crect.width >> 1); 36 | var centerY = crect.top + (crect.height >> 1); 37 | var elementAtPoint = document.elementFromPoint(centerX, centerY); 38 | if(!nameElement.isSameNode(elementAtPoint)) 39 | { 40 | nameElement.value = ""; 41 | } 42 | }); 43 | 44 | 45 | include footer.html 46 | -------------------------------------------------------------------------------- /views/ProgressBar.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p A web application may use a progress bar to reflect state of some lengthy process. Thus a test may need to read the value of a progress bar to determine if it is time to proceed or not. 11 | h4 Scenario 12 | ul 13 | li Create a test that clicks Start button and then waits for the progress bar to reach 75%. Then the test should click Stop. The less the differnce between value of the stopped progress bar and 75% the better your result. 14 | h4 Playground 15 | button( 16 | type="button" 17 | class="btn btn-primary btn-test" 18 | id="startButton" 19 | onclick="Start()" 20 | ) Start 21 | button( 22 | type="button" 23 | class="btn btn-info btn-test" 24 | id="stopButton" 25 | onclick="Stop()" 26 | ) Stop 27 | br 28 | br 29 | div(class="progress") 30 | div(class="progress-bar bg-info" 31 | id="progressBar" 32 | role="progressbar" 33 | style="width: 25%;" 34 | aria-valuenow="25" 35 | aria-valuemin="0" 36 | aria-valuemax="100") 25% 37 | div(id="content") 38 | p(id="result") Result: n/a 39 | 40 | script. 41 | var ratio = 25; 42 | var started = false; 43 | var startTime = 0; 44 | var endTime = 0; 45 | var delay = 100; 46 | 47 | function _setResult() 48 | { 49 | var label = document.getElementById("result"); 50 | label.innerHTML = "Result: " + (ratio == 25 ? "n/a" : ratio - 75) + ", duration: " + (endTime == 0 ? "n/a" : (endTime - startTime)); 51 | } 52 | 53 | function Start() 54 | { 55 | var random = "" + (new Date()).getTime(); 56 | random = parseInt(random.substr(random.length - 3, 3)); 57 | delay = Math.floor(random/2); 58 | 59 | startTime = new Date(); 60 | endTime = 0; 61 | started = true; 62 | ratio = 25; 63 | 64 | var progressBar = document.getElementById("progressBar"); 65 | 66 | _setResult(); 67 | _setProgress(); 68 | 69 | function _setProgress() 70 | { 71 | progressBar.setAttribute("style", "width: " + ratio + "%"); 72 | progressBar.setAttribute("aria-valuenow", ratio); 73 | progressBar.innerHTML = ratio + "%"; 74 | endTime = new Date(); 75 | } 76 | 77 | function _makeProgress() 78 | { 79 | if (!started) 80 | return; 81 | ratio += 1; 82 | _setProgress(); 83 | if (ratio < 100) 84 | { 85 | setTimeout(_makeProgress, delay); 86 | } 87 | } 88 | 89 | setTimeout(_makeProgress, 1000); 90 | } 91 | 92 | function Stop() 93 | { 94 | started = false; 95 | _setResult(); 96 | } 97 | 98 | include footer.html 99 | 100 | -------------------------------------------------------------------------------- /views/Resources.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | h4 Learning 11 | ul 12 | li 13 | a(href="https://www.w3schools.com") w3schools.com 14 | li 15 | a(href="https://developer.mozilla.org/en-US/") MDN 16 | li 17 | a(href="https://github.com/zeeshanu/learn-regex") Learn regex the easy way 18 | li 19 | a(href="https://devhints.io/") devhints.io 20 | h4 Standards 21 | ul 22 | li 23 | a(href="https://www.w3.org/") W3C 24 | h4 Articles 25 | ul 26 | li 27 | a(href="https://martinfowler.com/bliki/TestPyramid.html") Test Pyramid 28 | li 29 | a(href="https://testing.googleblog.com/2017/04/where-do-our-flaky-tests-come-from.html") Where do our flaky tests come from? 30 | 31 | h4 Community 32 | ul 33 | li 34 | a(href="https://ministryoftesting.com/") Ministry of Testing 35 | li 36 | a(href="https://www.utest.com") uTest 37 | li 38 | a(href="https://www.softwaretestinghelp.com") Software Testing Help 39 | li 40 | a(href="https://dzone.com/") DZone 41 | li 42 | a(href="https://stackoverflow.com/") StackOverflow 43 | 44 | include footer.html 45 | 46 | -------------------------------------------------------------------------------- /views/SampleApp.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p Fill in and submit the form. For successfull login use any non-empty user name and `pwd` as password. 11 | div(class="row") 12 | br 13 | div(class="col-sm-4") 14 | label( 15 | id="loginstatus" 16 | class="text-info" 17 | ) User logged out. 18 | div(class="row") 19 | div(class="col-sm-4") 20 | input( 21 | type="text" 22 | class="form-control" 23 | placeholder="User Name" 24 | name="UserName" 25 | id="username" 26 | ) 27 | div(class="row") 28 | div(class="col-sm-4") 29 | input( 30 | type="password" 31 | class="form-control" 32 | placeholder="********" 33 | name="Password" 34 | id="password" 35 | ) 36 | div(class="row") 37 | div(class="col-sm-4") 38 | br 39 | button( 40 | type="button" 41 | class="btn btn-primary" 42 | id="login" 43 | onclick="Login()" 44 | ) Log In 45 | 46 | script. 47 | 48 | function guid() { 49 | function s4() { 50 | return Math.floor((1 + Math.random()) * 0x10000) 51 | .toString(16) 52 | .substring(1); 53 | } 54 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + 55 | s4() + '-' + s4() + s4() + s4(); 56 | } 57 | 58 | var userNameId = guid(); 59 | var un = document.getElementById("username"); 60 | un.id = userNameId; 61 | un.value = ""; 62 | 63 | var passwordId = guid(); 64 | var pwd = document.getElementById("password"); 65 | pwd.id = passwordId; 66 | pwd.value = ""; 67 | 68 | var loggedIn = false; 69 | 70 | function Login() 71 | { 72 | var ls = document.getElementById("loginstatus"); 73 | var un = document.getElementById(userNameId); 74 | var pwd = document.getElementById(passwordId); 75 | var login = document.getElementById("login"); 76 | 77 | if (loggedIn) 78 | { 79 | ls.innerHTML = "User logged out."; 80 | ls.className = "text-info"; 81 | un.value = ""; 82 | pwd.value = ""; 83 | login.innerText = "Log In"; 84 | loggedIn = false; 85 | } 86 | else 87 | { 88 | if (pwd.value == "pwd" && un.value) 89 | { 90 | ls.innerHTML = "Welcome, " + un.value + "!"; 91 | ls.className = "text-success"; 92 | login.innerText = "Log Out"; 93 | loggedIn = true; 94 | } 95 | else 96 | { 97 | ls.innerHTML = "Invalid username/password"; 98 | ls.className = "text-danger"; 99 | un.value = ""; 100 | pwd.value = ""; 101 | } 102 | } 103 | } 104 | 105 | include footer.html 106 | -------------------------------------------------------------------------------- /views/Scrollbars.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p An application may use native or custom scrollbars and some elements may be out of view. A test scenario may require to ensure that an element is visible on screen and this may require scrolling. 11 | h4 Scenario 12 | ul 13 | li Find a button in the scroll view and record button click. 14 | li Update your test to automatically scroll the button into a visible area. 15 | li Then execute your test to make sure it works. 16 | h4 Playground 17 | div(style="height:150px;overflow-y: scroll;width:300px;overflow-x:scroll") 18 | div(style="height:300px;width:600px") 19 | div(style="height:150px;") 20 | table 21 | tr 22 | td(style="width:300px;") 23 | td 24 | button( 25 | type="button" 26 | class="btn btn-primary" 27 | id="hidingButton" 28 | ) Hiding Button 29 | 30 | include footer.html 31 | 32 | -------------------------------------------------------------------------------- /views/ShadowDom.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p This is a page with a Shadow DOM component guid-generator. Using it one can generate a guid and copy it to the clipboard. 11 | h4 Scenario 12 | ul 13 | li Create a test that clicks on and then on buttons. This sequence of steps generates new guid and copies it to the clipboard. 14 | li Add an assertion step to your test to compare the value from the clipboard with the value of the input field. 15 | li Then execute the test to make sure that the assertion step is not failing. 16 | h4 Playground 17 | h6 GUID Generator: 18 | guid-generator 19 | 20 | script. 21 | class GuidGenerator extends HTMLElement { 22 | constructor() { 23 | // Always call super first in constructor 24 | super(); 25 | 26 | function guid() { 27 | function s4() { 28 | return Math.floor((1 + Math.random()) * 0x10000) 29 | .toString(16) 30 | .substring(1); 31 | } 32 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + 33 | s4() + '-' + s4() + s4() + s4(); 34 | } 35 | 36 | // Create a shadow root 37 | const shadow = this.attachShadow({mode: 'open'}); 38 | 39 | // Create spans 40 | const editField = document.createElement('input'); 41 | editField.setAttribute('class', 'edit-field'); 42 | editField.setAttribute('id', 'editField'); 43 | 44 | const buttonGenerate = document.createElement('button'); 45 | buttonGenerate.setAttribute('class', 'button-generate'); 46 | buttonGenerate.setAttribute('id', 'buttonGenerate'); 47 | buttonGenerate.onclick = function() 48 | { 49 | editField.value = guid(); 50 | } 51 | 52 | const generateIcon = document.createElement('i'); 53 | generateIcon.setAttribute('class', 'fa fa-cog'); 54 | buttonGenerate.appendChild(generateIcon); 55 | 56 | const buttonCopy = document.createElement('button'); 57 | buttonCopy.setAttribute('class', 'button-copy'); 58 | 59 | const copyIcon = document.createElement('i'); 60 | copyIcon.setAttribute('class', 'fa fa-clone'); 61 | buttonCopy.appendChild(copyIcon); 62 | buttonCopy.setAttribute('id', 'buttonCopy'); 63 | buttonCopy.onclick = function() 64 | { 65 | navigator.clipboard.writeText(editField.value).then(function() { 66 | console.log("Copied to clipboard successfully!"); 67 | }, function() { 68 | console.error("Unable to write to clipboard. :-("); 69 | }); 70 | } 71 | 72 | // Create some CSS to apply to the shadow dom 73 | const style = document.createElement('style'); 74 | style.textContent = ` 75 | .edit-field { 76 | width: 300px; 77 | } 78 | 79 | .button-generate { 80 | width: 30px; 81 | height: 20px; 82 | } 83 | 84 | .button-copy { 85 | width: 30px; 86 | height: 20px; 87 | } 88 | 89 | `; 90 | 91 | let link = document.createElement('link'); 92 | link.setAttribute('rel', 'stylesheet'); 93 | link.setAttribute('href', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'); 94 | 95 | // Attach the created elements to the shadow dom 96 | shadow.appendChild(link); 97 | shadow.appendChild(style); 98 | shadow.appendChild(editField); 99 | shadow.appendChild(buttonGenerate); 100 | shadow.appendChild(buttonCopy); 101 | } 102 | } 103 | 104 | // Define the new element 105 | customElements.define('guid-generator', GuidGenerator); 106 | 107 | 108 | include footer.html 109 | -------------------------------------------------------------------------------- /views/TextInput.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p Entering text with physical keyboard can be different from sending DOM events to an element. This page is specifically desined to illustrate this problem. There are cases when attempts to set a text via DOM events lead to nowhere and the only way to proceed is to emulate real keyboard input at OS level. 11 | h4 Scenario 12 | ul 13 | li Record setting text into the input field and pressing the button. 14 | li Then execute your test to make sure that the button name is changing. 15 | h4 Playground 16 |
17 |
18 | label(for="newButtonName") Set New Button Name 19 | input( 20 | type="text" 21 | class="form-control" 22 | placeholder="MyButton" 23 | id="newButtonName" 24 | ) 25 | br 26 | button( 27 | type="button" 28 | class="btn btn-primary" 29 | id="updatingButton" 30 | ) Button That Should Change it's Name Based on Input Value 31 | 32 | script. 33 | function ClickEventHandler(event) 34 | { 35 | if (event.target.id == "updatingButton") 36 | { 37 | if (document.NewButtonNameC && document.NewButtonNameC == document.NewButtonNameI) 38 | { 39 | event.target.textContent = document.NewButtonNameC; 40 | } 41 | } 42 | } 43 | 44 | function InputEventHandler(event) 45 | { 46 | if (event.target.id == "newButtonName") 47 | { 48 | document.NewButtonNameI = event.target.value; 49 | } 50 | } 51 | 52 | function ChangeEventHandler(event) 53 | { 54 | if (event.target.id == "newButtonName") 55 | { 56 | document.NewButtonNameC = event.target.value; 57 | } 58 | } 59 | 60 | document.body.addEventListener('click', ClickEventHandler, true); 61 | document.body.addEventListener('input', InputEventHandler, true); 62 | document.body.addEventListener('change', ChangeEventHandler, true); 63 | document.NewButtonName = null; 64 | 65 | include footer.html 66 | -------------------------------------------------------------------------------- /views/Upload.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p Modern web applications often include file upload functionality, enabling users to easily share and manage documents, images, and other types of files directly from their devices, enhancing user interaction and content management. 11 | h4 Scenario 12 | ul 13 | li Attach a file via drag&drop. 14 | li Attach a file using `Browse files` button 15 | h4 Playground 16 | iframe(src="/static/upload.html" width="800" height="500" frameBorder="0") 17 | 18 | include footer.html -------------------------------------------------------------------------------- /views/VerifyText.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p In general inner text of a DOM element is different from displayed on screen. Browsers normalize text upon rendering, but DOM nodes contain text as it is in HTML markup. For example a browser may show the text as 11 | div 12 | p 13 | span(class="badge-secondary") Hello UserName! 14 | p and the text of the DOM element can be 15 | div. 16 |

17 | 18 |                 
19 |     Hello UserName!
20 |          21 |
22 |

23 | p Take this fact into account when searching for an element using it's text value. 24 | div(class="container") 25 | div(class="row") 26 | div(class="col-sm") Does not work 27 | div(class="col-sm") Works 28 | div(class="row") 29 | div(class="col-sm") 30 | code //span[.='Welcome UserName!'] 31 | div(class="col-sm") 32 | code //span[normalize-space(.)='Welcome UserName!'] 33 | br 34 | h4 Scenario 35 | ul 36 | li. 37 | Create a test that finds an element with Welcome... text. 38 | h4 Playground 39 | include text.html 40 | include footer.html 41 | 42 | -------------------------------------------------------------------------------- /views/Visibility.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | include head.pug 4 | body 5 | include navbar.pug 6 | 7 | section 8 | div(class="container") 9 | h3=title 10 | p Checking if element is visible on screen may be a non trivial task. 11 | ul 12 | li An element may be removed (simplest case), 13 | li it may have zero height or width, 14 | li it may be covered by another element, 15 | li it may be hidden using styles: opacity: 0, visibility: hidden, display: none, 16 | li or moved offscreen. 17 | h4 Scenario 18 | ul 19 | li Learn locators of all buttons. 20 | li In your testing scenario press Hide button. 21 | li Determine if other buttons visible or not. 22 | h4 Playground 23 | table( 24 | width="100%" 25 | border="1" 26 | ) 27 | tr 28 | td(width="25%") 29 | button( 30 | type="button" 31 | class="btn btn-primary" 32 | id="hideButton" 33 | onclick="HideButtons()" 34 | ) Hide 35 | td(width="25%") 36 | button( 37 | type="button" 38 | class="btn btn-danger" 39 | id="removedButton" 40 | ) Removed 41 | td(width="25%") 42 | button( 43 | type="button" 44 | class="btn btn-warning" 45 | id="zeroWidthButton" 46 | ) Zero Width 47 | td(width="25%") 48 | button( 49 | type="button" 50 | class="btn btn-success" 51 | id="overlappedButton" 52 | ) Overlapped 53 | div( 54 | id="hidingLayer" 55 | ) 56 | tr 57 | td(width="25%") 58 | button( 59 | type="button" 60 | class="btn btn-info" 61 | id="transparentButton" 62 | ) Opacity 0 63 | td(width="25%") 64 | button( 65 | type="button" 66 | class="btn btn-info" 67 | id="invisibleButton" 68 | ) Visibility Hidden 69 | td(width="25%") 70 | button( 71 | type="button" 72 | class="btn btn-info" 73 | id="notdisplayedButton" 74 | ) Display None 75 | td(width="25%") 76 | button( 77 | type="button" 78 | class="btn btn-info" 79 | id="offscreenButton" 80 | ) Offscreen 81 | 82 | script. 83 | function HideButtons() 84 | { 85 | // Remove button 86 | $("#removedButton").remove(); 87 | 88 | // Zero width button 89 | $("#zeroWidthButton").addClass("zerowidth"); 90 | 91 | // Button overlapped with another element 92 | $('#hidingLayer').css('position', 'absolute'); 93 | $('#hidingLayer').css('background-color', 'white'); 94 | var gb = $('#overlappedButton'); 95 | var width = gb[0].clientWidth + 2; 96 | var height = gb[0].clientHeight + 2; 97 | $('#hidingLayer').css('width', width); 98 | $('#hidingLayer').css('height', height); 99 | var left = gb.position().left; 100 | var top = gb.position().top; 101 | $('#hidingLayer').css('left', left); 102 | $('#hidingLayer').css('top', top); 103 | 104 | // Styled hidden 105 | $("#transparentButton").css("opacity", 0); 106 | $("#invisibleButton").css("visibility", "hidden"); 107 | $("#notdisplayedButton").css("display", "none"); 108 | $("#offscreenButton").addClass("offscreen"); 109 | } 110 | 111 | include footer.html 112 | 113 | -------------------------------------------------------------------------------- /views/footer.html: -------------------------------------------------------------------------------- 1 |
2 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /views/head.pug: -------------------------------------------------------------------------------- 1 | head 2 | meta(charset="utf-8") 3 | meta( 4 | name="viewport" 5 | content="width=device-width, initial-scale=1, shrink-to-fit=no" 6 | ) 7 | link( 8 | rel="stylesheet" 9 | href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" 10 | integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" 11 | crossorigin="anonymous" 12 | ) 13 | link( 14 | rel="stylesheet" 15 | href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" 16 | ) 17 | link( 18 | rel="stylesheet" 19 | href="/static/prism.css" 20 | ) 21 | link( 22 | rel="stylesheet" 23 | href="/static/style.css" 24 | ) 25 | title=title 26 | script. 27 | function byid(id) 28 | { 29 | return document.getElementById(id); 30 | } 31 | 32 | function setStatus(txt) 33 | { 34 | byid("opstatus").innerText = txt; 35 | } 36 | -------------------------------------------------------------------------------- /views/navbar.pug: -------------------------------------------------------------------------------- 1 | nav(class="navbar navbar-expand-lg navbar-light bg-light") 2 | a( 3 | class="navbar-brand" 4 | href="/" 5 | ) UITAP 6 | button( 7 | class="navbar-toggler" 8 | type="button" 9 | data-toggle="collapse" 10 | data-target="#navbarSupportedContent" 11 | aria-controls="navbarSupportedContent" 12 | aria-expanded="false" 13 | aria-label="Toggle navigation" 14 | ) 15 | span(class="navbar-toggler-icon") 16 | div( 17 | class="collapse navbar-collapse" 18 | id="navbarSupportedContent" 19 | ) 20 | ul(class="navbar-nav mr-auto") 21 | li( 22 | class=homeActive ? "nav-item active" : "nav-item" 23 | ) 24 | a( 25 | class="nav-link" 26 | href="/home" 27 | ) Home 28 | li( 29 | class=resourceActive ? "nav-item active" : "nav-item" 30 | ) 31 | a( 32 | class="nav-link" 33 | href="/resources" 34 | ) Resources 35 | -------------------------------------------------------------------------------- /views/text.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | Welcome UserName! 5 | 6 | 7 |
--------------------------------------------------------------------------------