├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── Gruntfile.js ├── LICENSE-MIT ├── README.md ├── examples └── pagetitle.js ├── lib ├── dalek.js └── dalek │ ├── actions.js │ ├── assertions.js │ ├── config.js │ ├── driver.js │ ├── host.js │ ├── remote.js │ ├── reporter.js │ ├── suite.js │ ├── timer.js │ ├── unit.js │ └── uuid.js ├── package.json └── test ├── lib_dalek_TEST.js ├── lib_dalek_actions_TEST.js ├── lib_dalek_actions_screenshots_TEST.js ├── lib_dalek_assertions_TEST.js ├── lib_dalek_config_TEST.js ├── lib_dalek_driver_TEST.js ├── lib_dalek_reporter_TEST.js ├── lib_dalek_suite_TEST.js ├── lib_dalek_timer_TEST.js ├── lib_dalek_unit_TEST.js ├── lib_dalek_uuid_TEST.js └── mock ├── Dalekfile.coffee ├── Dalekfile.js ├── Dalekfile.json ├── Dalekfile.json5 └── Dalekfile.yml /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | package-canary.json 15 | index-canary.js 16 | npm-debug.log 17 | report.zip 18 | 19 | /coverage/ 20 | /node_modules/ 21 | /report/ 22 | .DS_STORE 23 | .idea/ 24 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": false, 4 | "esnext": false, 5 | "bitwise": false, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "expr": true, 22 | "globals": { 23 | "describe": false, 24 | "it": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "after": false, 28 | "afterEach": false 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_script: 5 | - npm install -g grunt-cli 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ### 0.0.8 (2014-11-28) 3 | 4 | 5 | #### Bug Fixes 6 | 7 | * **actions:** 8 | * before & after test execution ([d0ce489c](http://github.com/dalekjs/dalek/commit/d0ce489cb10f778df0ec9bc1576862827643a370)) 9 | * better screenshot handling ([ed50a6fd](http://github.com/dalekjs/dalek/commit/ed50a6fdc8b90b01566a2847ca9087689d42d777)) 10 | * **build:** fix build process ([ff45eb3b](http://github.com/dalekjs/dalek/commit/ff45eb3bddd348aa65f3a17c8c440b13a3dd4292)) 11 | * **tests:** removed undefined variable ([1ef624da](http://github.com/dalekjs/dalek/commit/1ef624daf6de321996daba10557a50e3e5a7dece)) 12 | 13 | 14 | #### Features 15 | 16 | * **suite:** bind this to the current test ([37157057](http://github.com/dalekjs/dalek/commit/3715705799925dde865c36274ef05d7c7129c0da)) 17 | 18 | 19 | 20 | ### v0.0.7 (2013-10-28) 21 | 22 | 23 | #### Features 24 | 25 | * **config:** Moved config.js form dalek-internal-config into dalek main module ([a0dadca5](http://github.com/dalekjs/dalek/commit/a0dadca50282dddd52758051cb95a77ef0309894)) 26 | * **core:** added remote runner/host capabilities ([77ed1ece](http://github.com/dalekjs/dalek/commit/77ed1ecea5f5b8b2556cbe7606291cb44abda37c)) 27 | 28 | 29 | ### v0.0.6 (2013-10-15) 30 | 31 | #### Features 32 | 33 | * **core:** 34 | * bumping dependencies ([6b439aa](https://github.com/dalekjs/dalek/commit/6b439aabbe5f1659e22a34a6c578ac5389000788)) 35 | 36 | #### Bug Fixes 37 | 38 | * **core:** 39 | * Added fix for issue #1 of dalek-internal-config ([7ca7df5](https://github.com/dalekjs/dalek/commit/7ca7df54cc44ccc91ae2d8b797d915962c857042)) 40 | 41 | 42 | ### v0.0.5 (2013-09-26) 43 | 44 | #### Features 45 | 46 | * **test:** 47 | * improve testharness ([3c98bf9](https://github.com/dalekjs/dalek/commit/3c98bf919e4a9c2d23c1551b8e8690acf46290e0)) 48 | * improve testharness ([c890560](https://github.com/dalekjs/dalek/commit/c89056024957c855fe7c856b7c1dfe2c399c85b5)) 49 | 50 | #### Bug Fixes 51 | 52 | * **core:** 53 | * fix issue with reload() not working ([b221e77](https://github.com/dalekjs/dalek/commit/b221e77b6f5f27af74403330cce7f623164852a0)) 54 | 55 | 56 | ### v0.0.4 (2013-09-25) 57 | 58 | #### Features 59 | 60 | * **core:** 61 | * bumping console reporter dependency ([6325a7a](https://github.com/dalekjs/dalek/commit/6325a7a0f04c5fda7cac1c6e172453fdf59530d2)) 62 | 63 | 64 | ### v0.0.3 (2013-09-25) 65 | 66 | #### Features 67 | 68 | * **core:** 69 | * bumping phantom dependency ([b173fce](https://github.com/dalekjs/dalek/commit/b173fce963275102b42ce9ca322fb797981c373e)) 70 | 71 | 72 | ### v0.0.2 (2013-09-25) 73 | 74 | #### Features 75 | 76 | * **core:** 77 | * bumping dependencies ([5bd3a7b](https://github.com/dalekjs/dalek/commit/5bd3a7be590ed58e3972c6ae6aab0731567a44e5)) 78 | 79 | 80 | ### v0.0.1 (2013-09-23) 81 | 82 | #### Bug Fixes 83 | 84 | * **gruntfile:** 85 | * fix canary builds ([ccc2045](https://github.com/dalekjs/dalek/commit/ccc20454e9d4dde64b6ecdc1b367ebdb9dd41f5e)) 86 | 87 | * **typo:** 88 | * fix typo in README.md ([e37f0a1](https://github.com/dalekjs/dalek/commit/e37f0a1c6620def7d966429bbfb3537866ac0753)) 89 | 90 | #### Features 91 | 92 | * **gruntfile:** 93 | * added build/release automation tools ([92e2b6c](https://github.com/dalekjs/dalek/commit/92e2b6ca43b93db52d11ce259edd16067cedf69a)) 94 | * automatically generate CONTRIBUTORS file ([3b518cf](https://github.com/dalekjs/dalek/commit/3b518cf3e5b22aaf7d8426aa0af9ae1a44c4c093)) 95 | 96 | * **core:** 97 | * added proper Error handling for runtime exceptions ([c995bcd](https://github.com/dalekjs/dalek/commit/c995bcd6e3f89764d26c25efe4e73f068ac45ad7)) -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | DalekJS has a few very specific guidelines in addition 2 | to some of the standard guidelines that Github and open 3 | source projects in general recommend. These guidelines 4 | are here to facilitate your contribution and streamline 5 | the process of getting the changes merged in and released. 6 | 7 | If you don't follow these guidelines, we'll still work 8 | with you to get the changes in. Any contribution you can 9 | make will help tremendously. Following these guidelines 10 | will help to streamline the pull request and change 11 | submission process. 12 | 13 | ## Documentation Fixes 14 | 15 | If you notice any problems with any documentation, please 16 | fix it and we'll get it merged as soon as we can. For 17 | small things like typos and grammar (which we know I'm 18 | terrible with), just click the "Edit this file" button 19 | and send in the pull request for the fix. For larger 20 | changes and big swaths of documentation changes, a regular 21 | pull request as outlined below is more appropriate. 22 | 23 | ## Pull Requests 24 | 25 | See [Github's documentation for pull requests](https://help.github.com/articles/using-pull-requests). 26 | 27 | Pull requests are by far the best way to contribute to 28 | DalekJS. Any time you can send us a pull request with 29 | the changes that you want, we will have an easier time 30 | seeing what you are trying to do. But a pull request in 31 | itself is not usually sufficient. There needs to be some 32 | context and purpose with it, and it should be done 33 | against a specific branch. 34 | 35 | ## General Submission Guidelines 36 | 37 | These guidelines are generally applicable whether or not 38 | you are submitting a bug or a pull request. Please try to 39 | include as much of this information as possible with any 40 | submission. 41 | 42 | ### Version Numbers 43 | 44 | In order to best help out with bugs, we need to know the 45 | following information in your bug submission: 46 | 47 | * DalekJS version # 48 | * Operating System / version # 49 | * Browser and version # 50 | 51 | Including this information in a submission will help 52 | us to test the problem and ensure that the bug is 53 | both reproduced and corrected on the platforms / versions 54 | that you are having issues with. 55 | 56 | ### Provide A Meaningful Description 57 | 58 | It doesn't matter how beautiful and "obvious" your fix is. 59 | We have 10,000,000,000 things floating around the project 60 | at any given moment and we will not immediately understand 61 | why you are making changes. 62 | 63 | Given that, it is very important to provide a meaningful 64 | description with your pull requests that alter any code. 65 | A good format for these descriptions will include three things: 66 | 67 | 1. Why: The problem you are facing (in as much detail as is 68 | necessary to describe the problem to someone who doesn't 69 | know anything about the system you're building) 70 | 71 | 2. What: A summary of the proposed solution 72 | 73 | 3. How: A description of how this solution solves the problem, 74 | in more detail than item #2 75 | 76 | 4. Any additional discussion on possible problems this might 77 | introduce, questions that you have related to the changes, etc. 78 | 79 | Without at least the first 2 items in this list, we won't 80 | have any clue why you're changing the code. The first thing 81 | we'll ask, then, is that you add that information. 82 | 83 | ### Create A Topic Branch For Your Work 84 | 85 | The work you are doing for your pull request should not be 86 | done in the master branch of your forked repository. Create 87 | a topic branch for your work. This allows you to isolate 88 | the work you are doing from other changes that may be happening. 89 | 90 | Github is a smart system, too. If you submit a pull request 91 | from a topic branch and we ask you to fix something, pushing 92 | a change to your topic branch will automatically update the 93 | pull request. 94 | 95 | ### Isolate Your Changes For The Pull Request 96 | 97 | See the previous item on creating a topic branch. 98 | 99 | If you don't use a topic branch, we may ask you to re-do your 100 | pull request on a topic branch. If your pull request contains 101 | commits or other changes that are not related to the pull 102 | request, we will ask you to re-do your pull request. 103 | 104 | ### Submit Specs With Your Pull Request 105 | 106 | Whenever possible, submit the specs (unit tests) that 107 | correspond to your pull request. 108 | 109 | I would rather see a pull request that is nothing but a 110 | failing spec, than see a large change made to the real 111 | code with no test to support the change. 112 | 113 | In fact... 114 | 115 | ## Submit A Failing Spec If You Don't Know How To Fix The Problem 116 | 117 | If you are stuck in a scenario that fails in your app, 118 | but you don't know how to fix it, submit a failing spec 119 | to show the failing scenario. Follow the guidelines for 120 | pull request submission, but don't worry about fixing the 121 | problem. A failing spec to show that a problem exists is 122 | a very very very helpful pull request for us. 123 | 124 | We'll even accept a failing test pasted in to the ticket 125 | description instead of a pull request. That would at 126 | least get us started on creating the failing test in the code. 127 | 128 | ## Don't Be A Troll 129 | 130 | It is very sad that we need to include this section of 131 | the contribution guidelines... 132 | 133 | If you are running in to a scenario with a problem, don't 134 | be a troll. Comment like "does DalekJS even have tests?" 135 | are not useful, funny or constructive. In fact, it may get 136 | you blocked and reported for abuse to Github. 137 | 138 | Submit a useful comment describing the scenario that is 139 | having an issue. Show us a failing test. Show us some 140 | code that is not behaving the way the documentation says 141 | it should. Be useful and work with us to fix the problem. 142 | 143 | We're all for criticism and tearing apart DalekJS for 144 | the problems it has. Do it in a constructive and helpful 145 | manner: "There isn't a test for this scenario. Here's a 146 | rough idea for one that shows the problem." Tell us why 147 | you don't like DalekJS. Tell us that someone else is 148 | building something better and why that other thing fits 149 | your scenario and needs better than DalekJS does. Just 150 | do it in a manner that allows us to learn from your 151 | experiences, instead of reacting to you being a troll 152 | (likely causing us to get defensive and miss an opportunity 153 | to learn something). 154 | 155 | ----------- 156 | All credit for this goes to @derickbaily, he is not involved in this project anyhow, 157 | but made this nice piece up for backbone.marionette. 158 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | Ain Tohvri 2 | Altiss 3 | Dan Woodward 4 | Jev Zelenkov 5 | Johannes Heimbach 6 | JulianLeviston 7 | Krister Kari 8 | Mathias Schaefer (molily) 9 | Niall O'Higgins 10 | Paulo Pires 11 | Peter Dave Hello 12 | RichCrook 13 | Robin Böhm 14 | Rodney Rehm 15 | Ryan Zec 16 | Sebastian Golasch 17 | Sebastian Hardt 18 | Shahar Roth 19 | Skachov, Oleksandr 20 | Vladimir Makhaev 21 | asciidisco 22 | deedubbleyoo 23 | gskachkov 24 | mdix 25 | ryanzec 26 | ryanzec 27 | stuartlangridge 28 | waffle.io -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 'use strict'; 3 | 4 | // check task runtime 5 | require('time-grunt')(grunt); 6 | 7 | // load generic configs 8 | var configs = require('dalek-build-tools'); 9 | 10 | // project config 11 | grunt.initConfig({ 12 | 13 | // load module meta data 14 | pkg: grunt.file.readJSON('package.json'), 15 | 16 | // define a src set of files for other tasks 17 | src: { 18 | lint: ['Gruntfile.js', 'lib/**/*.js', 'test/*.js'], 19 | complexity: ['lib/**/*.js'], 20 | test: ['test/*TEST.js'], 21 | src: ['lib/dalek.js'] 22 | }, 23 | 24 | // clean automatically generated helper files & docs 25 | clean: configs.clean, 26 | 27 | // speed up build by defining concurrent tasks 28 | concurrent: configs.concurrent, 29 | 30 | // linting 31 | jshint: configs.jshint, 32 | 33 | // testing 34 | mochaTest: configs.mocha, 35 | 36 | // code metrics 37 | complexity: configs.complexity, 38 | plato: configs.plato(grunt.file.readJSON('.jshintrc')), 39 | 40 | // api docs 41 | yuidoc: configs.yuidocs(), 42 | 43 | // up version, tag & commit 44 | bump: configs.bump({ 45 | pushTo: 'git@github.com:dalekjs/dalek.git', 46 | files: ['package.json', 'CONTRIBUTORS.md', 'CHANGELOG.md'] 47 | }), 48 | 49 | // generate contributors file 50 | contributors: configs.contributors, 51 | 52 | // compress artifacts 53 | compress: configs.compress, 54 | 55 | // prepare files for grunt-plato to 56 | // avoid error messages (weird issue...) 57 | preparePlato: { 58 | options: { 59 | folders: [ 60 | 'coverage', 61 | 'report', 62 | 'report/coverage', 63 | 'report/complexity', 64 | 'report/complexity/files', 65 | 'report/complexity/files/_js', 66 | 'report/complexity/files/_actions_js', 67 | 'report/complexity/files/_assertions_js', 68 | 'report/complexity/files/_reporter_js', 69 | 'report/complexity/files/_config_js', 70 | 'report/complexity/files/_driver_js', 71 | 'report/complexity/files/_unit_js', 72 | 'report/complexity/files/_suite_js', 73 | 'report/complexity/files/_timer_js', 74 | 'report/complexity/files/_host_js', 75 | 'report/complexity/files/_remote_js', 76 | 'report/complexity/files/_uuid_js', 77 | ], 78 | files: [ 79 | 'report.history.json', 80 | 'files/_js/report.history.json', 81 | 'files/_actions_js/report.history.json', 82 | 'files/_assertions_js/report.history.json', 83 | 'files/_reporter_js/report.history.json', 84 | 'files/_config_js/report.history.json', 85 | 'files/_driver_js/report.history.json', 86 | 'files/_unit_js/report.history.json', 87 | 'files/_suite_js/report.history.json', 88 | 'files/_timer_js/report.history.json', 89 | 'files/_host_js/report.history.json', 90 | 'files/_remote_js/report.history.json', 91 | 'files/_uuid_js/report.history.json', 92 | ] 93 | } 94 | }, 95 | 96 | // prepare files & folders for coverage 97 | prepareCoverage: { 98 | options: { 99 | folders: ['coverage', 'report', 'report/coverage'], 100 | pattern: '[require("fs").realpathSync(__dirname + "/../lib/dalek.js"), require("fs").realpathSync(__dirname + "/../lib/dalek/")]' 101 | } 102 | }, 103 | 104 | // add current timestamp to the html document 105 | includereplace: { 106 | dist: { 107 | options: { 108 | globals: { 109 | timestamp: '<%= grunt.template.today("dddd, mmmm dS, yyyy, h:MM:ss TT") %>' 110 | }, 111 | }, 112 | src: 'report/docs/*.html', 113 | dest: './' 114 | } 115 | }, 116 | 117 | // user docs 118 | documantix: { 119 | options: { 120 | header: 'dalekjs/dalekjs.com/master/assets/header.html', 121 | footer: 'dalekjs/dalekjs.com/master/assets/footer.html', 122 | target: 'report/docs' 123 | }, 124 | 125 | // actions 126 | actions: { 127 | src: ['lib/dalek/actions.js'], 128 | options: { 129 | vars: { 130 | title: 'DalekJS - Documentation - Actions', 131 | desc: 'DalekJS - Documentation - Actions', 132 | docs: true 133 | } 134 | } 135 | }, 136 | 137 | // assertions 138 | assertions: { 139 | src: ['lib/dalek/assertions.js'], 140 | options: { 141 | vars: { 142 | title: 'DalekJS - Documentation - Actions', 143 | desc: 'DalekJS - Documentation - Actions', 144 | docs: true 145 | } 146 | } 147 | }, 148 | 149 | // config 150 | config: { 151 | src: ['lib/dalek/config.js'], 152 | options: { 153 | vars: { 154 | title: 'DalekJS - Documentation - Config', 155 | desc: 'DalekJS - Documentation - Config', 156 | docs: true 157 | } 158 | } 159 | }, 160 | 161 | }, 162 | 163 | // archive docs 164 | archive: { 165 | options: { 166 | files: ['config.html', 'actions.html', 'assertions.html'] 167 | } 168 | }, 169 | 170 | // release canary version 171 | 'release-canary': { 172 | options: { 173 | files: [] 174 | } 175 | } 176 | 177 | }); 178 | 179 | // load 3rd party tasks 180 | require('load-grunt-tasks')(grunt); 181 | grunt.loadTasks('./node_modules/dalek-build-tools/tasks'); 182 | grunt.loadNpmTasks('grunt-documantix'); 183 | 184 | // define runner tasks 185 | grunt.registerTask('lint', 'jshint'); 186 | 187 | // split test & docs for speed 188 | grunt.registerTask('test', ['clean:coverage', 'prepareCoverage', 'concurrent:test', 'generateCoverageBadge']); 189 | grunt.registerTask('docs', ['clean:reportZip', 'clean:report', 'preparePlato', 'concurrent:docs', 'documantix', 'includereplace', 'compress']); 190 | 191 | // release tasks 192 | grunt.registerTask('releasePatch', ['test', 'bump-only:patch', 'contributors', 'changelog', 'bump-commit']); 193 | grunt.registerTask('releaseMinor', ['test', 'bump-only:minor', 'contributors', 'changelog', 'bump-commit']); 194 | grunt.registerTask('releaseMajor', ['test', 'bump-only:major', 'contributors', 'changelog', 'bump-commit']); 195 | 196 | // clean, test, generate docs (the CI task) 197 | grunt.registerTask('all', ['clean', 'test', 'docs']); 198 | 199 | }; 200 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 asciidisco (Sebastian Golasch) 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 2 | 3 | # DalekJS is not maintained any longer :cry: 4 | 5 | We recommend [TestCafé](http://devexpress.github.io/testcafe/) for your automated browser testing needs. 6 | 7 | --- 8 | 9 | dalekjs 10 | ====================== 11 | 12 | > DalekJS base framework 13 | 14 | [![Build Status](https://travis-ci.org/dalekjs/dalek.svg)](https://travis-ci.org/dalekjs/dalek) 15 | [![Build Status](https://drone.io/github.com/dalekjs/dalek/status.png)](https://drone.io/github.com/dalekjs/dalek/latest) 16 | [![Dependency Status](https://david-dm.org/dalekjs/dalek.svg)](https://david-dm.org/dalekjs/dalek) 17 | [![devDependency Status](https://david-dm.org/dalekjs/dalek/dev-status.svg)](https://david-dm.org/dalekjs/dalek#info=devDependencies) 18 | [![NPM version](https://badge.fury.io/js/dalekjs.svg)](http://badge.fury.io/js/dalekjs) 19 | [![Coverage](http://dalekjs.com/package/dalekjs/master/coverage/coverage.png)](http://dalekjs.com/package/dalekjs/master/coverage/index.html) 20 | [![unstable](https://rawgithub.com/hughsk/stability-badges/master/dist/unstable.svg)](http://github.com/hughsk/stability-badges) 21 | [![Stories in Ready](https://badge.waffle.io/dalekjs/dalek.svg?label=ready)](https://waffle.io/dalekjs/dalek) 22 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/dalekjs/dalek/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 23 | [![Built with Grunt](https://cdn.gruntjs.com/builtwith.png)](http://gruntjs.com/) 24 | 25 | [![NPM](https://nodei.co/npm/dalekjs.png)](https://nodei.co/npm/dalekjs/) 26 | [![NPM](https://nodei.co/npm-dl/dalekjs.png)](https://nodei.co/npm/dalekjs/) 27 | 28 | ## Resources 29 | 30 | [API Docs](http://dalekjs.com/package/dalekjs/master/api/index.html) - 31 | [Trello](https://trello.com/b/gA1A6RZW/dalekjs) - 32 | [Code coverage](http://dalekjs.com/package/dalekjs/master/coverage/index.html) - 33 | [Code complexity](http://dalekjs.com/package/dalekjs/master/complexity/index.html) - 34 | [Contributing](https://github.com/dalekjs/dalek/blob/master/CONTRIBUTING.md) - 35 | [User Docs](http://dalekjs.com/pages/getStarted.html) - 36 | [Homepage](http://dalekjs.com) - 37 | [Twitter](http://twitter.com/dalekjs) 38 | 39 | # Docs 40 | 41 | Daleks documentation can be found [here](http://dalekjs.com/pages/documentation.html) 42 | 43 | ## Help Is Just A Click Away 44 | 45 | ### #dalekjs on FreeNode.net IRC 46 | 47 | Join the `#daleksjs` channel on [FreeNode.net](http://freenode.net) to ask questions and get help. 48 | 49 | ### [Google Group Mailing List](https://groups.google.com/forum/#!forum/dalekjs) 50 | 51 | Get announcements for new releases, share your projects and ideas that are 52 | using DalekJS, and join in open-ended discussion that does not fit in 53 | to the Github issues list or StackOverflow Q&A. 54 | 55 | **For help with syntax, specific questions on how to implement a feature 56 | using DalekJS, and other Q&A items, use StackOverflow.** 57 | 58 | ### [StackOverflow](http://stackoverflow.com/questions/tagged/dalekjs) 59 | 60 | Ask questions about using DalekJS in specific scenarios, with 61 | specific features. For example, help with syntax, understanding how a feature works and 62 | how to override that feature, browser specific problems and so on. 63 | 64 | Questions on StackOverflow often turn in to blog posts or issues. 65 | 66 | ### [Github Issues](//github.com/dalekjs/dalek/issues) 67 | 68 | Report issues with DalekJS, submit pull requests to fix problems, or to 69 | create summarized and documented feature requests (preferably with pull 70 | requests that implement the feature). 71 | 72 | **Please don't ask questions or seek help in the issues list.** There are 73 | other, better channels for seeking assistance, like StackOverflow and the 74 | Google Groups mailing list. 75 | 76 | ![DalekJS](https://raw.github.com/dalekjs/dalekjs.com/master/img/logo.png) 77 | 78 | ## Legal FooBar (MIT License) 79 | 80 | Copyright (c) 2013 Sebastian Golasch 81 | 82 | Distributed under [MIT license](https://github.com/dalekjs/dalek/blob/master/LICENSE-MIT) 83 | 84 | -------------------------------------------------------------------------------- /examples/pagetitle.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Checks if the of ´github.com´ has the expected value 3 | 'Page title is correct': function (test) { 4 | 'use strict'; 5 | test.expect(1); 6 | 7 | test 8 | .open('http://github.com') 9 | .assert.title('GitHub') 10 | .done(); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /lib/dalek.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Copyright (c) 2013 Sebastian Golasch 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a 6 | * copy of this software and associated documentation files (the "Software"), 7 | * to deal in the Software without restriction, including without limitation 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | * and/or sell copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included 13 | * in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | * DEALINGS IN THE SOFTWARE. 22 | * 23 | */ 24 | 25 | 'use strict'; 26 | 27 | // ext. libs 28 | var async = require('async'); 29 | var EventEmitter2 = require('eventemitter2').EventEmitter2; 30 | 31 | // int. libs 32 | var Driver = require('./dalek/driver'); 33 | var Reporter = require('./dalek/reporter'); 34 | var Timer = require('./dalek/timer'); 35 | var Config = require('./dalek/config'); 36 | var Host = require('./dalek/host'); 37 | 38 | /** 39 | * Default options 40 | * @type {Object} 41 | */ 42 | 43 | var defaults = { 44 | reporter: ['console'], 45 | driver: ['native'], 46 | browser: ['phantomjs'], 47 | viewport: {width: 1280, height: 1024}, 48 | logLevel: 3 49 | }; 50 | 51 | /** 52 | * Setup all the options needed to configure dalek 53 | * 54 | * @param {options} opts Configuration options 55 | * @constructor 56 | */ 57 | 58 | var Dalek = function (opts) { 59 | // setup instance 60 | this._initialize(); 61 | 62 | // register exception handler 63 | this._registerExceptionHandler(); 64 | 65 | // normalize options 66 | this.options = this.normalizeOptions(opts); 67 | 68 | // getting advanced options 69 | if (opts && opts.advanced) { 70 | this.advancedOptions = opts.advanced; 71 | } 72 | 73 | // initiate config 74 | this.config = new Config(defaults, this.options, this.advancedOptions); 75 | 76 | // override tests if provided on the commandline 77 | if (this.options.tests) { 78 | this.config.config.tests = this.options.tests; 79 | } 80 | 81 | // prepare and load reporter(s) 82 | this._setupReporters(); 83 | 84 | // count all passed & failed assertions 85 | this.reporterEvents.on('report:assertion', this._onReportAssertion.bind(this)); 86 | 87 | // init the timer instance 88 | this.timer = new Timer(); 89 | 90 | // prepare driver event emitter instance 91 | this._setupDriverEmitter(); 92 | 93 | // check for file option, throw error if none is given 94 | // only bypasses if dalek runs in the remote mode 95 | if (!Array.isArray(this.config.get('tests')) && !this.options.remote) { 96 | this.reporterEvents.emit('error', 'No test files given!'); 97 | this.driverEmitter.emit('killAll'); 98 | process.exit(127); 99 | } 100 | 101 | // init the driver instance 102 | this._initDriver(); 103 | 104 | // check if dalek runs as a remote browser launcher 105 | if (this.options.remote) { 106 | var host = new Host({reporterEvents: this.reporterEvents, config: this.config}); 107 | host.run({ 108 | port: !isNaN(parseFloat(this.options.remote)) && isFinite(this.options.remote) ? this.options.remote : false 109 | }); 110 | } 111 | }; 112 | 113 | /** 114 | * Daleks base module 115 | * Used to configure all the things 116 | * and to start off the tests 117 | * 118 | * @module DalekJS 119 | * @class Dalek 120 | */ 121 | 122 | Dalek.prototype = { 123 | 124 | /** 125 | * Runs the configured testsuites 126 | * 127 | * @method run 128 | * @chainable 129 | */ 130 | 131 | run: function () { 132 | // early return; in case of remote 133 | if (this.options.remote) { 134 | return this; 135 | } 136 | 137 | // start the timer to measure the execution time 138 | this.timer.start(); 139 | 140 | // emit the runner started event 141 | this.reporterEvents.emit('report:runner:started'); 142 | 143 | // execute all given drivers sequentially 144 | var drivers = this.driver.getDrivers(); 145 | async.series(drivers, this.testsuitesFinished.bind(this)); 146 | return this; 147 | }, 148 | 149 | /** 150 | * Reports the all testsuites executed event 151 | * 152 | * @method testsuitesFinished 153 | * @chainable 154 | */ 155 | 156 | testsuitesFinished: function () { 157 | this.driverEmitter.emit('tests:complete'); 158 | setTimeout(this.reportRunFinished.bind(this), 0); 159 | return this; 160 | }, 161 | 162 | /** 163 | * Reports the all testsuites executed event 164 | * 165 | * @method reportRunFinished 166 | * @chainable 167 | */ 168 | 169 | reportRunFinished: function () { 170 | this.reporterEvents.emit('report:runner:finished', { 171 | elapsedTime: this.timer.stop().getElapsedTimeFormatted(), 172 | assertions: this.assertionsFailed + this.assertionsPassed, 173 | assertionsFailed: this.assertionsFailed, 174 | assertionsPassed: this.assertionsPassed, 175 | status: this.runnerStatus 176 | }); 177 | 178 | //we want to exit process with code 1 to single that test did not pass 179 | if(this.runnerStatus !== true) { 180 | var processExitCaptured = false; 181 | 182 | process.on('exit', function() { 183 | if(processExitCaptured === false) { 184 | processExitCaptured = true; 185 | process.exit(1); 186 | } 187 | }); 188 | } 189 | 190 | return this; 191 | }, 192 | 193 | /** 194 | * Normalizes options 195 | * 196 | * @method normalizeOptions 197 | * @param {object} options Raw options 198 | * @return {object} Normalized options 199 | */ 200 | 201 | normalizeOptions: function (options) { 202 | Object.keys(options).forEach(function (key) { 203 | if ({reporter: 1, driver: 1}[key]) { 204 | options[key] = options[key].map(function (input) { return input.trim(); }); 205 | } 206 | }); 207 | 208 | return options; 209 | }, 210 | 211 | /** 212 | * Sets up system env properties 213 | * 214 | * @method _initialize 215 | * @chainable 216 | * @private 217 | */ 218 | 219 | _initialize: function () { 220 | // prepare error data 221 | this.warnings = []; 222 | this.errors = []; 223 | 224 | // prepare state data for the complete test run 225 | this.runnerStatus = true; 226 | this.assertionsFailed = 0; 227 | this.assertionsPassed = 0; 228 | 229 | return this; 230 | }, 231 | 232 | /** 233 | * Sets up all the reporters 234 | * 235 | * @method _setupReporters 236 | * @chainable 237 | * @private 238 | */ 239 | 240 | _setupReporters: function () { 241 | this.reporters = []; 242 | this.reporterEvents = new EventEmitter2(); 243 | this.reporterEvents.setMaxListeners(Infinity); 244 | this.options.reporter = this.config.verifyReporters(this.config.get('reporter'), Reporter); 245 | this.options.reporter.forEach(this._addReporter, this); 246 | return this; 247 | }, 248 | 249 | /** 250 | * Adds a reporter 251 | * 252 | * @method _addReporter 253 | * @param {string} reporter Name of the reporter to add 254 | * @chainable 255 | * @private 256 | */ 257 | 258 | _addReporter: function (reporter) { 259 | this.reporters.push(Reporter.loadReporter(reporter, {events: this.reporterEvents, config: this.config, logLevel: this.config.get('logLevel')})); 260 | return this; 261 | }, 262 | 263 | /** 264 | * Updates the assertion state 265 | * 266 | * @method _onReportAssertion 267 | * @param {object} assertion Informations aout the runned assertions 268 | * @chainable 269 | * @private 270 | */ 271 | 272 | _onReportAssertion: function (assertion) { 273 | if (assertion.success) { 274 | this.assertionsPassed++; 275 | } else { 276 | this.runnerStatus = false; 277 | this.assertionsFailed++; 278 | } 279 | return this; 280 | }, 281 | 282 | /** 283 | * Initizializes the driver instances 284 | * 285 | * @method _initDriver 286 | * @chainable 287 | * @private 288 | */ 289 | 290 | _initDriver: function () { 291 | this.driver = new Driver({ 292 | config: this.config, 293 | driverEmitter: this.driverEmitter, 294 | reporterEvents: this.reporterEvents 295 | }); 296 | 297 | this.options.driver = this.config.verifyDrivers(this.config.get('driver'), this.driver); 298 | return this; 299 | }, 300 | 301 | /** 302 | * Sets up the event dispatcher for driver events 303 | * 304 | * @method _setupDriverEmitter 305 | * @chainable 306 | * @private 307 | */ 308 | 309 | _setupDriverEmitter: function () { 310 | var driverEmitter = new EventEmitter2(); 311 | driverEmitter.setMaxListeners(Infinity); 312 | this.driverEmitter = driverEmitter; 313 | return this; 314 | }, 315 | 316 | /** 317 | * Make sure to shutdown dalek & its spawned 318 | * components, webservers gracefully if a 319 | * runtime error pops up 320 | * 321 | * @method _registerExceptionHandler 322 | * @private 323 | * @chainable 324 | */ 325 | 326 | _registerExceptionHandler: function () { 327 | process.setMaxListeners(Infinity); 328 | process.on('uncaughtException', this._shutdown.bind(this)); 329 | return this; 330 | }, 331 | 332 | /** 333 | * Shutdown on uncaught exception 334 | * 335 | * @method _shutdown 336 | * @param {object} exception Runtime exception 337 | * @private 338 | */ 339 | 340 | _shutdown: function (exception) { 341 | // ios emulator hack, needs to go in the future 342 | if (exception.message && exception.message.search('This socket has been ended by the other party') !== -1) { 343 | return false; 344 | } 345 | 346 | this.driverEmitter.emit('killAll'); 347 | this.reporterEvents.emit('error', exception); 348 | } 349 | 350 | }; 351 | 352 | // export dalek as a module 353 | module.exports = Dalek; 354 | -------------------------------------------------------------------------------- /lib/dalek/actions.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Copyright (c) 2013 Sebastian Golasch 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a 6 | * copy of this software and associated documentation files (the "Software"), 7 | * to deal in the Software without restriction, including without limitation 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | * and/or sell copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included 13 | * in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | * DEALINGS IN THE SOFTWARE. 22 | */ 23 | 24 | 'use strict'; 25 | 26 | // ext. libs 27 | var Q = require('q'); 28 | var uuid = require('./uuid'); 29 | var cheerio = require('cheerio'); 30 | 31 | // int. global 32 | var reporter = null; 33 | 34 | /** 35 | * Actions are a way to control your browsers, e.g. simulate user interactions 36 | * like clicking elements, open urls, filling out input fields, etc. 37 | * 38 | * @class Actions 39 | * @constructor 40 | * @part Actions 41 | * @api 42 | */ 43 | 44 | var Actions = function () { 45 | this.uuids = {}; 46 | }; 47 | 48 | /** 49 | * It can be really cumbersome to repeat selectors all over when performing 50 | * multiple actions or assertions on the same element(s). 51 | * When you use the query method (or its alias $), you're able to specify a 52 | * selector once instead of repeating it all over the place. 53 | * 54 | * So, instead of writing this: 55 | * 56 | * ```javascript 57 | * test.open('http://doctorwhotv.co.uk/') 58 | * .assert.text('#nav').is('Navigation') 59 | * .assert.visible('#nav') 60 | * .assert.attr('#nav', 'data-nav', 'true') 61 | * .click('#nav') 62 | * .done(); 63 | * ``` 64 | * 65 | * you can write this: 66 | * 67 | * ```javascript 68 | * test.open('http://doctorwhotv.co.uk/') 69 | * .query('#nav') 70 | * .assert.text().is('Navigation') 71 | * .assert.visible() 72 | * .assert.attr('data-nav', 'true') 73 | * .click() 74 | * .end() 75 | * .done(); 76 | * ``` 77 | * 78 | * Always make sure to terminate it with the [end](assertions.html#meth-end) method! 79 | * 80 | * @api 81 | * @method query 82 | * @param {string} selector Selector of the element to query 83 | * @chainable 84 | */ 85 | 86 | Actions.prototype.query = function (selector) { 87 | var that = !this.test ? this : this.test; 88 | that.lastChain.push('querying'); 89 | that.selector = selector; 90 | that.querying = true; 91 | return this.test ? this : that; 92 | }; 93 | 94 | /** 95 | * Alias of query 96 | * 97 | * @api 98 | * @method $ 99 | * @param {string} selector Selector of the element to query 100 | * @chainable 101 | */ 102 | 103 | Actions.prototype.$ = Actions.prototype.query; 104 | 105 | /** 106 | * Triggers a mouse event on the first element found matching the provided selector. 107 | * Supported events are mouseup, mousedown, click, mousemove, mouseover and mouseout. 108 | * TODO: IMPLEMENT 109 | * 110 | * @method mouseEvent 111 | * @param {string} type 112 | * @param {string} selector 113 | * @chainable 114 | */ 115 | 116 | Actions.prototype.mouseEvent = function (type, selector) { 117 | var hash = uuid(); 118 | var cb = this._generateCallbackAssertion('mouseEvent', 'mouseEvent', type, selector, hash); 119 | this._addToActionQueue([type, selector, hash], 'mouseEvent', cb); 120 | return this; 121 | }; 122 | 123 | /** 124 | * Sets HTTP_AUTH_USER and HTTP_AUTH_PW values for HTTP based authentication systems. 125 | * 126 | * If your site is behind a HTTP basic auth, you're able to set the username and the password 127 | * 128 | * ```javascript 129 | * test.setHttpAuth('OSWIN', 'rycbrar') 130 | * .open('http://admin.therift.com'); 131 | * ``` 132 | * 133 | * Most of the time, you`re not storing your passwords within files that will be checked 134 | * in your vcs, for this scenario, you have two options: 135 | * 136 | * The first option is, to use daleks cli capabilities to generate config variables 137 | * from the command line, like this 138 | * 139 | * ```batch 140 | * $ dalek --vars USER=OSWIN,PASS=rycbrar 141 | * ``` 142 | * 143 | * ```javascript 144 | * test.setHttpAuth(test.config.get('USER'), test.config.get('PASS')) 145 | * .open('http://admin.therift.com'); 146 | * ``` 147 | * 148 | * The second option is, to use env variables to generate config variables 149 | * from the command line, like this 150 | * 151 | * ```batch 152 | * $ SET USER=OSWIN 153 | * $ SET PASS=rycbrar 154 | * $ dalek 155 | * ``` 156 | * 157 | * ```javascript 158 | * test.setHttpAuth(test.config.get('USER'), test.config.get('PASS')) 159 | * .open('http://admin.therift.com'); 160 | * ``` 161 | * 162 | * If both, dalek variables & env variables are set, the dalek variables win. 163 | * For more information about this, I recommend to check out the [configuration docs](/docs/config.html) 164 | * 165 | * TODO: IMPLEMENT 166 | * 167 | * @method setHttpAuth 168 | * @param {string} username 169 | * @param {string} password 170 | * @return {Actions} 171 | */ 172 | 173 | Actions.prototype.setHttpAuth = function (username, password) { 174 | var hash = uuid(); 175 | var cb = this._generateCallbackAssertion('setHttpAuth', 'setHttpAuth', username, password, hash); 176 | this._addToActionQueue([username, password, hash], 'setHttpAuth', cb); 177 | return this; 178 | }; 179 | 180 | /** 181 | * Switches to an iFrame context 182 | * 183 | * Sometimes you encounter situations, where you need to drive/access an iFrame sitting in your page. 184 | * You can access such frames with this method, but be aware of the fact, that the complete test context 185 | * than switches to the iframe context, every action and assertion will be executed within the iFrame context. 186 | * Btw.: The domain of the IFrame can be whatever you want, this method has no same origin policy restrictions. 187 | * 188 | * If you wan't to get back to the parents context, you have to use the [toParent](#meth-toParent) method. 189 | * 190 | * ```html 191 | * <div> 192 | * <iframe id="login" src="/login.html"/> 193 | * </div> 194 | * ``` 195 | * 196 | * ```javascript 197 | * test.open('http://adomain.withiframe.com') 198 | * .assert.title().is('Title of a page that embeds an iframe') 199 | * .toFrame('#login') 200 | * .assert.title().is('Title of a page that can be embedded as an iframe') 201 | * .toParent() 202 | * .done(); 203 | * ``` 204 | * 205 | * > NOTE: Buggy in Firefox 206 | * 207 | * @api 208 | * @method toFrame 209 | * @param {string} selector Selector of the frame to switch to 210 | * @chainable 211 | */ 212 | 213 | Actions.prototype.toFrame = function (selector) { 214 | var hash = uuid(); 215 | 216 | if (this.querying === true) { 217 | selector = this.selector; 218 | } 219 | 220 | var cb = this._generateCallbackAssertion('toFrame', 'toFrame', selector, hash); 221 | this._addToActionQueue([selector, hash], 'toFrame', cb); 222 | return this; 223 | }; 224 | 225 | /** 226 | * Switches back to the parent page context when the test context has been 227 | * switched to an iFrame context 228 | * 229 | * ```html 230 | * <div> 231 | * <iframe id="login" src="/login.html"/> 232 | * </div> 233 | * ``` 234 | * 235 | * ```javascript 236 | * test.open('http://adomain.withiframe.com') 237 | * .assert.title().is('Title of a page that embeds an iframe') 238 | * .toFrame('#login') 239 | * .assert.title().is('Title of a page that can be embedded as an iframe') 240 | * .toParent() 241 | * .assert.title().is('Title of a page that embeds an iframe') 242 | * .done(); 243 | * ``` 244 | * 245 | * > NOTE: Buggy in Firefox 246 | * 247 | * @api 248 | * @method toParent 249 | * @chainable 250 | */ 251 | 252 | Actions.prototype.toParent = function () { 253 | var hash = uuid(); 254 | var cb = this._generateCallbackAssertion('toFrame', 'toFrame', null, hash); 255 | this._addToActionQueue([null, hash], 'toFrame', cb); 256 | return this; 257 | }; 258 | 259 | /** 260 | * Switches to a different window context 261 | * 262 | * Sometimes you encounter situations, where you need to access a different window, like popup windows. 263 | * You can access such windows with this method, but be aware of the fact, that the complete test context 264 | * than switches to the window context, every action and assertion will be executed within the chosen window context. 265 | * Btw.: The domain of the window can be whatever you want, this method has no same origin policy restrictions. 266 | * 267 | * If you want to get back to the parents context, you have to use the [toParentWindow](#meth-toParentWindow) method. 268 | * 269 | * ```html 270 | * <div> 271 | * <a onclick="window.open('http://google.com','goog','width=480, height=300')">Open Google</a> 272 | * </div> 273 | * ``` 274 | * 275 | * ```javascript 276 | * test.open('http://adomain.com') 277 | * .assert.title().is('Title of a page that can open a popup window') 278 | * .toWindow('goog') 279 | * .assert.title().is('Google') 280 | * .toParentWindow() 281 | * .done(); 282 | * ``` 283 | * 284 | * > NOTE: Buggy in Firefox 285 | * 286 | * @api 287 | * @method toWindow 288 | * @param {string} name Name of the window to switch to 289 | * @chainable 290 | */ 291 | 292 | Actions.prototype.toWindow = function (name) { 293 | var hash = uuid(); 294 | var cb = this._generateCallbackAssertion('toWindow', 'toWindow', name, hash); 295 | this._addToActionQueue([name, hash], 'toWindow', cb); 296 | return this; 297 | }; 298 | 299 | /** 300 | * Switches back to the parent window context when the test context has been 301 | * switched to a different window context 302 | * 303 | * ```html 304 | * <div> 305 | * <a onclick="window.open('http://google.com','goog','width=480, height=300')">Open Google</a> 306 | * </div> 307 | * ``` 308 | * 309 | * ```javascript 310 | * test.open('http://adomain.com') 311 | * .assert.title().is('Title of a page that can open a popup window') 312 | * .toWindow('goog') 313 | * .assert.title().is('Google') 314 | * .toParentWindow() 315 | * .assert.title().is('Title of a page that can open a popup window') 316 | * .done(); 317 | * ``` 318 | * 319 | * > NOTE: Buggy in Firefox 320 | * 321 | * @api 322 | * @method toParentWindow 323 | * @chainable 324 | */ 325 | 326 | Actions.prototype.toParentWindow = function () { 327 | var hash = uuid(); 328 | var cb = this._generateCallbackAssertion('toWindow', 'toWindow', null, hash); 329 | this._addToActionQueue([null, hash], 'toWindow', cb); 330 | return this; 331 | }; 332 | 333 | /** 334 | * Wait until a resource that matches the given testFx is loaded to process a next step. 335 | * 336 | * TODO: IMPLEMENT 337 | * 338 | * @method waitForResource 339 | * @param {string} ressource URL of the ressource that should be waited for 340 | * @param {number} timeout Timeout in miliseconds 341 | * @chainable 342 | */ 343 | 344 | Actions.prototype.waitForResource = function (ressource, timeout) { 345 | var hash = uuid(); 346 | var cb = this._generateCallbackAssertion('waitForResource', 'waitForResource', ressource, timeout, hash); 347 | this._addToActionQueue([ressource, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitForResource', cb); 348 | return this; 349 | }; 350 | 351 | /** 352 | * Waits until the passed text is present in the page contents before processing the immediate next step. 353 | * 354 | * TODO: IMPLEMENT 355 | * 356 | * @method waitForText 357 | * @param {string} text Text to be waited for 358 | * @param {number} timeout Timeout in miliseconds 359 | * @chainable 360 | */ 361 | 362 | Actions.prototype.waitForText = function (text, timeout) { 363 | var hash = uuid(); 364 | var cb = this._generateCallbackAssertion('waitForText', 'waitForText', text, timeout, hash); 365 | this._addToActionQueue([text, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitForText', cb); 366 | return this; 367 | }; 368 | 369 | /** 370 | * Waits until an element matching the provided selector expression is visible in the remote DOM to process a next step. 371 | * 372 | * TODO: IMPLEMENT 373 | * 374 | * @method waitUntilVisible 375 | * @param {string} selector Selector of the element that should be waited to become invisible 376 | * @param {number} timeout Timeout in miliseconds 377 | * @chainable 378 | */ 379 | 380 | Actions.prototype.waitUntilVisible = function (selector, timeout) { 381 | var hash = uuid(); 382 | 383 | if (this.querying === true) { 384 | timeout = selector; 385 | selector = this.selector; 386 | } 387 | 388 | var cb = this._generateCallbackAssertion('waitUntilVisible', 'waitUntilVisible', selector, timeout, hash); 389 | this._addToActionQueue([selector, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitUntilVisible', cb); 390 | return this; 391 | }; 392 | 393 | /** 394 | * Waits until an element matching the provided selector expression is no longer visible in remote DOM to process a next step. 395 | * 396 | * TODO: IMPLEMENT 397 | * 398 | * @method waitWhileVisible 399 | * @param {string} selector Selector of the element that should be waited to become visible 400 | * @param {number} timeout Timeout in miliseconds 401 | * @chainable 402 | */ 403 | 404 | Actions.prototype.waitWhileVisible = function (selector, timeout) { 405 | var hash = uuid(); 406 | 407 | if (this.querying === true) { 408 | timeout = selector; 409 | selector = this.selector; 410 | } 411 | 412 | var cb = this._generateCallbackAssertion('waitWhileVisible', 'waitWhileVisible', selector, timeout, hash); 413 | this._addToActionQueue([selector, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitWhileVisible', cb); 414 | return this; 415 | }; 416 | 417 | /** 418 | * Take a screenshot of the current page or css element. 419 | * 420 | * The pathname argument takes some placeholders that will be replaced 421 | * Placeholder: 422 | * 423 | * - `:browser` - The browser name (e.g. 'Chrome', 'Safari', 'Firefox', etc.) 424 | * - `:version` - The browser version (e.g. '10_0', '23_11_5', etc.) 425 | * - `:os` - The operating system (e.g. `OSX`, `Windows`, `Linux`) 426 | * - `:osVersion` - The operating system version (e.g `XP`, `7`, `10_8`, etc.) 427 | * - `:viewport` - The current viewport in pixels (e.g. `w1024_h768`) 428 | * - `:timestamp` - UNIX like timestapm (e.g. `637657345`) 429 | * - `:date` - Current date in format MM_DD_YYYY (e.g. `12_24_2013`) 430 | * - `:datetime` - Current datetime in format MM_DD_YYYY_HH_mm_ss (e.g. `12_24_2013_14_55_23`) 431 | * 432 | * ```javascript 433 | * // creates 'my/folder/my_file.png' 434 | * test.screenshot('my/folder/my_file.png'); 435 | * // creates 'my/page/in/safari/homepage.png' 436 | * test.screenshot('my/page/in/:browser/homepage.png'); 437 | * // creates 'my/page/in/safari_6_0_1/homepage.png' 438 | * test.screenshot('my/page/in/:browser_:version/homepage.png'); 439 | * // creates 'my/page/in/safari_6_0_1/on/osx/homepage.png' 440 | * test.screenshot('my/page/in/:browser_:version/on/:os/homepage.png'); 441 | * // creates 'my/page/in/safari_6_0_1/on/osx_10_8/homepage.png' 442 | * test.screenshot('my/page/in/:browser_:version/on/:os_:osVersion/homepage.png'); 443 | * // creates 'my/page/at/w1024_h768/homepage.png' 444 | * test.screenshot('my/page/at/:viewport/homepage.png'); 445 | * // creates 'my/page/at/637657345/homepage.png' 446 | * test.screenshot('my/page/in_time/:timestamp/homepage.png'); 447 | * // creates 'my/page/at/12_24_2013/homepage.png' 448 | * test.screenshot('my/page/in_time/:date/homepage.png'); 449 | * // creates 'my/page/at/12_24_2013_14_55_23/homepage.png' 450 | * test.screenshot('my/page/in_time/:datetime/homepage.png'); 451 | * ``` 452 | * 453 | * @api 454 | * @method screenshot 455 | * @param {string} pathname Name of the folder and file the screenshot should be saved to 456 | * @param {string} css selector of element should be screeshoted 457 | * @return chainable 458 | */ 459 | 460 | Actions.prototype.screenshot = function (pathname, selector) { 461 | var hash = uuid(); 462 | 463 | if (this.querying === true) { 464 | selector = this.selector; 465 | } 466 | 467 | var opts = { 468 | realpath : undefined, 469 | selector : selector 470 | }; 471 | 472 | this.screenshotParams = opts; 473 | 474 | var screenshotcb = this._generatePlainCallback('screenshot', hash, opts, 'realpath', typeof selector === 'undefined'); 475 | this._addToActionQueue(['', pathname, hash], 'screenshot', screenshotcb.bind(this)); 476 | 477 | if (selector) { 478 | var imagecutcb = this._generateCallbackAssertion('imagecut', 'screenshot element', opts, hash); 479 | this._addToActionQueue([opts, hash], 'imagecut', imagecutcb); 480 | } 481 | 482 | this.reporter.emit('report:screenshot', { 483 | 'pathname' : pathname, 484 | 'uuid' : hash 485 | }); 486 | 487 | return this; 488 | }; 489 | 490 | /** 491 | * Generates a callback that will be fired when the action has been completed. 492 | * The callback will then store value into opts variable. 493 | * 494 | * @method _generateCallbackAssertion 495 | * @param {string} type Type of the action (normalle the actions name) 496 | * @param {string} hash Unique id of the action 497 | * @param {string} opts Variable where will be stored result of execution of the action 498 | * @param {string} key Name of the property where will be stored result of execution of the action 499 | * @return {function} The generated callback function 500 | * @private 501 | */ 502 | Actions.prototype._generatePlainCallback = function (type, hash, opts, property, last) { 503 | var cb = function (data) { 504 | if (data.hash === hash && data.key === type && !this.uuids[data.uuid]) { 505 | if (typeof opts === 'object' && typeof property === 'string') { 506 | opts[property] = data.value; 507 | } 508 | if (data.key === 'screenshot') { 509 | this.reporter.emit('report:action', { 510 | value: data.value, 511 | type: type, 512 | uuid: data.uuid 513 | }); 514 | } 515 | 516 | if (last) { 517 | this.uuids[data.uuid] = true; 518 | } 519 | } 520 | }; 521 | return cb; 522 | }; 523 | 524 | /** 525 | * Pause steps suite execution for a given amount of time, and optionally execute a step on done. 526 | * 527 | * This makes sense, if you have a ticker for example, tht scrolls like every ten seconds 528 | * & you want to assure that the visible content changes every ten seconds 529 | * 530 | * ```javascript 531 | * test.open('http://myticker.org') 532 | * .assert.visible('.ticker-element:first-child', 'First ticker element is visible') 533 | * .wait(10000) 534 | * .assert.visible('.ticker-element:nth-child(2)', 'Snd. ticker element is visible') 535 | * .wait(10000) 536 | * .assert.visible('.ticker-element:last-child', 'Third ticker element is visible') 537 | * .done(); 538 | * ``` 539 | * If no timeout argument is given, a default timeout of 5 seconds will be used 540 | * 541 | * ```javascript 542 | * test.open('http://myticker.org') 543 | * .assert.visible('.ticker-element:first-child', 'First ticker element is visible') 544 | * .wait() 545 | * .assert.visible('.ticker-element:nth-child(2)', 'Snd. ticker element is visible') 546 | * .wait() 547 | * .assert.visible('.ticker-element:last-child', 'Third ticker element is visible') 548 | * .done(); 549 | * ``` 550 | * 551 | * @api 552 | * @method wait 553 | * @param {number} timeout in milliseconds 554 | * @chainable 555 | */ 556 | 557 | Actions.prototype.wait = function (timeout) { 558 | var hash = uuid(); 559 | var cb = this._generateCallbackAssertion('wait', 'wait', timeout, hash); 560 | this._addToActionQueue([(timeout ? parseInt(timeout, 10) : 5000), hash], 'wait', cb); 561 | return this; 562 | }; 563 | 564 | /** 565 | * Reloads current page location. 566 | * 567 | * This is basically the same as hitting F5/refresh in your browser 568 | * 569 | * ```javascript 570 | * test.open('http://google.com') 571 | * .reload() 572 | * .done(); 573 | * ``` 574 | * 575 | * @api 576 | * @method reload 577 | * @chainable 578 | */ 579 | 580 | Actions.prototype.reload = function () { 581 | var hash = uuid(); 582 | var cb = this._generateCallbackAssertion('refresh', 'refresh', '', hash); 583 | this._addToActionQueue([hash], 'refresh', cb); 584 | return this; 585 | }; 586 | 587 | /** 588 | * Moves a step forward in browser's history. 589 | * 590 | * This is basically the same as hitting the forward button in your browser 591 | * 592 | * ```javascript 593 | * test.open('http://google.com') 594 | * .open('https://github.com') 595 | * .assert.url.is('https://github.com/', 'We are at GitHub') 596 | * .back() 597 | * .assert.url.is('http://google.com', 'We are at Google!') 598 | * .forward() 599 | * .assert.url.is('https://github.com/', 'Back at GitHub! Timetravel FTW') 600 | * .done(); 601 | * ``` 602 | * 603 | * @api 604 | * @method forward 605 | * @chainable 606 | */ 607 | 608 | Actions.prototype.forward = function () { 609 | var hash = uuid(); 610 | var cb = this._generateCallbackAssertion('forward', 'forward', '', hash); 611 | this._addToActionQueue([hash], 'forward', cb); 612 | return this; 613 | }; 614 | 615 | /** 616 | * Moves back a step in browser's history. 617 | * 618 | * This is basically the same as hitting the back button in your browser 619 | * 620 | * ```javascript 621 | * test.open('http://google.com') 622 | * .open('https://github.com') 623 | * .assert.url.is('https://github.com/', 'We are at GitHub') 624 | * .back() 625 | * .assert.url.is('http://google.com', 'We are at Google!') 626 | * .forward() 627 | * .assert.url.is('https://github.com/', 'Back at GitHub! Timetravel FTW'); 628 | * .done(); 629 | * ``` 630 | * 631 | * @api 632 | * @method back 633 | * @chainable 634 | */ 635 | 636 | Actions.prototype.back = function () { 637 | var hash = uuid(); 638 | var cb = this._generateCallbackAssertion('back', 'back', '', hash); 639 | this._addToActionQueue([hash], 'back', cb); 640 | return this; 641 | }; 642 | 643 | /** 644 | * Performs a click on the element matching the provided selector expression. 645 | * 646 | * If we take Daleks homepage (the one you're probably visiting right now), 647 | * the HTML looks something like this (it does not really, but hey, lets assume this for a second) 648 | * 649 | * ```html 650 | * <nav> 651 | * <ul> 652 | * <li><a id="homeapge" href="/index.html">DalekJS</a></li> 653 | * <li><a id="docs" href="/docs.html">Documentation</a></li> 654 | * <li><a id="faq" href="/faq.html">F.A.Q</a></li> 655 | * </ul> 656 | * </nav> 657 | * ``` 658 | * 659 | * ```javascript 660 | * test.open('http://dalekjs.com') 661 | * .click('#faq') 662 | * .assert.title().is('DalekJS - Frequently asked questions', 'What the F.A.Q.') 663 | * .done(); 664 | * ``` 665 | * 666 | * By default, this performs a left click. 667 | * In the future it might become the ability to also execute a "right button" click. 668 | * 669 | * > Note: Does not work correctly in Firefox when used on `<select>` & `<option>` elements 670 | * 671 | * @api 672 | * @method click 673 | * @param {string} selector Selector of the element to be clicked 674 | * @chainable 675 | */ 676 | 677 | Actions.prototype.click = function (selector) { 678 | var hash = uuid(); 679 | 680 | if (this.querying === true) { 681 | selector = this.selector; 682 | } 683 | 684 | var cb = this._generateCallbackAssertion('click', 'click', selector, hash); 685 | this._addToActionQueue([selector, hash], 'click', cb); 686 | return this; 687 | }; 688 | 689 | /** 690 | * Submits a form. 691 | * 692 | * ```html 693 | * <form id="skaaro" action="skaaro.php" method="GET"> 694 | * <input type="hidden" name="intheshadows" value="itis"/> 695 | * <input type="text" name="truth" id="truth" value=""/> 696 | * </form> 697 | * ``` 698 | * 699 | * ```javascript 700 | * test.open('http://home.dalek.com') 701 | * .type('#truth', 'out there is') 702 | * .submit('#skaaro') 703 | * .done(); 704 | * ``` 705 | * 706 | * > Note: Does not work in Firefox yet 707 | * 708 | * @api 709 | * @method submit 710 | * @param {string} selector Selector of the form to be submitted 711 | * @chainable 712 | */ 713 | 714 | Actions.prototype.submit = function (selector) { 715 | var hash = uuid(); 716 | 717 | if (this.querying === true) { 718 | selector = this.selector; 719 | } 720 | 721 | var cb = this._generateCallbackAssertion('submit', 'submit', selector, hash); 722 | this._addToActionQueue([selector, hash], 'submit', cb); 723 | return this; 724 | }; 725 | 726 | /** 727 | * Performs an HTTP request for opening a given location. 728 | * You can forge GET, POST, PUT, DELETE and HEAD requests. 729 | * 730 | * Basically the same as typing a location into your browsers URL bar and 731 | * hitting return. 732 | * 733 | * ```javascript 734 | * test.open('http://dalekjs.com') 735 | * .assert.url().is('http://dalekjs.com', 'DalekJS I\'m in you') 736 | * .done(); 737 | * ``` 738 | * 739 | * @api 740 | * @method open 741 | * @param {string} location URL of the page to open 742 | * @chainable 743 | */ 744 | 745 | Actions.prototype.open = function (location) { 746 | //see if we should prepend the location with the configured base url is available and needed 747 | if(location.substr(0, 1) === '/' && this.driver.config.config.baseUrl) { 748 | location = this.driver.config.config.baseUrl + location; 749 | } 750 | 751 | var hash = uuid(); 752 | var cb = this._generateCallbackAssertion('open', 'open', location, hash); 753 | this._addToActionQueue([location, hash], 'open', cb); 754 | return this; 755 | }; 756 | 757 | /** 758 | * Types a text into an input field or text area. 759 | * And yes, it really types, character for character, like you would 760 | * do when using your keyboard. 761 | * 762 | * 763 | * ```html 764 | * <form id="skaaro" action="skaaro.php" method="GET"> 765 | * <input type="hidden" name="intheshadows" value="itis"/> 766 | * <input type="text" name="truth" id="truth" value=""/> 767 | * </form> 768 | * ``` 769 | * 770 | * ```javascript 771 | * test.open('http://home.dalek.com') 772 | * .type('#truth', 'out there is') 773 | * .assert.val('#truth', 'out there is', 'Text has been set') 774 | * .done(); 775 | * ``` 776 | * 777 | * You can also send special keys using unicode. 778 | * 779 | * * ```javascript 780 | * test.open('http://home.dalek.com') 781 | * .type('#truth', 'out \uE008there\uE008 is') 782 | * .assert.val('#truth', 'out THERE is', 'Text has been set') 783 | * .done(); 784 | * ``` 785 | * You can go [here](https://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/element/:id/value) to read up on special keys and unicodes for them (note that a code of U+EXXX is actually written in code as \uEXXX). 786 | * 787 | * > Note: Does not work correctly in Firefox with special keys 788 | * 789 | * @api 790 | * @method type 791 | * @param {string} selector Selector of the form field to be filled 792 | * @param {string} keystrokes Text to be applied to the element 793 | * @chainable 794 | */ 795 | 796 | Actions.prototype.type = function (selector, keystrokes) { 797 | var hash = uuid(); 798 | 799 | if (this.querying === true) { 800 | keystrokes = selector; 801 | selector = this.selector; 802 | } 803 | 804 | var cb = this._generateCallbackAssertion('type', 'type', selector, keystrokes, hash); 805 | this._addToActionQueue([selector, keystrokes], 'type', cb); 806 | return this; 807 | }; 808 | 809 | /** 810 | * This acts just like .type() with a key difference. 811 | * This action can be used on non-input elements (useful for test site wide keyboard shortcuts and the like). 812 | * So assumeing we have a keyboard shortcut that display an alert box, we could test that with something like this: 813 | * 814 | * ```javascript 815 | * test.open('http://home.dalek.com') 816 | * .sendKeys('body', '\uE00C') 817 | * .assert.dialogText('press the escape key give this alert text') 818 | * .done(); 819 | * ``` 820 | * 821 | * 822 | * > Note: Does not work correctly in Firefox with special keys 823 | * 824 | * @api 825 | * @method sendKeys 826 | * @param {string} selector Selector of the form field to be filled 827 | * @param {string} keystrokes Text to be applied to the element 828 | * @chainable 829 | */ 830 | 831 | Actions.prototype.sendKeys = function (selector, keystrokes) { 832 | var hash = uuid(); 833 | 834 | if (this.querying === true) { 835 | keystrokes = selector; 836 | selector = this.selector; 837 | } 838 | 839 | var cb = this._generateCallbackAssertion('sendKeys', 'sendKeys', selector, keystrokes, hash); 840 | this._addToActionQueue([selector, keystrokes], 'sendKeys', cb); 841 | return this; 842 | }; 843 | 844 | /** 845 | * Types a text into the text input field of a prompt dialog. 846 | * Like you would do when using your keyboard. 847 | * 848 | * ```html 849 | * <div> 850 | * <a id="aquestion" onclick="this.innerText = window.prompt('Your favourite companion:')">????</a> 851 | * </div> 852 | * ``` 853 | * 854 | * ```javascript 855 | * test.open('http://adomain.com') 856 | * .click('#aquestion') 857 | * .answer('Rose') 858 | * .assert.text('#aquestion').is('Rose', 'Awesome she was!') 859 | * .done(); 860 | * ``` 861 | * 862 | * 863 | * > Note: Does not work in Firefox & PhantomJS 864 | * 865 | * @api 866 | * @method answer 867 | * @param {string} keystrokes Text to be applied to the element 868 | * @return chainable 869 | */ 870 | 871 | Actions.prototype.answer = function (keystrokes) { 872 | var hash = uuid(); 873 | var cb = this._generateCallbackAssertion('promptText', 'promptText', keystrokes, hash); 874 | this._addToActionQueue([keystrokes, hash], 'promptText', cb); 875 | return this; 876 | }; 877 | 878 | /** 879 | * Executes a JavaScript function within the browser context 880 | * 881 | * ```javascript 882 | * test.open('http://adomain.com') 883 | * .execute(function () { 884 | * window.myFramework.addRow('foo'); 885 | * window.myFramework.addRow('bar'); 886 | * }) 887 | * .done(); 888 | * ``` 889 | * 890 | * You can also apply arguments to the function 891 | * 892 | * ```javascript 893 | * test.open('http://adomain.com') 894 | * .execute(function (paramFoo, aBar) { 895 | * window.myFramework.addRow(paramFoo); 896 | * window.myFramework.addRow(aBar); 897 | * }, 'foo', 'bar') 898 | * .done(); 899 | * ``` 900 | * 901 | * > Note: Buggy in Firefox 902 | * 903 | * @api 904 | * @method execute 905 | * @param {function} script JavaScript function that should be executed 906 | * @return chainable 907 | */ 908 | 909 | Actions.prototype.execute = function (script) { 910 | var hash = uuid(); 911 | var args = [this.contextVars].concat(Array.prototype.slice.call(arguments, 1) || []); 912 | var cb = this._generateCallbackAssertion('execute', 'execute', script, args, hash); 913 | this._addToActionQueue([script, args, hash], 'execute', cb); 914 | return this; 915 | }; 916 | 917 | /** 918 | * Waits until a function returns true to process any next step. 919 | * 920 | * You can also set a callback on timeout using the onTimeout argument, 921 | * and set the timeout using the timeout one, in milliseconds. The default timeout is set to 5000ms. 922 | * 923 | * ```javascript 924 | * test.open('http://adomain.com') 925 | * .waitFor(function () { 926 | * return window.myCheck === true; 927 | * }) 928 | * .done(); 929 | * ``` 930 | * 931 | * You can also apply arguments to the function, as well as a timeout 932 | * 933 | * ```javascript 934 | * test.open('http://adomain.com') 935 | * .waitFor(function (aCheck) { 936 | * return window.myThing === aCheck; 937 | * }, ['arg1', 'arg2'], 10000) 938 | * .done(); 939 | * ``` 940 | * 941 | * > Note: Buggy in Firefox 942 | * 943 | * @method waitFor 944 | * @param {function} fn Async function that resolves an promise when ready 945 | * @param {array} args Additional arguments 946 | * @param {number} timeout Timeout in miliseconds 947 | * @chainable 948 | * @api 949 | */ 950 | 951 | Actions.prototype.waitFor = function (script, args, timeout) { 952 | var hash = uuid(); 953 | timeout = timeout || 5000; 954 | args = [this.contextVars].concat(Array.prototype.slice.call(arguments, 1) || []); 955 | var cb = this._generateCallbackAssertion('waitFor', 'waitFor', script, args, timeout, hash); 956 | this._addToActionQueue([script, args, timeout, hash], 'waitFor', cb); 957 | return this; 958 | }; 959 | 960 | /** 961 | * Accepts an alert/prompt/confirm dialog. This is basically the same actions as when 962 | * you are clicking okay or hitting return in one of that dialogs. 963 | * 964 | * ```html 965 | * <div> 966 | * <a id="attentione" onclick="window.alert('Alonsy!')">ALERT!ALERT!</a> 967 | * </div> 968 | * ``` 969 | * 970 | * ```javascript 971 | * test.open('http://adomain.com') 972 | * // alert appears 973 | * .click('#attentione') 974 | * // alert is gone 975 | * .accept() 976 | * .done(); 977 | * ``` 978 | * 979 | * > Note: Does not work in Firefox & PhantomJS 980 | * 981 | * @api 982 | * @method accept 983 | * @return chainable 984 | */ 985 | 986 | Actions.prototype.accept = function () { 987 | var hash = uuid(); 988 | var cb = this._generateCallbackAssertion('acceptAlert', 'acceptAlert', hash); 989 | this._addToActionQueue([hash], 'acceptAlert', cb); 990 | return this; 991 | }; 992 | 993 | /** 994 | * Dismisses an prompt/confirm dialog. This is basically the same actions as when 995 | * you are clicking cancel in one of that dialogs. 996 | * 997 | * ```html 998 | * <div> 999 | * <a id="nonono" onclick="(this.innerText = window.confirm('No classic doctors in the 50th?') ? 'Buh!' : ':(') ">What!</a> 1000 | * </div> 1001 | * ``` 1002 | * 1003 | * ```javascript 1004 | * test.open('http://adomain.com') 1005 | * // prompt appears 1006 | * .click('#nonono') 1007 | * // prompt is gone 1008 | * .dismiss() 1009 | * .assert.text('#nonono').is(':(', 'So sad') 1010 | * .done(); 1011 | * ``` 1012 | * 1013 | * > Note: Does not work in Firefox & PhantomJS 1014 | * 1015 | * @api 1016 | * @method dismiss 1017 | * @return chainable 1018 | */ 1019 | 1020 | Actions.prototype.dismiss = function () { 1021 | var hash = uuid(); 1022 | var cb = this._generateCallbackAssertion('dismissAlert', 'dismissAlert', hash); 1023 | this._addToActionQueue([hash], 'dismissAlert', cb); 1024 | return this; 1025 | }; 1026 | 1027 | /** 1028 | * Resizes the browser window to a set of given dimensions (in px). 1029 | * The default configuration of dalek opening pages is a width of 1280px 1030 | * and a height of 1024px. You can specify your own default in the configuration. 1031 | * 1032 | * ```html 1033 | * <div> 1034 | * <span id="magicspan">The span in the fireplace</span> 1035 | * </div> 1036 | * ``` 1037 | * 1038 | * ```css 1039 | * #magicspan { 1040 | * display: inline; 1041 | * } 1042 | * 1043 | * // @media all and (max-width: 500px) and (min-width: 300px) 1044 | * #magicspan { 1045 | * display: none; 1046 | * } 1047 | * ``` 1048 | * 1049 | * ```javascript 1050 | * test.open('http://adomain.com') 1051 | * .assert.visible('#magicspan', 'Big screen, visible span') 1052 | * .resize({width: 400, height: 500}) 1053 | * .assert.notVisible('#magicspan', 'Small screen, no visible span magic!') 1054 | * .done(); 1055 | * ``` 1056 | * 1057 | * 1058 | * > Note: Does not work in Firefox 1059 | * 1060 | * @api 1061 | * @method resize 1062 | * @param {object} dimensions Width and height as properties to apply 1063 | * @chainable 1064 | */ 1065 | 1066 | Actions.prototype.resize = function (dimensions) { 1067 | var hash = uuid(); 1068 | var cb = this._generateCallbackAssertion('resize', 'resize', dimensions, hash); 1069 | this._addToActionQueue([dimensions, hash], 'resize', cb); 1070 | return this; 1071 | }; 1072 | 1073 | /** 1074 | * Maximizes the browser window. 1075 | * 1076 | * ```html 1077 | * <div> 1078 | * <span id="magicspan">The span in the fireplace</span> 1079 | * </div> 1080 | * ``` 1081 | * 1082 | * ```css 1083 | * #magicspan { 1084 | * display: inline; 1085 | * } 1086 | * 1087 | * @media all and (max-width: 500px) and (min-width: 300px) { 1088 | * #magicspan { 1089 | * display: none; 1090 | * } 1091 | * } 1092 | * ``` 1093 | * 1094 | * ```javascript 1095 | * test.open('http://adomain.com') 1096 | * .resize({width: 400, height: 500}) 1097 | * .assert.notVisible('#magicspan', 'Small screen, no visible span magic!') 1098 | * .maximize() 1099 | * .assert.visible('#magicspan', 'Big screen, visible span') 1100 | * .done(); 1101 | * ``` 1102 | * 1103 | * > Note: Does not work in Firefox and PhantomJS 1104 | * 1105 | * @api 1106 | * @method maximize 1107 | * @chainable 1108 | */ 1109 | 1110 | Actions.prototype.maximize = function () { 1111 | var hash = uuid(); 1112 | var cb = this._generateCallbackAssertion('maximize', 'maximize', hash); 1113 | this._addToActionQueue([hash], 'maximize', cb); 1114 | return this; 1115 | }; 1116 | 1117 | /** 1118 | * Sets a cookie. 1119 | * More configuration options will be implemented in the future, 1120 | * by now, you can only set a cookie with a specific name and contents. 1121 | * This will be a domain wide set cookie. 1122 | * 1123 | * ```javascript 1124 | * test.open('http://adomain.com') 1125 | * .setCookie('my_cookie_name', 'my=content') 1126 | * .done(); 1127 | * ``` 1128 | * 1129 | * @api 1130 | * @method setCookie 1131 | * @chainable 1132 | */ 1133 | 1134 | Actions.prototype.setCookie = function (name, contents) { 1135 | var hash = uuid(); 1136 | var cb = this._generateCallbackAssertion('setCookie', 'setCookie', name, contents, hash); 1137 | this._addToActionQueue([name, contents, hash], 'setCookie', cb); 1138 | return this; 1139 | }; 1140 | 1141 | /** 1142 | * Waits until an element matching the provided 1143 | * selector expression exists in remote DOM to process any next step. 1144 | * 1145 | * Lets assume we have a ticker that loads its contents via AJAX, 1146 | * and appends new elements, when the call has been successfully answered: 1147 | * 1148 | * ```javascript 1149 | * test.open('http://myticker.org') 1150 | * .assert.text('.ticker-element:first-child', 'First!', 'First ticker element is visible') 1151 | * // now we load the next ticker element, defsult timeout is 5 seconds 1152 | * .waitForElement('.ticker-element:nth-child(2)') 1153 | * .assert.text('.ticker-element:nth-child(2)', 'Me snd. one', 'Snd. ticker element is visible') 1154 | * // Lets assume that this AJAX call can take longer, so we raise the default timeout to 10 seconds 1155 | * .waitForElement('.ticker-element:last-child', 10000) 1156 | * .assert.text('.ticker-element:last-child', 'Me, third one!', 'Third ticker element is visible') 1157 | * .done(); 1158 | * ``` 1159 | * 1160 | * Note that the function exits succesfully when the first element is found, matching the given selector 1161 | * 1162 | * @api 1163 | * @method waitForElement 1164 | * @param {string} selector Selector that matches the element to wait for 1165 | * @param {number} timeout Timeout in milliseconds 1166 | * @chainable 1167 | */ 1168 | 1169 | Actions.prototype.waitForElement = function (selector, timeout) { 1170 | var hash = uuid(); 1171 | 1172 | if (this.querying === true) { 1173 | timeout = selector; 1174 | selector = this.selector; 1175 | } 1176 | 1177 | var cb = this._generateCallbackAssertion('waitForElement', 'waitForElement', selector + ' : ' + timeout, hash); 1178 | this._addToActionQueue([selector, (timeout ? parseInt(timeout, 10) : 5000), hash], 'waitForElement', cb); 1179 | return this; 1180 | }; 1181 | 1182 | /** 1183 | * Fills the fields of a form with given values. 1184 | * 1185 | * ```html 1186 | * <input type="text" value="not really a value" id="ijustwannahaveavalue"/> 1187 | * ``` 1188 | * 1189 | * ```javascript 1190 | * test.open('http://dalekjs.com') 1191 | * .setValue('#ijustwannahaveavalue', 'a value') 1192 | * .assert.val('#ijustwannahaveavalue', 'a value', 'Value is changed'); 1193 | * ``` 1194 | * 1195 | * @api 1196 | * @method setValue 1197 | * @param {string} selector 1198 | * @param {string} value 1199 | * @return {Actions} 1200 | */ 1201 | 1202 | Actions.prototype.setValue = function (selector, value) { 1203 | var hash = uuid(); 1204 | 1205 | if (this.querying === true) { 1206 | value = selector; 1207 | selector = this.selector; 1208 | } 1209 | 1210 | var cb = this._generateCallbackAssertion('setValue', 'setValue', selector + ' : ' + value, hash); 1211 | this._addToActionQueue([selector, value, hash], 'setValue', cb); 1212 | return this; 1213 | }; 1214 | 1215 | // LOG (May should live in its own module) 1216 | // --------------------------------------- 1217 | 1218 | Actions.prototype.logger = {}; 1219 | 1220 | /** 1221 | * Logs a part of the remote dom 1222 | * 1223 | * ```html 1224 | * <body> 1225 | * <div id="smth"> 1226 | * <input type="hidden" value="not really a value" id="ijustwannahaveavalue"/> 1227 | * </div> 1228 | * </body> 1229 | * ``` 1230 | * 1231 | * ```javascript 1232 | * test.open('http://dalekjs.com/guineapig') 1233 | * .log.dom('#smth') 1234 | * .done(); 1235 | * ``` 1236 | * 1237 | * Will output this: 1238 | * 1239 | * ```html 1240 | * DOM: #smth <input type="hidden" value="not really a value" id="ijustwannahaveavalue"/> 1241 | * ``` 1242 | 1243 | * 1244 | * @api 1245 | * @method log.dom 1246 | * @param {string} selector CSS selector 1247 | * @chainable 1248 | */ 1249 | 1250 | Actions.prototype.logger.dom = function (selector) { 1251 | var hash = uuid(); 1252 | 1253 | var cb = function logDomCb (data) { 1254 | if (data && data.key === 'source' && !this.uuids[data.uuid]) { 1255 | this.uuids[data.uuid] = true; 1256 | var $ = cheerio.load(data.value); 1257 | var result = selector ? $(selector).html() : $.html(); 1258 | selector = selector ? selector : ' '; 1259 | result = !result ? ' Not found' : result; 1260 | this.reporter.emit('report:log:user', 'DOM: ' + selector + ' ' + result); 1261 | } 1262 | }.bind(this); 1263 | 1264 | this._addToActionQueue([hash], 'source', cb); 1265 | return this; 1266 | }; 1267 | 1268 | /** 1269 | * Logs a user defined message 1270 | * 1271 | * ```javascript 1272 | * test.open('http://dalekjs.com/guineapig') 1273 | * .execute(function () { 1274 | * this.data('aKey', 'aValue'); 1275 | * }) 1276 | * .log.message(function () { 1277 | * return test.data('aKey'); // outputs MESSAGE: 'aValue' 1278 | * }) 1279 | * .done(); 1280 | * ``` 1281 | * 1282 | * 'Normal' messages can be logged too: 1283 | * 1284 | * ```javascript 1285 | * test.open('http://dalekjs.com/guineapig') 1286 | * .log.message('FooBar') // outputs MESSAGE: FooBar 1287 | * .done(); 1288 | * ``` 1289 | * 1290 | * @api 1291 | * @method log.message 1292 | * @param {function|string} message 1293 | * @chainable 1294 | */ 1295 | 1296 | Actions.prototype.logger.message = function (message) { 1297 | var hash = uuid(); 1298 | 1299 | var cb = function logMessageCb (data) { 1300 | if (data && data.key === 'noop' && !this.uuids[data.hash]) { 1301 | this.uuids[data.hash] = true; 1302 | var result = (typeof(data.value) === 'function') ? data.value.bind(this)() : data.value; 1303 | this.reporter.emit('report:log:user', 'MESSAGE: ' + result); 1304 | } 1305 | }.bind(this); 1306 | 1307 | this._addToActionQueue([message, hash], 'noop', cb); 1308 | return this; 1309 | }; 1310 | 1311 | /** 1312 | * Generates a callback that will be fired when the action has been completed. 1313 | * The callback itself will then validate the answer and will also emit an event 1314 | * that the action has been successfully executed. 1315 | * 1316 | * @method _generateCallbackAssertion 1317 | * @param {string} key Unique key of the action 1318 | * @param {string} type Type of the action (normalle the actions name) 1319 | * @return {function} The generated callback function 1320 | * @private 1321 | */ 1322 | 1323 | Actions.prototype._generateCallbackAssertion = function (key, type) { 1324 | var cb = function (data) { 1325 | if (data && data.key === key && !this.uuids[data.uuid]) { 1326 | if (!data || (data.value && data.value === null)) { 1327 | data.value = ''; 1328 | } 1329 | 1330 | if (key === 'execute') { 1331 | Object.keys(data.value.dalek).forEach(function (key) { 1332 | this.contextVars[key] = data.value.dalek[key]; 1333 | }.bind(this)); 1334 | 1335 | data.value.test.forEach(function (test) { 1336 | this.reporter.emit('report:assertion', { 1337 | success: test.ok, 1338 | expected: true, 1339 | value: test.ok, 1340 | message: test.message, 1341 | type: 'OK' 1342 | }); 1343 | 1344 | this.incrementExpectations(); 1345 | 1346 | if (!test.ok) { 1347 | this.incrementFailedAssertions(); 1348 | } 1349 | }.bind(this)); 1350 | 1351 | data.value = ''; 1352 | } 1353 | 1354 | this.uuids[data.uuid] = true; 1355 | reporter.emit('report:action', { 1356 | value: data.value, 1357 | type: type, 1358 | uuid: data.uuid 1359 | }); 1360 | } 1361 | }.bind(this); 1362 | return cb; 1363 | }; 1364 | 1365 | /** 1366 | * Adds a method to the queue of actions/assertions to execute 1367 | * 1368 | * @method _addToActionQueue 1369 | * @param {object} opts Options of the action to invoke 1370 | * @param {string} driverMethod Name of the method to call on the driver 1371 | * @param {function} A callback function that will be executed when the action has been executed 1372 | * @private 1373 | * @chainable 1374 | */ 1375 | 1376 | Actions.prototype._addToActionQueue = function (opts, driverMethod, cb) { 1377 | if (driverMethod !== 'screenshot' && driverMethod !== 'imagecut') { 1378 | this.screenshotParams = undefined; 1379 | } 1380 | 1381 | this.actionPromiseQueue.push(function () { 1382 | var deferred = Q.defer(); 1383 | // add a generic identifier as the last argument to any action method call 1384 | opts.push(uuid()); 1385 | // check the method on the driver object && the callback function 1386 | if (typeof(this.driver[driverMethod]) === 'function' && typeof(cb) === 'function') { 1387 | // call the method on the driver object 1388 | this.driver[driverMethod].apply(this.driver, opts); 1389 | deferred.resolve(); 1390 | } else { 1391 | deferred.reject(); 1392 | } 1393 | 1394 | // listen to driver message events & apply the callback argument 1395 | this.driver.events.on('driver:message', cb); 1396 | return deferred.promise; 1397 | }.bind(this)); 1398 | return this; 1399 | }; 1400 | 1401 | Actions.prototype._button = function(button) { 1402 | var buttons = {LEFT: 0, MIDDLE: 1, RIGHT: 2}; 1403 | 1404 | if (button === undefined) { 1405 | button = 0; 1406 | } else if (typeof button !== 'number') { 1407 | button = buttons[button.toUpperCase()] || 0; 1408 | } 1409 | 1410 | return button; 1411 | }; 1412 | 1413 | // http://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/click 1414 | Actions.prototype.buttonClick = function (button) { 1415 | var hash = uuid(); 1416 | button = this._button(button); 1417 | 1418 | var cb = this._generateCallbackAssertion('buttonClick', 'buttonClick'); 1419 | this._addToActionQueue([button, hash], 'buttonClick', cb); 1420 | 1421 | return this; 1422 | }; 1423 | 1424 | // http://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/moveto 1425 | Actions.prototype.moveTo = function (selector, x, y) { 1426 | var hash = uuid(); 1427 | 1428 | if (this.querying === true) { 1429 | selector = this.selector; 1430 | } 1431 | 1432 | if (x === undefined) { 1433 | x = null; 1434 | } 1435 | 1436 | if (y === undefined) { 1437 | y = null; 1438 | } 1439 | 1440 | // move to coordinate 1441 | var cb = this._generateCallbackAssertion('moveto', 'moveto'); 1442 | this._addToActionQueue([selector, x, y, hash], 'moveto', cb); 1443 | 1444 | return this; 1445 | }; 1446 | 1447 | /** 1448 | * Close the active window and automatically selects the parent window. 1449 | * 1450 | * ```javascript 1451 | * this.test.toWindow('test'); 1452 | * this.test.close(); 1453 | * 1454 | * //you can now write your code as if the original parent window was selected because .close() 1455 | * //selects that automatically for you so you don't have to call .toParentWindow() everytime 1456 | * ``` 1457 | * 1458 | * @api 1459 | * @method close 1460 | * @chainable 1461 | */ 1462 | Actions.prototype.close = function () { 1463 | var hash = uuid(); 1464 | var cb = this._generateCallbackAssertion('close', 'close', hash); 1465 | this._addToActionQueue([hash], 'close', cb); 1466 | 1467 | //since the current window is now closed, make sense to automatically select the parent window since you would have to do this anyway 1468 | this.toParentWindow(); 1469 | 1470 | return this; 1471 | }; 1472 | 1473 | /** 1474 | * @module DalekJS 1475 | */ 1476 | 1477 | module.exports = function (opts) { 1478 | reporter = opts.reporter; 1479 | return Actions; 1480 | }; 1481 | -------------------------------------------------------------------------------- /lib/dalek/config.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Copyright (c) 2013 Sebastian Golasch 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a 6 | * copy of this software and associated documentation files (the "Software"), 7 | * to deal in the Software without restriction, including without limitation 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | * and/or sell copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included 13 | * in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | * DEALINGS IN THE SOFTWARE. 22 | * 23 | */ 24 | 25 | 'use strict'; 26 | 27 | // ext. libs 28 | var path = require('path'); 29 | var fs = require('fs'); 30 | var _ = require('lodash'); 31 | var yaml = require('js-yaml'); 32 | var JSON5 = require('json5'); 33 | var glob = require('glob'); 34 | require('coffee-script/register'); 35 | 36 | /** 37 | * Configures the config instance 38 | * 39 | * @param {object} defaults Default parameter options 40 | * @param {object} opts Command line options 41 | * @constructor 42 | */ 43 | 44 | var Config = function (defaults, opts, advOpts) { 45 | this.customFilename = null; 46 | this.defaultFilename = 'Dalekfile'; 47 | this.supportedExtensions = ['yml', 'json5', 'json', 'js', 'coffee']; 48 | this.advancedOptions = advOpts; 49 | this.config = this.load(defaults, opts.config, opts); 50 | }; 51 | 52 | /** 53 | * Parses config data & loads config files for [DalekJS](//github.com/dalekjs/dalek) tests. 54 | * 55 | * This module is a driver plugin for [DalekJS](//github.com/dalekjs/dalek). 56 | * It connects Daleks testsuite with the remote testing environment of [Sauce Labs](https://saucelabs.com). 57 | * 58 | * The driver can be installed with the following command: 59 | * 60 | * ```bash 61 | * $ npm install dalek-driver-sauce --save-dev 62 | * ``` 63 | * 64 | * You can use the driver by adding a config option to the your [Dalekfile](/docs/config.html) 65 | * 66 | * ```javascript 67 | * "driver": ["sauce"] 68 | * ``` 69 | * 70 | * Or you can tell Dalek that it should run your tests via sauces service via the command line: 71 | * 72 | * ```bash 73 | * $ dalek mytest.js -d sauce 74 | * ``` 75 | * 76 | * In order to run your tests within the Sauce Labs infrastructure, you must add your sauce username & key 77 | * to your dalek configuration. Those two parameters must be set in order to get this driver up & running. 78 | * You can specifiy them within your [Dalekfile](/docs/config.html) like so: 79 | * 80 | * ```javascript 81 | * "driver.sauce": { 82 | * "user": "dalekjs", 83 | * "key": "aaaaaa-1234-567a-1abc-1br6d9f68689" 84 | * } 85 | * ``` 86 | * 87 | * It is also possible to specify a set of other extra saucy parameters like `name` & `tags`: 88 | * 89 | * ```javascript 90 | * "driver.sauce": { 91 | * "user": "dalekjs", 92 | * "key": "aaaaaa-1234-567a-1abc-1br6d9f68689", 93 | * "name": "Guineapig", 94 | * "tags": ["dalek", "testproject"] 95 | * } 96 | * ``` 97 | * 98 | * If you would like to have a more control over the browser/OS combinations that are available, you are able 99 | * to configure you custom combinations: 100 | * 101 | * ```javascript 102 | * "browsers": [{ 103 | * "chrome": { 104 | * "platform": "OS X 10.6", 105 | * "actAs": "chrome", 106 | * "version": 27 107 | * }, 108 | * "chromeWin": { 109 | * "platform": "Windows 7", 110 | * "actAs": "chrome", 111 | * "version": 27 112 | * }, 113 | * "chromeLinux": { 114 | * "platform": "Linux", 115 | * "actAs": "chrome", 116 | * "version": 26 117 | * } 118 | * ``` 119 | * 120 | * You can then call your custom browsers like so: 121 | * 122 | * ```bash 123 | * $ dalek mytest.js -d sauce -b chrome,chromeWin,chromeLinux 124 | * ``` 125 | * 126 | * or you can define them in your Dalekfile: 127 | * 128 | * ```javascript 129 | * "browser": ["chrome", "chromeWin", "chromeLinux"] 130 | * ``` 131 | * 132 | * A list of all available browser/OS combinations, can be found [here](https://saucelabs.com/docs/platforms). 133 | * 134 | * @module DalekJS 135 | * @class Config 136 | * @namespace Dalek 137 | * @part Config 138 | * @api 139 | */ 140 | 141 | Config.prototype = { 142 | 143 | /** 144 | * Checks if a config file is available 145 | * 146 | * @method checkAvailabilityOfConfigFile 147 | * @param {String} pathname 148 | * @return {String} config File path 149 | */ 150 | 151 | checkAvailabilityOfConfigFile: function (pathname) { 152 | // check if a pathname is given, 153 | // then check if the file is available 154 | if (pathname && fs.existsSync(pathname)) { 155 | return fs.realpathSync(pathname); 156 | } 157 | 158 | // check if any of the default configuration files is available 159 | return this.supportedExtensions.reduce(this._checkFile.bind(this)); 160 | }, 161 | 162 | /** 163 | * Iterator function that checks the existance of a given file 164 | * 165 | * @method _checkFile 166 | * @param {String} previousValue Last iterations result 167 | * @param {String} ext File extension to check 168 | * @param {integer} idx Iteration index 169 | * @param {object} data File data 170 | * @return {String} config File path 171 | * @private 172 | */ 173 | 174 | _checkFile: function (previousValue, ext, idx, data) { 175 | if (previousValue.length > 6) { 176 | return previousValue; 177 | } 178 | 179 | var fileToCheck = this.defaultFilename + '.' + previousValue; 180 | if (fs.existsSync(fileToCheck)) { 181 | return fs.realpathSync(fileToCheck); 182 | } 183 | 184 | return this._checkDefaultFile(ext, data); 185 | }, 186 | 187 | /** 188 | * Iterator function that checks the existance of a the default file 189 | * 190 | * @method _checkDefaultFile 191 | * @param {String} ext File extension to check 192 | * @param {object} data File data 193 | * @return {String} config File path 194 | * @private 195 | */ 196 | 197 | _checkDefaultFile: function (ext, data) { 198 | if (ext === data[data.length - 1]) { 199 | var fileToCheck = this.defaultFilename + '.' + ext; 200 | if (fs.existsSync(fileToCheck)) { 201 | return fs.realpathSync(fileToCheck); 202 | } 203 | } 204 | 205 | return ext; 206 | }, 207 | 208 | /** 209 | * Loads a file & merges the results with the 210 | * commandline options & the default config 211 | * 212 | * @method load 213 | * @param {object} defaults Default config 214 | * @param {String} pathname Filename of the config file to load 215 | * @param {object} opts Command line options 216 | * @return {object} config Merged config data 217 | */ 218 | 219 | load: function (defaults, pathname, opts) { 220 | var file = this.checkAvailabilityOfConfigFile(pathname); 221 | var data = {}; 222 | 223 | if (!this.advancedOptions || this.advancedOptions.dalekfile !== false) { 224 | data = this.loadFile(file); 225 | } 226 | 227 | // remove the tests property if the array length is 0 228 | if (opts.tests.length === 0) { 229 | delete opts.tests; 230 | } 231 | 232 | if (data.tests && _.isArray(data.tests) && data.tests.length > 0) { 233 | var tests = []; 234 | 235 | //get all the files that match 236 | _.forEach(data.tests, function(search) { 237 | tests = tests.concat(glob.sync(search)); 238 | }); 239 | 240 | //remove duplicate files 241 | tests = tests.filter(function(elem, pos, self) { 242 | return self.indexOf(elem) === pos; 243 | }); 244 | 245 | data.tests = tests; 246 | } 247 | 248 | return _.merge(defaults, data, opts, (this.advancedOptions || {})); 249 | }, 250 | 251 | /** 252 | * Loads a config file & parses it based on the file extension 253 | * 254 | * @method loadFile 255 | * @param {String} pathname Filename of the config file to load 256 | * @return {object} data Config data 257 | */ 258 | 259 | loadFile: function (pathname) { 260 | var ext = path.extname(pathname).replace('.', ''); 261 | return this['read' + ext] ? this['read' + ext](pathname) : {}; 262 | }, 263 | 264 | /** 265 | * Fetches & returns a config item 266 | * 267 | * @method get 268 | * @param {String} item Key of the item to load 269 | * @return {mixed|null} data Requested config data 270 | */ 271 | 272 | get: function (item) { 273 | return this.config[item] || null; 274 | }, 275 | 276 | /** 277 | * Loads a json config file 278 | * 279 | * @method readjson 280 | * @return {object} data Parsed config data 281 | */ 282 | 283 | readjson: function (pathname) { 284 | var contents = fs.readFileSync((pathname || this.defaultFilename + '.json'), 'utf8'); 285 | return JSON.parse(contents); 286 | }, 287 | 288 | /** 289 | * Loads a json5 config file 290 | * 291 | * @method readJson5 292 | * @return {object} data Parsed config data 293 | */ 294 | 295 | readjson5: function (pathname) { 296 | var contents = fs.readFileSync((pathname || this.defaultFilename + '.json5'), 'utf8'); 297 | return JSON5.parse(contents); 298 | }, 299 | 300 | /** 301 | * Loads a yaml config file 302 | * 303 | * @method readyaml 304 | * @return {object} data Parsed config data 305 | */ 306 | 307 | readyml: function (pathname) { 308 | var contents = fs.readFileSync((pathname || this.defaultFilename + '.yml'), 'utf8'); 309 | return yaml.load(contents); 310 | }, 311 | 312 | /** 313 | * Loads a javascript config file 314 | * 315 | * @method readjs 316 | * @return {object} data Parsed config data 317 | */ 318 | 319 | readjs: function (pathname) { 320 | return require((pathname || this.defaultFilename)); 321 | }, 322 | 323 | /** 324 | * Loads a coffescript config file 325 | * 326 | * @method readcoffee 327 | * @return {object} data Parsed config data 328 | */ 329 | 330 | readcoffee: function (pathname) { 331 | return require((pathname || this.defaultFilename)); 332 | }, 333 | 334 | /** 335 | * Verifies if a reporter is given, exists & is valid 336 | * 337 | * @method verifyReporters 338 | * @return {array} data List of verified reporters 339 | */ 340 | 341 | verifyReporters: function (reporters, reporter) { 342 | return _.compact(this._verify(reporters, 'isReporter', reporter)); 343 | }, 344 | 345 | /** 346 | * Verifies if a driver is given, exists & is valid 347 | * 348 | * @method verifyDrivers 349 | * @return {array} data List of verified drivers 350 | */ 351 | 352 | verifyDrivers: function (drivers, driver) { 353 | return _.compact(this._verify(drivers, 'isDriver', driver)); 354 | }, 355 | 356 | /** 357 | * Verifies if a driver is given, exists & is valid 358 | * 359 | * @method _verify 360 | * @param {array} check Data that should be mapped 361 | * @param {string} fn Name of the function that should be invoked on the veryify object 362 | * @param {object} instance Object instance where the verify function should be invoked 363 | * @return {array} data List of verified items 364 | * @private 365 | */ 366 | 367 | _verify: function (check, fn, instance) { 368 | return check.map(this._verifyIterator.bind(this, fn, instance)); 369 | }, 370 | 371 | /** 372 | * Verifies if a driver is given, exists & is valid 373 | * 374 | * @method _verifyIterator 375 | * @param {string} fn Name of the function that should be invoked on the veryify object 376 | * @param {object} instance Object instance where the verify function should be invoked 377 | * @param {string} elm Name of the element that should be checked 378 | * @return {string|null} element name of the verified element or false if checked failed 379 | * @priavte 380 | */ 381 | 382 | _verifyIterator: function (fn, instance, elm) { 383 | return instance[fn](elm) ? elm : false; 384 | } 385 | }; 386 | 387 | // export the module 388 | module.exports = Config; 389 | -------------------------------------------------------------------------------- /lib/dalek/driver.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Copyright (c) 2013 Sebastian Golasch 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a 6 | * copy of this software and associated documentation files (the "Software"), 7 | * to deal in the Software without restriction, including without limitation 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | * and/or sell copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included 13 | * in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | * DEALINGS IN THE SOFTWARE. 22 | * 23 | */ 24 | 25 | 'use strict'; 26 | 27 | // ext. libs 28 | var async = require('async'); 29 | 30 | // int. libs 31 | var Suite = require('./suite'); 32 | 33 | /** 34 | * Configures the driver instance 35 | * 36 | * @constructor 37 | */ 38 | 39 | var Driver = function (options) { 40 | // add configuration data to the driver instance 41 | this.config = options.config; 42 | this.browser = this.config.get('browser'); 43 | this.files = this.config.get('tests'); 44 | this.drivers = this.config.get('driver'); 45 | 46 | // flag if we use the canary driver builds 47 | this.driverIsCanary = false; 48 | 49 | // link driver events 50 | this.driverEmitter = options.driverEmitter; 51 | this.reporterEvents = options.reporterEvents; 52 | }; 53 | 54 | /** 55 | * Generates & starts drivers & browsers 56 | * the tests will be run in 57 | * 58 | * @module DalekJS 59 | * @class Driver 60 | * @namespace Dalek 61 | * @part Driver 62 | * @api 63 | */ 64 | 65 | Driver.prototype = { 66 | 67 | /** 68 | * Checks if the requested driver is available 69 | * 70 | * @method isDriver 71 | * @param {string} driver Name of the requested driver 72 | * @return {bool} isDriver Driver is availavle 73 | */ 74 | 75 | isDriver: function (driver) { 76 | try { 77 | require.resolve('dalek-driver-' + driver); 78 | } catch (e) { 79 | try { 80 | require.resolve('dalek-driver-' + driver + '-canary'); 81 | } catch (e) { 82 | return false; 83 | } 84 | this.driverIsCanary = true; 85 | return true; 86 | } 87 | return true; 88 | }, 89 | 90 | /** 91 | * Loads the requested driver 92 | * Emits an event to the reporter 93 | * 94 | * @method loadDriver 95 | * @param {string} driver Name of the requested driver 96 | * @return {object} driverModule Instance of the driver module 97 | */ 98 | 99 | loadDriver: function (driver) { 100 | this.reporterEvents.emit('report:log:system', 'dalek-internal-driver: Loading driver: "' + driver + '"'); 101 | return require('dalek-driver-' + driver + (this.driverIsCanary ? '-canary' : '')); 102 | }, 103 | 104 | /** 105 | * Returns a list with browser driver instances 106 | * 107 | * @method getDrivers 108 | * @return {array} verifiedDrivers 109 | */ 110 | 111 | getDrivers: function () { 112 | return this.drivers.map(this.getVerifiedBrowser, this)[0]; 113 | }, 114 | 115 | /** 116 | * Returns a list with browser driver instances 117 | * 118 | * @method getVerifiedBrowser 119 | * @param {string} driver Name of the requested driver 120 | * @return {array} verifiedDrivers Array of dribver 'run' functions 121 | */ 122 | 123 | getVerifiedBrowser: function (driver) { 124 | return this.browser.map(this.getVerifiedDriver.bind(this, this.loadDriver(driver), driver)); 125 | }, 126 | 127 | /** 128 | * Returns a scoped version of the driver run function 129 | * 130 | * @method getVerifiedDriver 131 | * @param {object} driverModule Instance of the used driver 132 | * @param {string} driver Name of ther used driver 133 | * @param {string} browser Name of the used browser 134 | * @return {function} run Function that kicks off execution of a testsuite chain in a browser 135 | */ 136 | 137 | getVerifiedDriver: function (driverModule, driver, browser) { 138 | return this.run.bind(this, driver, driverModule, browser); 139 | }, 140 | 141 | /** 142 | * Loads a browser driver 143 | * 144 | * @method loadBrowserConfiguration 145 | * @param {string} browser Name of the requested browser driver 146 | * @param {object} browsers Configuration options for the requested browser 147 | * @return {object} browserConfiguration Browser driver isntance and configuration meta data 148 | */ 149 | 150 | loadBrowserConfiguration: function (browser, browsers, driver) { 151 | var browserConfiguration; 152 | 153 | if (driver.dummyBrowser && driver.dummyBrowser()) { 154 | return driver.getBrowser(driver); 155 | } 156 | 157 | try { 158 | browserConfiguration = this.getDefaultBrowserConfiguration(browser, browsers); 159 | } catch (e) { 160 | browserConfiguration = this.getUserBrowserConfiguration(browser, browsers); 161 | } 162 | 163 | return browserConfiguration; 164 | }, 165 | 166 | /** 167 | * Loads the default browser driver 168 | * 169 | * @method getDefaultBrowserConfiguration 170 | * @param {string} browser Name of the requested browser driver 171 | * @param {object} browsers Configuration options for the requested browser 172 | * @return {object} browserConfiguration Browser driver isntance and configuration meta data 173 | */ 174 | 175 | getDefaultBrowserConfiguration: function (browser, browsers) { 176 | var browserConfiguration = {configuration: null, module: null}; 177 | 178 | // set browser configuration 179 | if (browsers[browser]) { 180 | browserConfiguration.configuration = browsers[browser]; 181 | } 182 | 183 | // try to load `normal` browser modules first, 184 | // if that doesnt work, try canary builds 185 | try { 186 | // check if the browser is a remote instance 187 | // else, try to load the local browser 188 | if (browserConfiguration.configuration && browserConfiguration.configuration.type === 'remote') { 189 | browserConfiguration.module = require('./remote'); 190 | } else { 191 | browserConfiguration.module = require('dalek-browser-' + browser); 192 | } 193 | } catch (e) { 194 | browserConfiguration.module = require('dalek-browser-' + browser + '-canary'); 195 | } 196 | 197 | return browserConfiguration; 198 | }, 199 | 200 | /** 201 | * Loads a user configured browser driver 202 | * 203 | * @method getUserBrowserConfiguration 204 | * @param {string} browser Name of the requested browser driver 205 | * @param {object} browsers Configuration options for the requested browser 206 | * @return {object} browserConfiguration Browser driver isntance and configuration meta data 207 | */ 208 | 209 | getUserBrowserConfiguration: function (browser, browsers) { 210 | var browserConfiguration = {configuration: null, module: null}; 211 | 212 | if (browsers && browsers[browser] && browsers[browser].actAs) { 213 | browserConfiguration.module = require('dalek-browser-' + browsers[browser].actAs); 214 | browserConfiguration.configuration = browsers[browser]; 215 | } 216 | 217 | if (!browserConfiguration.module && browser.search(':') !== -1) { 218 | var args = browser.split(':'); 219 | var extractedBrowser = args[0].trim(); 220 | var browserType = args[1].trim().toLowerCase(); 221 | browserConfiguration.module = require('dalek-browser-' + extractedBrowser); 222 | 223 | if (browserConfiguration.module && browserConfiguration.module.browserTypes && browserConfiguration.module.browserTypes[browserType]) { 224 | var binary = (process.platform === 'win32' ? browserConfiguration.module.browserTypes[browserType].win32 : browserConfiguration.module.browserTypes[browserType].darwin); 225 | browserConfiguration.configuration = { 226 | binary: binary, 227 | type: browserType 228 | }; 229 | } 230 | } 231 | 232 | return browserConfiguration; 233 | }, 234 | 235 | /** 236 | * Couple driver & session status events for the reporter 237 | * 238 | * @method coupleReporterEvents 239 | * @param {string} driverName Name of the requested driver 240 | * @param {string} browser Name of the requested browser 241 | * @chainable 242 | */ 243 | 244 | coupleReporterEvents: function (driverName, browser) { 245 | this.driverEmitter.on('driver:sessionStatus:' + driverName + ':' + browser, this.reporterEvents.emit.bind(this.reporterEvents, 'report:driver:session')); 246 | this.driverEmitter.on('driver:status:' + driverName + ':' + browser, this.reporterEvents.emit.bind(this.reporterEvents, 'report:driver:status')); 247 | return this; 248 | }, 249 | 250 | /** 251 | * Returns a list of testsuite runner functions 252 | * 253 | * @method getTestsuiteInstances 254 | * @param {object} driverInstance Instance of the requested driver 255 | * @return {array} testsuiteRunners List of testsuites that should be run 256 | */ 257 | 258 | getTestsuiteInstances: function (driverInstance) { 259 | return this.files.map(this.createTestsuiteInstance.bind(this, driverInstance)); 260 | }, 261 | 262 | /** 263 | * Creates a testsuite runner function 264 | * 265 | * @method createTestsuiteInstance 266 | * @param {object} driverInstance Instance of the requested driver 267 | * @param {string} file Filename of the testsuite 268 | * @return {function} testsuiteRunner Runner function from the testsuite 269 | */ 270 | 271 | createTestsuiteInstance: function (driverInstance, file) { 272 | var suite = new Suite({numberOfSuites: this.files.length, file: file, driver: driverInstance, driverEmitter: this.driverEmitter, reporterEmitter: this.reporterEvents}); 273 | return suite.run.bind(suite); 274 | }, 275 | 276 | /** 277 | * Generates a testsuite instance, emits the 278 | * browser running event & starts a new async() sesries execution 279 | * Will be called when the driver is ready 280 | * 281 | * @method _onDriverReady 282 | * @param {string} browser Name of the requested browser 283 | * @param {string} driverName Name of the requested driver 284 | * @param {function} callback Asyncs next() callback function 285 | * @param {object} driverInstance Instance of the requested driver 286 | * @chainable 287 | * @private 288 | */ 289 | 290 | _onDriverReady: function (browser, driverName, callback, driverInstance) { 291 | // generate testsuite instance from test files 292 | var testsuites = this.getTestsuiteInstances(driverInstance); 293 | this.reporterEvents.emit('report:run:browser', driverInstance.webdriverClient.opts.longName); 294 | async.series(testsuites, this._onTestsuiteComplete.bind(this, callback, driverName, browser)); 295 | return this; 296 | }, 297 | 298 | /** 299 | * Emits a 'tests complete' event & calls async's next() callback 300 | * 301 | * @method _onTestsuiteComplete 302 | * @param {function} callback Async's next() callback function 303 | * @param {string} driverName Name of the requested driver 304 | * @param {string} browser Name of the requested browser 305 | * @chainable 306 | * @private 307 | */ 308 | 309 | _onTestsuiteComplete: function (callback, driverName, browser) { 310 | this.driverEmitter.emit('tests:complete:' + driverName + ':' + browser); 311 | callback(); 312 | return this; 313 | }, 314 | 315 | /** 316 | * Driver runner function. 317 | * Registers event handlers for this run, 318 | * loads browser & driver configuration & instances, 319 | * emits the 'driver ready' event for the browser/driver combination 320 | * 321 | * @method run 322 | * @param {string} driverName Name of the requested driver 323 | * @param {object} driverModule Instance of the used driver module 324 | * @param {string} browser Name of the requested browser 325 | * @param {function} callback Asyncs next() callback function 326 | * @chainable 327 | */ 328 | 329 | run: function (driverName, driverModule, browser, callback) { 330 | // load browser configuration 331 | var browsersRaw = this.config.get('browsers'); 332 | var browsers = []; 333 | 334 | // Check if we have a valid browser conf, then get the data out 335 | if (browsersRaw !== null) { 336 | browsers = browsersRaw[0]; 337 | } 338 | 339 | // init the browser configuration 340 | var browserConfiguration = this.loadBrowserConfiguration(browser, browsers, driverModule); 341 | 342 | // check if we need to inject the browser alias into the browser module 343 | if (browserConfiguration.module.setBrowser) { 344 | browserConfiguration.module.setBrowser(browser); 345 | } 346 | 347 | // init the driver instance 348 | var driverInstance = driverModule.create({events: this.driverEmitter, reporter: this.reporterEvents, browser: browser, config: this.config, browserMo: browserConfiguration.module, browserConf: browserConfiguration.configuration}); 349 | // couple driver & session status events for the reporter 350 | this.coupleReporterEvents(driverName, browser); 351 | 352 | // register shutdown handler 353 | if (driverInstance.webdriverClient.opts && driverInstance.webdriverClient.opts.kill) { 354 | this.driverEmitter.on('killAll', driverInstance.webdriverClient.opts.kill.bind(driverInstance.webdriverClient.opts)); 355 | } 356 | 357 | if (driverInstance.webdriverClient.quit) { 358 | this.driverEmitter.on('killAll', driverInstance.webdriverClient.quit.bind(driverInstance.webdriverClient)); 359 | } 360 | 361 | // dispatch some (web)driver events to the reporter 362 | this.driverEmitter.on('driver:webdriver:response', function (res) { 363 | this.reporterEvents.emit('report:log:system:webdriver', 'webdriver: ' + res.statusCode + ' ' + res.method + ' ' + res.path); 364 | this.reporterEvents.emit('report:log:system:webdriver', 'webdriver: ' + res.data); 365 | }.bind(this)); 366 | 367 | // run the tests in the browser, when the driver is ready 368 | // emit the tests:complete event, when all tests have been run 369 | this.driverEmitter.on('driver:ready:' + driverName + ':' + browser, this._onDriverReady.bind(this, browser, driverName, callback, driverInstance)); 370 | return this; 371 | } 372 | }; 373 | 374 | // export driver module 375 | module.exports = Driver; 376 | -------------------------------------------------------------------------------- /lib/dalek/host.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Copyright (c) 2013 Sebastian Golasch 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a 6 | * copy of this software and associated documentation files (the "Software"), 7 | * to deal in the Software without restriction, including without limitation 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | * and/or sell copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included 13 | * in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | * DEALINGS IN THE SOFTWARE. 22 | * 23 | */ 24 | 25 | 'use strict'; 26 | 27 | // ext. libs 28 | var http = require('http'); 29 | var os = require('os'); 30 | var Q = require('q'); 31 | 32 | /** 33 | * Sets the configuration options for the 34 | * dalek remote browser executor 35 | * 36 | * @param {options} opts Configuration options 37 | * @constructor 38 | */ 39 | 40 | var Host = function (opts) { 41 | this.reporterEvents = opts.reporterEvents; 42 | this.config = opts.config; 43 | }; 44 | 45 | /** 46 | * Remote Dalek host proxy 47 | * 48 | * @module Dalek 49 | * @class Host 50 | * @part Remote 51 | * @api 52 | */ 53 | 54 | Host.prototype = { 55 | 56 | /** 57 | * Default port that the Dalek remote server is linking against 58 | * 59 | * @property defaultPort 60 | * @type {integer} 61 | * @default 9020 62 | */ 63 | 64 | defaultPort: 9020, 65 | 66 | /** 67 | * Instance of the local browser 68 | * 69 | * @property bro 70 | * @type {object} 71 | * @default null 72 | */ 73 | 74 | bro: null, 75 | 76 | /** 77 | * Instance of the reporter event emitter 78 | * 79 | * @property reporterEvents 80 | * @type {EventEmitter2} 81 | * @default null 82 | */ 83 | 84 | reporterEvents: null, 85 | 86 | /** 87 | * Instance of the dalek config 88 | * 89 | * @property config 90 | * @type {Dalek.Config} 91 | * @default null 92 | */ 93 | 94 | config: null, 95 | 96 | /** 97 | * Local configuration 98 | * 99 | * @property configuration 100 | * @type {object} 101 | * @default {} 102 | */ 103 | 104 | configuration: {}, 105 | 106 | /** 107 | * Host address of the called webdriver server 108 | * 109 | * @property remoteHost 110 | * @type {string} 111 | * @default null 112 | */ 113 | 114 | remoteHost: null, 115 | 116 | /** 117 | * Path of the webdriver server endpoint 118 | * 119 | * @property remotePath 120 | * @type {string} 121 | * @default null 122 | */ 123 | 124 | remotePath: null, 125 | 126 | /** 127 | * Port of the called webdriver server 128 | * 129 | * @property remotePort 130 | * @type {string} 131 | * @default null 132 | */ 133 | 134 | remotePort: null, 135 | 136 | /** 137 | * Secret that got emitted by the remote instance 138 | * 139 | * @property remoteSecret 140 | * @type {string} 141 | * @default null 142 | */ 143 | 144 | remoteSecret: null, 145 | 146 | /** 147 | * Identifier of the remote client 148 | * 149 | * @property remoteId 150 | * @type {string} 151 | * @default null 152 | */ 153 | 154 | remoteId: null, 155 | 156 | /** 157 | * Secret that is stored in the local instance 158 | * 159 | * @property secret 160 | * @type {string} 161 | * @default null 162 | */ 163 | 164 | secret: null, 165 | 166 | /** 167 | * Incoming message that needs to be proxied 168 | * to the local webdriver server 169 | * 170 | * @property proxyRequest 171 | * @type {http.IncomingMessage} 172 | * @default null 173 | */ 174 | 175 | proxyRequest: null, 176 | 177 | /** 178 | * Starts the remote proxy server, 179 | * prepares the config 180 | * 181 | * @method run 182 | * @param {object} opts Configuration options 183 | * @chainable 184 | */ 185 | 186 | run: function (opts) { 187 | // apply configuration 188 | this.configuration = this.config.get('host') || {}; 189 | this.configuration.host = this.configuration.host ? !this.configuration.port : 'localhost'; 190 | this.secret = this.configuration.secret ? this.configuration.secret : this.secret; 191 | if (!this.configuration.port || opts.port) { 192 | this.configuration.port = opts.port ? opts.port : this.defaultPort; 193 | } 194 | 195 | // start the proxy server// emit the instance ready event 196 | this.server = http.createServer(this._createServer.bind(this)).listen(this.configuration.port, this.reporterEvents.emit.bind(this.reporterEvents, 'report:remote:ready', {ip: this._getLocalIp(), port: this.configuration.port})); 197 | return this; 198 | }, 199 | 200 | /** 201 | * Shutdown the proxy server 202 | * 203 | * @method kill 204 | * @return {object} Promise 205 | */ 206 | 207 | kill: function () { 208 | var deferred = Q.defer(); 209 | this.server.close(deferred.resolve); 210 | return deferred.promise; 211 | }, 212 | 213 | /** 214 | * Launches the local browser 215 | * 216 | * @method _launcher 217 | * @param {object} request Request from the dalek remote caller 218 | * @param {object} response Response to the dalek remote caller 219 | * @private 220 | * @chainable 221 | */ 222 | 223 | _launcher: function (request, response) { 224 | // extract the browser id from the request url 225 | var browser = this._extractBrowser(request.url); 226 | 227 | // load local browser module 228 | this.bro = this._loadBrowserModule(browser, response); 229 | 230 | // launch the local browser 231 | if (this.bro) { 232 | this.bro 233 | .launch({}, this.reporterEvents, this.config) 234 | .then(this._onBrowserLaunch.bind(this, browser, response)); 235 | } 236 | 237 | return this; 238 | }, 239 | 240 | /** 241 | * Shuts the local browser down, 242 | * end the otherwise hanging request 243 | * 244 | * @method _launcher 245 | * @param {object} response Response to the dalek remote caller 246 | * @private 247 | * @chainable 248 | */ 249 | 250 | _killer: function (response) { 251 | if (this.bro) { 252 | this.bro.kill(); 253 | } 254 | response.setHeader('Connection', 'close'); 255 | response.end(); 256 | this.reporterEvents.emit('report:remote:closed', {id: this.remoteId, browser: this.bro.longName}); 257 | return this; 258 | }, 259 | 260 | /** 261 | * Requires the local browser module & returns it 262 | * 263 | * @method _loadBrowserModule 264 | * @param {string} browser Name of the browser to load 265 | * @param {object} response Response to the dalek remote caller 266 | * @return {object} The local browser module 267 | * @private 268 | */ 269 | 270 | _loadBrowserModule: function (browser, response) { 271 | var bro = null; 272 | try { 273 | bro = require('dalek-browser-' + browser); 274 | } catch (e) { 275 | try { 276 | bro = require('dalek-browser-' + browser + '-canary'); 277 | } catch (e) { 278 | response.setHeader('Connection', 'close'); 279 | response.end(JSON.stringify({error: 'The requested browser "' + browser + '" could not be loaded'})); 280 | } 281 | } 282 | 283 | return bro; 284 | }, 285 | 286 | /** 287 | * Stores network data from the local browser instance, 288 | * sends browser specific data to the client 289 | * 290 | * @method _onBrowserLaunch 291 | * @param {string} browser Name of the browser to load 292 | * @param {object} response Response to the dalek remote caller 293 | * @chainable 294 | * @private 295 | */ 296 | 297 | _onBrowserLaunch: function (browser, response) { 298 | this.remoteHost = this.bro.getHost(); 299 | this.remotePort = this.bro.getPort(); 300 | this.remotePath = this.bro.path.replace('/', ''); 301 | this.reporterEvents.emit('report:remote:established', {id: this.remoteId, browser: this.bro.longName}); 302 | response.setHeader('Connection', 'close'); 303 | response.end(JSON.stringify({browser: browser, caps: this.bro.desiredCapabilities, defaults: this.bro.driverDefaults, name: this.bro.longName})); 304 | return this; 305 | }, 306 | 307 | /** 308 | * Dispatches all incoming requests, 309 | * possible endpoints local webdriver server, 310 | * browser launcher, browser shutdown handler 311 | * 312 | * @method _createServer 313 | * @param {object} request Request from the dalek remote caller 314 | * @param {object} response Response to the dalek remote caller 315 | * @private 316 | * @chainable 317 | */ 318 | 319 | _createServer: function (request, response) { 320 | // delegate calls based on url 321 | if (request.url.search('/dalek/launch/') !== -1) { 322 | 323 | // store the remotes ip address 324 | this.remoteId = request.connection.remoteAddress; 325 | 326 | // store the remote secret 327 | if (request.headers['secret-token']) { 328 | this.remoteSecret = request.headers['secret-token']; 329 | } 330 | 331 | // check if the secrets match, then launch browser 332 | // else emit an error 333 | if (this.secret === this.remoteSecret) { 334 | this._launcher(request, response); 335 | } else { 336 | response.setHeader('Connection', 'close'); 337 | response.end(JSON.stringify({error: 'Secrets do not match'})); 338 | } 339 | 340 | } else if (request.url.search('/dalek/kill') !== -1) { 341 | this._killer(response); 342 | } else { 343 | this.proxyRequest = http.request(this._generateProxyRequestOptions(request.headers, request.method, request.url), this._onProxyRequest.bind(this, response, request)); 344 | request.on('data', this._onRequestDataChunk.bind(this)); 345 | request.on('end', this.proxyRequest.end.bind(this.proxyRequest)); 346 | } 347 | 348 | return this; 349 | }, 350 | 351 | /** 352 | * Proxies data from the local webdriver server to the client 353 | * 354 | * @method _onRequestDataChunk 355 | * @param {buffer} chunk Chunk of the incoming request data 356 | * @private 357 | * @chainable 358 | */ 359 | 360 | _onRequestDataChunk: function (chunk) { 361 | this.proxyRequest.write(chunk, 'binary'); 362 | return this; 363 | }, 364 | 365 | /** 366 | * Proxies remote data to the webdriver server 367 | * 368 | * @method _onProxyRequest 369 | * @param {object} request Request from the dalek remote caller 370 | * @param {object} response Response to the dalek remote caller 371 | * @param {object} res Response to the local webdriver server 372 | * @private 373 | * @chainable 374 | */ 375 | 376 | _onProxyRequest: function (response, request, res) { 377 | var chunks = []; 378 | 379 | // deny access if the remote ids (onitial request, webdriver request) do not match 380 | if (this.remoteId !== request.connection.remoteAddress) { 381 | response.setHeader('Connection', 'close'); 382 | response.end(); 383 | return this; 384 | } 385 | 386 | res.on('data', function (chunk) { 387 | chunks.push(chunk+''); 388 | }).on('end', this._onProxyRequestEnd.bind(this, res, response, request, chunks)); 389 | return this; 390 | }, 391 | 392 | /** 393 | * Handles data exchange between the client and the 394 | * local webdriver server 395 | * 396 | * @method _onProxyRequest 397 | * @param {object} request Request from the dalek remote caller 398 | * @param {object} response Response to the dalek remote caller 399 | * @param {object} res Response to the local webdriver server 400 | * @param {array} chunks Array of received data pieces that should be forwarded to the local webdriver server 401 | * @private 402 | * @chainable 403 | */ 404 | 405 | _onProxyRequestEnd: function (res, response, request, chunks) { 406 | var buf = ''; 407 | 408 | // proxy headers for the session request 409 | if (request.url === '/session') { 410 | response.setHeader('Connection', 'close'); 411 | Object.keys(res.headers).forEach(function (key) { 412 | response.setHeader(key, res.headers[key]); 413 | }); 414 | } 415 | 416 | if (chunks.length) { 417 | buf = chunks.join(''); 418 | } 419 | 420 | response.write(buf); 421 | response.end(); 422 | return this; 423 | }, 424 | 425 | /** 426 | * Extracts the browser that should be launched 427 | * from the launch url request 428 | * 429 | * @method _extractBrowser 430 | * @param {string} url Url to parse 431 | * @return {string} Extracted browser 432 | * @private 433 | */ 434 | 435 | _extractBrowser: function (url) { 436 | return url.replace('/dalek/launch/', ''); 437 | }, 438 | 439 | /** 440 | * Generates the request options from the incoming 441 | * request that should then be forwared to the local 442 | * webdriver server 443 | * 444 | * @method _generateProxyRequestOptions 445 | * @param {object} header Header meta data 446 | * @param {string} method HTTP method 447 | * @param {string} url Webriver server endpoint url 448 | * @return {object} Request options 449 | * @private 450 | */ 451 | 452 | _generateProxyRequestOptions: function (headers, method, url) { 453 | var options = { 454 | host: this.remoteHost, 455 | port: this.remotePort, 456 | path: this.remotePath + url, 457 | method: method, 458 | headers: { 459 | 'Content-Type': headers['content-type'], 460 | 'Content-Length': headers['content-length'] 461 | } 462 | }; 463 | 464 | // check if the path is valid, 465 | // else prepend a `root` slash 466 | if (options.path.charAt(0) !== '/') { 467 | options.path = '/' + options.path; 468 | } 469 | 470 | return options; 471 | }, 472 | 473 | /** 474 | * Gets the local ip address 475 | * (should be the IPv4 address where the runner is accessible from) 476 | * 477 | * @method _getLocalIp 478 | * @return {string} Local IP address 479 | * @private 480 | */ 481 | 482 | _getLocalIp: function () { 483 | var ifaces = os.networkInterfaces(); 484 | var address = [null]; 485 | for (var dev in ifaces) { 486 | var alias = [0]; 487 | ifaces[dev].forEach(this._grepIp.bind(this, alias, address)); 488 | } 489 | 490 | return address[0]; 491 | }, 492 | 493 | /** 494 | * Tries to find the local IP address 495 | * 496 | * @method _grepIp 497 | * @param 498 | * @param 499 | * @param 500 | * @chainable 501 | * @private 502 | */ 503 | 504 | _grepIp: function (alias, address, details) { 505 | if (details.family === 'IPv4') { 506 | if (details.address !== '127.0.0.1') { 507 | address[0] = details.address; 508 | } 509 | ++alias[0]; 510 | } 511 | 512 | return this; 513 | } 514 | 515 | }; 516 | 517 | module.exports = Host; -------------------------------------------------------------------------------- /lib/dalek/remote.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Copyright (c) 2013 Sebastian Golasch 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a 6 | * copy of this software and associated documentation files (the "Software"), 7 | * to deal in the Software without restriction, including without limitation 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | * and/or sell copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included 13 | * in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | * DEALINGS IN THE SOFTWARE. 22 | * 23 | */ 24 | 25 | 'use strict'; 26 | 27 | // ext. libs 28 | var Q = require('q'); 29 | var http = require('http'); 30 | 31 | /** 32 | * Mimics a real browser that runs in a remote dalek instance 33 | * 34 | * @module Remote 35 | * @class Remote 36 | * @namespace Dalek 37 | */ 38 | 39 | var Remote = { 40 | 41 | /** 42 | * Remote webdriver path 43 | * 44 | * @property path 45 | * @type {string} 46 | * @default '' 47 | */ 48 | 49 | path: '', 50 | 51 | /** 52 | * Remote port 53 | * 54 | * @property port 55 | * @type {integer} 56 | * @default 9020 57 | */ 58 | 59 | port: 9020, 60 | 61 | /** 62 | * Remote host 63 | * 64 | * @property host 65 | * @type {string} 66 | * @default '' 67 | */ 68 | 69 | host: '', 70 | 71 | /** 72 | * Url (with placeholders) to launch browsers on the remote instance 73 | * 74 | * @property defaultLaunchUrl 75 | * @type {string} 76 | * @default http://{{host}}:{{port}}/dalek/launch/{{browser}} 77 | */ 78 | 79 | defaultLaunchUrl: 'http://{{host}}:{{port}}/dalek/launch/{{browser}}', 80 | 81 | /** 82 | * Url (with placeholders) to kill browsers on the remote instance 83 | * 84 | * @property defaultKillUrl 85 | * @type {string} 86 | * @default http://{{host}}:{{port}}/dalek/kill 87 | */ 88 | 89 | defaultKillUrl: 'http://{{host}}:{{port}}/dalek/kill', 90 | 91 | /** 92 | * Url to start a specific remote browser session 93 | * 94 | * @property launchUrl 95 | * @type {string} 96 | * @default '' 97 | */ 98 | 99 | launchUrl: '', 100 | 101 | /** 102 | * Url to kill a specific remote browser session 103 | * 104 | * @property killUrl 105 | * @type {string} 106 | * @default '' 107 | */ 108 | 109 | killUrl: '', 110 | 111 | /** 112 | * Internal config name of the browser to start remotly 113 | * 114 | * @property browser 115 | * @type {string} 116 | * @default ' 117 | */ 118 | 119 | browser: '', 120 | 121 | /** 122 | * Remote browser alias to start a browser 123 | * (browser.name or browser.actAs or user input browser alias) 124 | * 125 | * @property browserAlias 126 | * @type {string} 127 | * @default ' 128 | */ 129 | 130 | browserAlias: '', 131 | 132 | /** 133 | * Driver defaults 134 | * 135 | * @property driverDefaults 136 | * @type {object} 137 | */ 138 | 139 | driverDefaults: {}, 140 | 141 | /** 142 | * Request secret or false when unsecure 143 | * 144 | * @param secret 145 | * @type {string|bool} 146 | * @default false 147 | */ 148 | 149 | secret: false, 150 | 151 | /** 152 | * Stores & validates the incoming browser config 153 | * 154 | * @method launch 155 | * @param {object} configuration Browser configuration 156 | * @param {EventEmitter2} events EventEmitter (Reporter Emitter instance) 157 | * @param {Dalek.Internal.Config} config Dalek configuration class 158 | * @return {object} Browser promise 159 | */ 160 | 161 | launch: function (configuration, events, config) { 162 | var deferred = Q.defer(); 163 | 164 | // store injected configuration/log event handlers 165 | this.reporterEvents = events; 166 | this.configuration = configuration; 167 | this.config = config; 168 | 169 | // load configs 170 | this._loadConfigs(configuration, config); 171 | 172 | // fire up the remote browser 173 | var request = http.request(this.launchUrl, this._afterRemoteBrowserLaunched.bind(this, deferred)); 174 | 175 | // set secret header if available 176 | if (this.secret) { 177 | request.setHeader('secret-token', this.secret); 178 | } 179 | 180 | // fire the request 181 | request.end(); 182 | 183 | return deferred.promise; 184 | }, 185 | 186 | /** 187 | * Kills the remote browser 188 | * 189 | * @method kill 190 | * @return {object} Promise 191 | */ 192 | 193 | kill: function () { 194 | http.request(this.killUrl).end(); 195 | return this; 196 | }, 197 | 198 | /** 199 | * Injects the browser name 200 | * 201 | * @method setBrowser 202 | * @param {string} browser Browser to launch 203 | * @chainable 204 | */ 205 | 206 | setBrowser: function (browser) { 207 | this.browser = browser; 208 | 209 | // generate kill & launch url 210 | this.launchUrl = this._replaceUrlPlaceholder(this.defaultLaunchUrl); 211 | this.killUrl = this._replaceUrlPlaceholder(this.defaultKillUrl); 212 | return this; 213 | }, 214 | 215 | /** 216 | * Listens on the response of the initial browser launch call 217 | * and collects the response data, fires the _handshakeFinished call 218 | * after the response ended 219 | * 220 | * @method _afterRemoteBrowserLaunched 221 | * @param {object} deferred Promise 222 | * @param {object} response Browser launch response object 223 | * @chainable 224 | * @private 225 | */ 226 | 227 | _afterRemoteBrowserLaunched: function (deferred, response) { 228 | // collect remote browser information and 229 | // start the test execution after the handshake finished 230 | var data = []; 231 | response.on('data', function (chunk) { 232 | data.push(chunk+''); 233 | }).on('end', this._handshakeFinished.bind(this, deferred, data)); 234 | return this; 235 | }, 236 | 237 | /** 238 | * Parses the response data of the initial browser handshake, 239 | * sets the longName, desiredCapabilities & driverDefaults, 240 | * emits the browser data (can be used by reporters & drivers) 241 | * 242 | * @method _handshakeFinished 243 | * @param {object} deferred Promise 244 | * @param {array} data Remote browser data (longName, desiredCapabilities, driverDefaults) 245 | * @chainable 246 | * @private 247 | */ 248 | 249 | _handshakeFinished: function (deferred, data) { 250 | var br = JSON.parse(data); 251 | 252 | // check if an error happened 253 | if (!!br.error) { 254 | this.reporterEvents.emit('error', br.error); 255 | return this; 256 | } 257 | 258 | // update the desired capabilities & browser defaults in the remote instance 259 | this.longName = br.name; 260 | this.desiredCapabilities = br.caps; 261 | this.driverDefaults = br.defaults; 262 | 263 | // update the desired capabilities & browser defaults in the driver instance 264 | this.reporterEvents.emit('browser:notify:data:' + this.browser, {desiredCapabilities: this.desiredCapabilities, defaults: this.driverDefaults}); 265 | 266 | deferred.resolve(); 267 | return this; 268 | }, 269 | 270 | /** 271 | * Sets the host & port of the remote instance, 272 | * extracts the remote browser to call, 273 | * generates the launch & kill objects for this session 274 | * 275 | * @method _loadConfigs 276 | * @param {object} configuration Browser session configuration 277 | * @param {object} config Dalek configuration data 278 | * @chainable 279 | * @private 280 | */ 281 | 282 | _loadConfigs: function (configuration, config) { 283 | // set host & port 284 | this.host = configuration.host ? configuration.host : this.host; 285 | this.port = configuration.port ? configuration.port : this.port; 286 | 287 | // get the browser alias & secret 288 | this.browserAlias = this.browser; 289 | var browserConfig = config.get('browsers'); 290 | if (browserConfig && browserConfig[0] && browserConfig[0][this.browser]) { 291 | this.browserAlias = browserConfig[0][this.browser].actAs ? browserConfig[0][this.browser].actAs : this.browserAlias; 292 | this.browserAlias = browserConfig[0][this.browser].name ? browserConfig[0][this.browser].name : this.browserAlias; 293 | this.secret = browserConfig[0][this.browser].secret ? browserConfig[0][this.browser].secret : this.secret; 294 | } 295 | 296 | // generate kill & launch url 297 | this.launchUrl = this._replaceUrlPlaceholder(this.defaultLaunchUrl); 298 | this.killUrl = this._replaceUrlPlaceholder(this.defaultKillUrl); 299 | 300 | return this; 301 | }, 302 | 303 | /** 304 | * Replaces {{host}}, {{port}} & {{browser}} placeholders 305 | * in the given url with data from this.host, this.port & this.browserAlias 306 | * 307 | * @method _replaceUrlPlaceholder 308 | * @param {string} url Url with placeholder 309 | * @return {string} Url with replaced placeholders 310 | * @private 311 | */ 312 | 313 | _replaceUrlPlaceholder: function (url) { 314 | url = url.replace('{{port}}', this.port).replace('{{host}}', this.host).replace('{{browser}}', this.browserAlias); 315 | return url; 316 | } 317 | }; 318 | 319 | module.exports = Remote; -------------------------------------------------------------------------------- /lib/dalek/reporter.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Copyright (c) 2013 Sebastian Golasch 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a 6 | * copy of this software and associated documentation files (the "Software"), 7 | * to deal in the Software without restriction, including without limitation 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | * and/or sell copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included 13 | * in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | * DEALINGS IN THE SOFTWARE. 22 | * 23 | */ 24 | 25 | 'use strict'; 26 | 27 | /** 28 | * Checks & loads reporter modules 29 | * 30 | * @module DalekJS 31 | * @class Reporter 32 | * @namespace Dalek 33 | * @part Reporter 34 | * @api 35 | */ 36 | 37 | var Reporter = { 38 | 39 | /** 40 | * Reporters from the canary channel 41 | * 42 | * @param isCanary 43 | */ 44 | 45 | isCanary: {}, 46 | 47 | /** 48 | * Checks if the requested reporter exists 49 | * 50 | * @method isReporter 51 | * @param {string} reporter Name of the reporter 52 | * @return {bool} isReporter Reporter exists 53 | */ 54 | 55 | isReporter: function (reporter) { 56 | try { 57 | require.resolve('dalek-reporter-' + reporter); 58 | } catch (e) { 59 | try { 60 | require.resolve('dalek-reporter-' + reporter + '-canary'); 61 | } catch (e) { 62 | return false; 63 | } 64 | 65 | this.isCanary[reporter] = true; 66 | return true; 67 | } 68 | return true; 69 | }, 70 | 71 | /** 72 | * Loads a requested reporter 73 | * 74 | * @method loadReporter 75 | * @param {string} reporter Name of the reporter 76 | * @param {object} options Options to pass to the reporter 77 | * @return {object} reporterInstance Reporter instance 78 | */ 79 | 80 | loadReporter: function (reporter, options) { 81 | return require('dalek-reporter-' + reporter + (this.isCanary[reporter] ? '-canary' : ''))(options); 82 | } 83 | }; 84 | 85 | // export the module 86 | module.exports = Reporter; 87 | -------------------------------------------------------------------------------- /lib/dalek/suite.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Copyright (c) 2013 Sebastian Golasch 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a 6 | * copy of this software and associated documentation files (the "Software"), 7 | * to deal in the Software without restriction, including without limitation 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | * and/or sell copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included 13 | * in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | * DEALINGS IN THE SOFTWARE. 22 | * 23 | */ 24 | 25 | 'use strict'; 26 | 27 | // ext. libs 28 | var _ = require('lodash'); 29 | var fs = require('fs'); 30 | var EventEmitter2 = require('eventemitter2').EventEmitter2; 31 | 32 | // int. libs 33 | var unit = require('./unit'); 34 | 35 | /** 36 | * @constructor 37 | * @param {object} options 38 | */ 39 | 40 | var Suite = function (options) { 41 | this.emitter = new EventEmitter2(); 42 | this.emitter.setMaxListeners(Infinity); 43 | this.initialize(options); 44 | this.suite = this.loadTestsuite(options.file); 45 | }; 46 | 47 | /** 48 | * Suite (Testsuite) 49 | * 50 | * @module DalekJS 51 | * @class Suite 52 | * @namespace Dalek 53 | * @part Testsuite 54 | * @api 55 | */ 56 | 57 | Suite.prototype = { 58 | 59 | /** 60 | * Assigns the initial options 61 | * driverEmitter -> the drivers event dispatcher 62 | * reporterEmitter -> the reporters event dispatcher 63 | * driver -> the driver instance (e.g. native webdriver, selenium, etc.) 64 | * name -> the suites filename (default suite name) 65 | * 66 | * @method initialize 67 | * @param {object} options 68 | * @chainable 69 | */ 70 | 71 | initialize: function (options) { 72 | this.driverEmitter = options.driverEmitter; 73 | this.reporterEmitter = options.reporterEmitter; 74 | this.driver = options.driver; 75 | this.name = options.file; 76 | this.numberOfSuites = options.numberOfSuites; 77 | this.error = null; 78 | return this; 79 | }, 80 | 81 | /** 82 | * Loads the testsuite that should be executed 83 | * 84 | * @method loadTestsuite 85 | * @param {string} testfile 86 | * @return {object} testsuite 87 | */ 88 | 89 | loadTestsuite: function (testfile) { 90 | var suite = {}; 91 | 92 | // if the tests were passed in *as* a list of tests, just use them 93 | if (testfile && typeof testfile === 'object') { 94 | var allAreTestFunctions = true; 95 | for (var testname in testfile) { 96 | if (typeof testfile[testname] !== 'function') { allAreTestFunctions = false; } 97 | } 98 | if (allAreTestFunctions) { 99 | return testfile; 100 | } 101 | } 102 | 103 | // catch any errors, like falsy requires & stuff 104 | try { 105 | 106 | if (fs.existsSync(process.cwd() + '/' + testfile)) { 107 | suite = require(process.cwd() + '/' + testfile.replace('.js', '')); 108 | } else { 109 | this.error = 'Suite "' + testfile + '" does not exist. Skipping!'; 110 | return suite; 111 | } 112 | } catch (e) { 113 | this.error = '\n' + e.name + ': ' + e.message + '\nFailure loading suite "' + testfile + '". Skipping!' + e; 114 | return suite; 115 | } 116 | 117 | suite._uid = _.uniqueId('Suite'); 118 | return suite; 119 | }, 120 | 121 | /** 122 | * Checks if all tests from the testsuite are executed. 123 | * Runs the next test if not. 124 | * Triggers `asyncs` callback if the suite is finished. 125 | * Decrements the `testsToBeExecuted` counter 126 | * 127 | * @method testFinished 128 | * @param {function} callback 129 | * @param {array} tests 130 | * @param {object} test 131 | * @param {string} event 132 | * @chainable 133 | */ 134 | 135 | testFinished: function (callback, tests) { 136 | var complete = function() { 137 | // check if there are still tests that should be executed in this suite, 138 | // if so, run them 139 | if (this.decrementTestsToBeExecuted() > 1) { 140 | this.executeNextTest(tests); 141 | return this; 142 | } 143 | 144 | // run a function after the testsuite, if given 145 | if (this.options.teardown) { 146 | this.options.teardown(); 147 | } 148 | 149 | // emit the suite finished event 150 | this.reporterEmitter.emit('report:testsuite:finished', this.name); 151 | 152 | // move on to the next suite 153 | callback(); 154 | return this; 155 | }.bind(this); 156 | 157 | // run a function after the test, if given 158 | 159 | if (typeof this.options.afterEach === 'function') { 160 | // If there is an argument, assume async. 161 | if (this.options.afterEach.length === 1) { 162 | this.options.afterEach(function() { 163 | return complete(); 164 | }.bind(this)); 165 | } else { 166 | // no argument, assume sync. 167 | this.options.afterEach(); 168 | return complete(); 169 | } 170 | } else { 171 | return complete(); 172 | } 173 | 174 | }, 175 | 176 | /** 177 | * Decrements number of tests that should be executed in this suite 178 | * 179 | * @method decrementTestsToBeExecuted 180 | * @return {integer} numberOfTestsToBeExecuted 181 | */ 182 | 183 | decrementTestsToBeExecuted: function () { 184 | return (this.testsToBeExecuted--) -1; 185 | }, 186 | 187 | /** 188 | * Returns the name of the testsuite 189 | * If the suite has no name, it will return the testsuites filename 190 | * 191 | * @method getName 192 | * @return {string} name 193 | */ 194 | 195 | getName: function () { 196 | if (this.suite.name && _.isString(this.suite.name)) { 197 | var name = this.suite.name; 198 | delete this.suite.name; 199 | return name; 200 | } 201 | 202 | return this.name; 203 | }, 204 | 205 | /** 206 | * Returns the options of the testsuite 207 | * If the suite has no options, it will return an empty object 208 | * 209 | * @method getOptions 210 | * @return {object} options Suite options 211 | */ 212 | 213 | getOptions: function () { 214 | if (this.suite.options && _.isObject(this.suite.options)) { 215 | var options = this.suite.options; 216 | delete this.suite.options; 217 | return options; 218 | } 219 | 220 | return {}; 221 | }, 222 | 223 | /** 224 | * Returns all names (aka. object keys) the tests that should be executed 225 | * 226 | * @method getTests 227 | * @return {array} test 228 | */ 229 | 230 | getTests: function () { 231 | return Object.keys(this.suite); 232 | }, 233 | 234 | /** 235 | * Returns the number of tests to be executed 236 | * 237 | * @method getNumberOfTests 238 | * @param {array} tests 239 | * @return {integer} numberOfTests 240 | */ 241 | 242 | getNumberOfTests: function (tests) { 243 | return tests.length; 244 | }, 245 | 246 | /** 247 | * Returns the next test, that should be executed 248 | * 249 | * @method getNextTest 250 | * @return {string} testName 251 | */ 252 | 253 | getNextTest: function (tests) { 254 | return tests.shift(); 255 | }, 256 | 257 | /** 258 | * Executes the next test in the sequence 259 | * 260 | * @method executeNextTest 261 | * @param {array} tests 262 | * @return {mixed} testValue 263 | */ 264 | 265 | executeNextTest: function (tests, callback) { 266 | var cb = callback || function() {}; 267 | // grab the next test in the queue 268 | var testName = this.getNextTest(tests); 269 | // get the next test function 270 | var testFunction = this.getTest(testName); 271 | // generate an instance of the test 272 | var test = this.getTestInstance(testName); 273 | // run a setup function before the test, if given 274 | if (typeof this.options.beforeEach !== 'function') { 275 | cb(null, null); 276 | testFunction.apply(test,[test]); 277 | return this; 278 | } 279 | if (this.options.beforeEach.length === 1) { 280 | // if function takes an argument, assume async 281 | this.options.beforeEach(function() { 282 | // start it 283 | testFunction.apply(test,[test]); 284 | cb(null, null); 285 | }); 286 | } else { 287 | // otherwise, assume sync 288 | this.options.beforeEach(); 289 | testFunction.apply(test,[test]); 290 | cb(null, null); 291 | } 292 | return this; 293 | }, 294 | 295 | /** 296 | * Generates a new test instance 297 | * 298 | * @method getTestInstance 299 | * @param {string} name 300 | * @return {Dalek.Test} test 301 | */ 302 | 303 | getTestInstance: function (name) { 304 | return unit({events: this.emitter, driver: this.driver, reporter: this.reporterEmitter, name: name}); 305 | }, 306 | 307 | /** 308 | * Returns a test function by its name 309 | * 310 | * @method getTest 311 | * @param {string} name 312 | * @return {function} test 313 | */ 314 | 315 | getTest: function (name) { 316 | return this.suite[name] && _.isFunction(this.suite[name]) ? this.suite[name] : this.testDoesNotExist; 317 | }, 318 | 319 | /** 320 | * Will be executed if a test is started, that does not exist 321 | * 322 | * @method testDoesNotExist 323 | * @param {object} options 324 | */ 325 | 326 | testDoesNotExist: function (options) { 327 | if (options.name) { 328 | this.reporterEmitter.emit('warning', 'Test "' + options.name + '" does not exist! Skipping.'); 329 | } 330 | return this; 331 | }, 332 | 333 | /** 334 | * Runs any tests from this testsuite in sequence 335 | * 336 | * @method run 337 | * @param {function} callback 338 | * @chainable 339 | */ 340 | 341 | run: function (callback) { 342 | var tests = []; 343 | 344 | // check if the suite is 345 | if (this.error) { 346 | this.reporterEmitter.emit('report:testsuite:started', null); 347 | // emit a warning notice 348 | this.reporterEmitter.emit('warning', this.error); 349 | // emit the suite finished event 350 | this.reporterEmitter.emit('report:testsuite:finished', null); 351 | // move on to the next suite 352 | callback(); 353 | } 354 | 355 | // extract suite name 356 | this.name = this.getName(); 357 | // extract suite options 358 | this.options = this.getOptions(); 359 | 360 | // extract tests 361 | tests = this.getTests(); 362 | this.testsToBeExecuted = this.numberOfTests = this.getNumberOfTests(tests); 363 | 364 | // run a function before the testsuite has been launched, if given 365 | if (this.options.setup) { 366 | this.options.setup(); 367 | } 368 | 369 | // kickstart the test execution 370 | this.executeNextTest(tests, function() { 371 | // emit the suite started event 372 | this.reporterEmitter.emit('report:testsuite:started', this.name); 373 | // listen to the test:finished event & then start the next test 374 | // if there are no tests in this suite left, 375 | // run the async callback & mark this suite as finished 376 | this.emitter.onAny(this.testFinished.bind(this, callback, tests)); 377 | }.bind(this)); 378 | 379 | return this; 380 | } 381 | }; 382 | 383 | // export the testuite instance 384 | module.exports = Suite; 385 | -------------------------------------------------------------------------------- /lib/dalek/timer.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Copyright (c) 2013 Sebastian Golasch 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a 6 | * copy of this software and associated documentation files (the "Software"), 7 | * to deal in the Software without restriction, including without limitation 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | * and/or sell copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included 13 | * in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | * DEALINGS IN THE SOFTWARE. 22 | * 23 | */ 24 | 25 | 'use strict'; 26 | 27 | /** 28 | * Initializes the timers default values 29 | * 30 | * @constructor 31 | * @class Timer 32 | */ 33 | 34 | var Timer = function () { 35 | this.timerData = [0, 0]; 36 | this.interval = [0, 0]; 37 | }; 38 | 39 | /** 40 | * Timing module to measure test run times 41 | * 42 | * @module DalekJS 43 | * @class Timer 44 | * @namespace Dalek 45 | * @part Timer 46 | * @api 47 | */ 48 | 49 | Timer.prototype = { 50 | 51 | /** 52 | * Starts the timer 53 | * 54 | * @method start 55 | * @chainable 56 | */ 57 | 58 | start: function () { 59 | this.timerData = process.hrtime(); 60 | return this; 61 | }, 62 | 63 | /** 64 | * Stops the timer 65 | * 66 | * @method stop 67 | * @chainable 68 | */ 69 | 70 | stop: function () { 71 | this.interval = process.hrtime(this.timerData); 72 | return this; 73 | }, 74 | 75 | /** 76 | * Returns the elapsed time in ms 77 | * 78 | * @method getElapsedTime 79 | * @return {float} 80 | */ 81 | 82 | getElapsedTime: function () { 83 | return (this.interval[0]*1e3 + this.interval[1]/1e6) / 1000; 84 | }, 85 | 86 | /** 87 | * Returns an object with test run time information 88 | * containing hours, minutes & seconds 89 | * 90 | * @method getElapsedTimeFormatted 91 | * @return {Object} 92 | */ 93 | 94 | getElapsedTimeFormatted: function () { 95 | var hours, minutes, seconds; 96 | var elapsedTimeInSeconds = this.getElapsedTime(); 97 | 98 | // assign elapsed time (in seconds) to the seconds output 99 | seconds = elapsedTimeInSeconds; 100 | 101 | // check if the elapsed time in seconds is more than a minute 102 | // and convert the raw seconds to minutes & seconds 103 | if (elapsedTimeInSeconds > 60) { 104 | minutes = Math.floor(elapsedTimeInSeconds / 60); 105 | seconds = elapsedTimeInSeconds - minutes * 60; 106 | } 107 | 108 | // check if the elapsed time in minutes is more than an hour 109 | // and convert the raw seconds to hours, minutes & seconds 110 | if (minutes > 60) { 111 | hours = Math.floor(elapsedTimeInSeconds / 3600); 112 | minutes = elapsedTimeInSeconds - hours * 60; 113 | seconds = elapsedTimeInSeconds - minutes * 3600; 114 | } 115 | 116 | return {hours: hours, minutes: minutes, seconds: seconds}; 117 | } 118 | }; 119 | 120 | // export the timer module 121 | module.exports = Timer; 122 | -------------------------------------------------------------------------------- /lib/dalek/unit.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Copyright (c) 2013 Sebastian Golasch 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a 6 | * copy of this software and associated documentation files (the "Software"), 7 | * to deal in the Software without restriction, including without limitation 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | * and/or sell copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included 13 | * in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | * DEALINGS IN THE SOFTWARE. 22 | * 23 | */ 24 | 25 | 'use strict'; 26 | 27 | // Ext. libs 28 | var _ = require('lodash'); 29 | var Q = require('q'); 30 | 31 | // Int. libs 32 | var actions = require('./actions'); 33 | var assertions = require('./assertions'); 34 | 35 | // Default timeout for calling done 36 | var DONE_TIMEOUT = 10000; 37 | 38 | /** 39 | * Prepares the test instance values 40 | * 41 | * @param {object} opts Options like the tests name, etc. 42 | * @constructor 43 | */ 44 | 45 | var Unit = function (opts) { 46 | // prepare meta queue data 47 | this.actionPromiseQueue = []; 48 | 49 | // prepare assertion data 50 | this.expectation = null; 51 | this.runnedExpactations = 0; 52 | this.failedAssertions = 0; 53 | 54 | // prepare test specific data 55 | this.name = opts.name; 56 | this.lastChain = []; 57 | this.uuids = {}; 58 | this.contextVars = {}; 59 | 60 | if (this.name) { 61 | this.timeoutForDone = setTimeout(function () { 62 | this.done(); 63 | this.reporter.emit('warning', 'done() not called before timeout!'); 64 | }.bind(this), DONE_TIMEOUT); 65 | } 66 | }; 67 | 68 | /** 69 | * Generates an test instance 70 | * 71 | * @module DalekJS 72 | * @class Unit 73 | * @namespace Dalek 74 | * @part test 75 | * @api 76 | */ 77 | 78 | Unit.prototype = { 79 | 80 | /** 81 | * Specify how many assertions are expected to run within a test. 82 | * Very useful for ensuring that all your callbacks and assertions are run. 83 | * 84 | * @method expect 85 | * @param {Integer} expecatation Number of assertions that should be run 86 | * @chainable 87 | */ 88 | 89 | expect: function (expectation) { 90 | this.expectation = parseInt(expectation, 10); 91 | return this; 92 | }, 93 | 94 | /** 95 | * Global data store (works between the node & browser envs) 96 | * 97 | * @method data 98 | * @param {string|number} key Key to store or fetch data 99 | * @param {mixed} value *optional* Data that should be stored 100 | * @return {mixed} Data that has been stored 101 | * @chainable 102 | */ 103 | 104 | data: function (key, value) { 105 | if (value) { 106 | this.contextVars[key] = value; 107 | return this; 108 | } 109 | 110 | return this.contextVars[key]; 111 | }, 112 | 113 | /** 114 | * Increment the number of executed assertions 115 | * 116 | * @method incrementExpectations 117 | * @chainable 118 | */ 119 | 120 | incrementExpectations: function () { 121 | this.runnedExpactations++; 122 | return this; 123 | }, 124 | 125 | /** 126 | * Increment the number of failed assertions 127 | * 128 | * @method incrementFailedAssertions 129 | * @chainable 130 | */ 131 | 132 | incrementFailedAssertions: function () { 133 | this.failedAssertions++; 134 | return this; 135 | }, 136 | 137 | /** 138 | * Checks if the runned tests fullfill the set expectations 139 | * or if no expectations were raised 140 | * 141 | * @method checkExpectations 142 | * @return {bool} checkedExpectations Expectations match 143 | */ 144 | 145 | checkExpectations: function () { 146 | return (this.expectation === null || !this.expectation || (this.runnedExpactations === this.expectation)); 147 | }, 148 | 149 | /** 150 | * Checks if all runned assertions passed 151 | * 152 | * @method checkAssertions 153 | * @return {bool} assertionFailed Any expectation failed 154 | */ 155 | 156 | checkAssertions: function () { 157 | return this.failedAssertions === 0; 158 | }, 159 | 160 | /** 161 | * Sets up all the bindings needed for a test to run 162 | * 163 | * @method done 164 | * @return {object} result A promise 165 | * @private 166 | */ 167 | 168 | done: function () { 169 | var result = Q.resolve(); 170 | // clear the done error timeout 171 | clearTimeout(this.timeoutForDone); 172 | // remove all previously attached event listeners to clear the message queue 173 | this.driver.events.removeAllListeners('driver:message'); 174 | // resolve the deferred when the test is finished 175 | Unit.testStarted.fin(this._testFinished.bind(this, result)); 176 | return result; 177 | }, 178 | 179 | /** 180 | * Allow to use custom functions in order to embrace code reuse across 181 | * multiple files (for example for use in Page Objects). 182 | * 183 | * @method andThen 184 | * @param {function} a function, where 'this' references the test 185 | * @chainable 186 | */ 187 | andThen: function(func) { 188 | return func.call(this); 189 | }, 190 | 191 | /** 192 | * Adds a node style function (with node err callback) style to the test. 193 | * 194 | * @method node 195 | * @param {function} a node function that is executed in the context of the test. 196 | * @chainable 197 | */ 198 | node: function(nodeFunction) { 199 | var deferred = Q.defer(), 200 | that = this; 201 | nodeFunction.call(this, function(err) { 202 | if (typeof err !== 'undefined') { 203 | that.reporter.emit('error', err); 204 | that.incrementFailedAssertions(); 205 | deferred.reject(); 206 | } else { 207 | deferred.resolve(); 208 | } 209 | }); 210 | this.promise(deferred); 211 | return this; 212 | }, 213 | 214 | /** 215 | * Adds a promise to the chain of tests. 216 | * 217 | * @method promise 218 | * @param {promise} a q promise 219 | * @chainable 220 | */ 221 | promise: function(deferred) { 222 | this.actionPromiseQueue.push(deferred); 223 | return this; 224 | }, 225 | 226 | /** 227 | * Emits the test finished events & resolves all promises 228 | * when its done 229 | * 230 | * @method _testFinished 231 | * @param {object} result Promised result var 232 | * @return {object} result Promised result var 233 | * @private 234 | */ 235 | 236 | _testFinished: function (result) { 237 | // add a last deferred function on the end of the action queue, 238 | // to tell that this test is finished 239 | this.actionPromiseQueue.push(this._testFin.bind(this)); 240 | 241 | // initialize all of the event receiver functions, 242 | // that later take the driver result 243 | this.actionPromiseQueue.forEach(function (f) { 244 | result = result.then(f).fail(function () { 245 | console.error(arguments); 246 | process.exit(0); 247 | }); 248 | }.bind(this)); 249 | 250 | // run the driver when all actions are stored in the queue 251 | Q.allSettled(this.actionPromiseQueue) 252 | .then(this.driver.run.bind(this.driver)); 253 | 254 | return result; 255 | }, 256 | 257 | /** 258 | * Emits the test started event 259 | * 260 | * @method _reportTestStarted 261 | * @param {string} name Name of the test 262 | * @chainable 263 | * @private 264 | */ 265 | 266 | _reportTestStarted: function (name) { 267 | this.reporter.emit('report:test:started', {name: name}); 268 | return this; 269 | }, 270 | 271 | /** 272 | * Checks if the test run is complete & emits/resolves 273 | * all the needed events/promises when the run is complete 274 | * 275 | * @method _onDriverMessage 276 | * @param {object} data Data that is returned by the driver:message event 277 | * @chainable 278 | * @private 279 | */ 280 | 281 | _onDriverMessage: function (data) { 282 | // check if the test run is complete 283 | if (data && data.key === 'run.complete') { 284 | // emit the test finish events & resolve the deferred 285 | this._emitConcreteTestFinished(); 286 | this._emitAssertionStatus(); 287 | this._emitTestFinished(); 288 | this.deferred.resolve(); 289 | } 290 | 291 | return this; 292 | }, 293 | 294 | /** 295 | * Emits an event, that the current test run has been finished 296 | * 297 | * @method _emitConcreteTestFinished 298 | * @chainable 299 | * @private 300 | */ 301 | 302 | _emitConcreteTestFinished: function () { 303 | this.events.emit('test:' + this._uid + ':finished', 'test:finished', this); 304 | return this; 305 | }, 306 | 307 | /** 308 | * Emits an event that describes the current state of all assertions 309 | * 310 | * @method _emitAssertionStatus 311 | * @chainable 312 | * @private 313 | */ 314 | 315 | _emitAssertionStatus: function () { 316 | this.reporter.emit('report:assertion:status', { 317 | expected: (this.expectation ? this.expectation : this.runnedExpactations), 318 | run: this.runnedExpactations, 319 | status: this._testStatus() 320 | }); 321 | return this; 322 | }, 323 | 324 | /** 325 | * Get the overall test status (assertions & expectation) 326 | * 327 | * @method _testStatus 328 | * @return {bool} status The test status 329 | * @chainable 330 | * @private 331 | */ 332 | 333 | _testStatus: function () { 334 | return this.checkExpectations() && this.checkAssertions(); 335 | }, 336 | 337 | /** 338 | * Emits an event that describes the current state of all assertions. 339 | * The event should be fired when a test is finished 340 | * 341 | * @method _emitTestFinished 342 | * @chainable 343 | * @private 344 | */ 345 | 346 | _emitTestFinished: function () { 347 | this.reporter.emit('report:test:finished', { 348 | name: this.name, 349 | id: this._uid, 350 | passedAssertions: this.runnedExpactations - this.failedAssertions, 351 | failedAssertions: this.failedAssertions, 352 | runnedExpactations: this.runnedExpactations, 353 | status: this._testStatus(), 354 | nl: true 355 | }); 356 | 357 | return this; 358 | }, 359 | 360 | /** 361 | * Kicks off the test & binds all promises/events 362 | * 363 | * @method _testFin 364 | * @return {object} promise A promise 365 | * @private 366 | */ 367 | 368 | _testFin: function () { 369 | this.deferred = Q.defer(); 370 | 371 | if (_.isFunction(this.driver.end)) { 372 | this.driver.end(); 373 | } 374 | 375 | // emit report startet event 376 | this._reportTestStarted(this.name); 377 | 378 | // listen to all the messages from the driver 379 | this.driver.events.on('driver:message', this._onDriverMessage.bind(this)); 380 | return this.deferred.promise; 381 | }, 382 | 383 | /** 384 | * Copies assertion methods 385 | * 386 | * @method _inheritAssertions 387 | * @param {Test} test Instacne of test 388 | * @chainable 389 | * @private 390 | */ 391 | 392 | _inheritAssertions: function (test) { 393 | ['is'].forEach(function (method) { 394 | test[method] = test.assert[method].bind(test.assert); 395 | }); 396 | return test; 397 | }, 398 | 399 | /** 400 | * Copies assertion helper methods 401 | * 402 | * @method _inheritAssertions 403 | * @param {Test} test Instacne of test 404 | * @chainable 405 | * @private 406 | */ 407 | 408 | _inheritAssertionHelpers: function (test) { 409 | ['not', 'between', 'gt', 'gte', 'lt', 'lte', 'equalsCaseInsensitive'].forEach(function (method) { 410 | test.is[method] = test.assert[method].bind(test.assert); 411 | test.assert.is[method] = test.assert[method].bind(test.assert); 412 | }); 413 | ['contain', 'match'].forEach(function (method) { 414 | test.to = test.to || {}; 415 | test.assert.to = test.assert.to || {}; 416 | 417 | test.to[method] = test.assert[method].bind(test.assert); 418 | test.assert.to[method] = test.assert[method].bind(test.assert); 419 | }); 420 | ['notContain'].forEach(function (method) { 421 | var apiName = method.substr(3, 1).toLowerCase() + method.substr(4); 422 | test.to.not = test.to.not || {}; 423 | test.assert.to.not = test.assert.to.not || {}; 424 | 425 | test.to.not[apiName] = test.assert[method].bind(test.assert); 426 | test.assert.to.not[apiName] = test.assert[method].bind(test.assert); 427 | }); 428 | return test; 429 | }, 430 | 431 | /** 432 | * Set up the instance 433 | * 434 | * @method _inheritAssertions 435 | * @param {Test} test Instacne of test 436 | * @param {object} opts Options 437 | * @chainable 438 | * @private 439 | */ 440 | 441 | _initialize: function (test, opts) { 442 | test._uid = _.uniqueId('test'); 443 | test.events = opts.events; 444 | test.driver = opts.driver; 445 | test.reporter = opts.reporter; 446 | return test; 447 | } 448 | 449 | }; 450 | 451 | /** 452 | * Alias for 'andThen'; use if it is the first function called in the test case. 453 | * 454 | * @method start 455 | * @chainable 456 | */ 457 | Unit.prototype.start = Unit.prototype.andThen; 458 | 459 | // export a function that generates a new test instance 460 | module.exports = function (opts) { 461 | // mixin assertions, actions & getters 462 | Unit.prototype = _.extend(Unit.prototype, actions({reporter: opts.reporter}).prototype); 463 | var unit = new Unit(opts); 464 | unit.assert = new (assertions())({test: unit}); 465 | unit.assert.done = unit.done.bind(this); 466 | unit.assert.query = unit.query.bind(unit.assert); 467 | unit.assert.$ = unit.query.bind(unit.assert); 468 | unit.end = unit.assert.end.bind(unit.assert); 469 | 470 | // copy log methods 471 | unit.log = {}; 472 | unit.log.dom = unit.logger.dom.bind(unit); 473 | unit.log.message = unit.logger.message.bind(unit); 474 | 475 | // copy assertions methods 476 | unit = unit._inheritAssertions(unit); 477 | 478 | // copy assertion helper methods 479 | unit = unit._inheritAssertionHelpers(unit); 480 | 481 | // initialize the instance 482 | unit = unit._initialize(unit, opts); 483 | 484 | // TODO: Promise driver start 485 | // so that we can reexecute them and clean the env between tests 486 | Unit.testStarted = unit.driver.start(Q); 487 | return unit; 488 | }; 489 | -------------------------------------------------------------------------------- /lib/dalek/uuid.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * Copyright (c) 2013 Sebastian Golasch 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a 6 | * copy of this software and associated documentation files (the "Software"), 7 | * to deal in the Software without restriction, including without limitation 8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | * and/or sell copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included 13 | * in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | * DEALINGS IN THE SOFTWARE. 22 | * 23 | */ 24 | 25 | 'use strict'; 26 | 27 | var counter = 0; 28 | 29 | module.exports = function() { 30 | return 'uuid-' + (++counter); 31 | }; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dalekjs", 3 | "version": "0.0.9", 4 | "description": "A node based cross browser testing tool", 5 | "main": "./lib/dalek.js", 6 | "directories": { 7 | "test": "test", 8 | "example": "examples" 9 | }, 10 | "dependencies": { 11 | "dalek-driver-native": "0.0.6", 12 | "dalek-reporter-console": "0.0.7", 13 | "dalek-browser-phantomjs": "0.0.4", 14 | "q": "1.1.2", 15 | "chai": "1.9.1", 16 | "async": "0.9.0", 17 | "eventemitter2": "0.4.14", 18 | "cheerio": "0.18.0", 19 | "lodash": "2.4.1", 20 | "js-yaml": "3.2.3", 21 | "json5": "0.4.0", 22 | "coffee-script": "1.8.0", 23 | "glob": "4.2.1" 24 | }, 25 | "devDependencies": { 26 | "grunt": "0.4.5", 27 | "grunt-contrib-clean": "0.6.0", 28 | "grunt-contrib-jshint": "0.10.0", 29 | "grunt-contrib-yuidoc": "0.5.2", 30 | "grunt-contrib-compress": "0.12.0", 31 | "grunt-mocha-test": "0.12.4", 32 | "grunt-complexity": "0.2.0", 33 | "grunt-plato": "1.3.0", 34 | "grunt-git-contributors": "0.1.5", 35 | "grunt-conventional-changelog": "1.1.0", 36 | "grunt-concurrent": "1.0.0", 37 | "grunt-documantix": "0.0.5", 38 | "grunt-bump": "0.0.16", 39 | "grunt-include-replace": "2.0.0", 40 | "blanket": "1.1.6", 41 | "chai": "1.10.0", 42 | "load-grunt-tasks": "1.0.0", 43 | "time-grunt": "1.0.0", 44 | "dalek-build-tools": "0.0.2" 45 | }, 46 | "scripts": { 47 | "test": "grunt test" 48 | }, 49 | "repository": { 50 | "type": "git", 51 | "url": "http://github.com/dalekjs/dalek.git" 52 | }, 53 | "bugs": { 54 | "url": "https://github.com/dalekjs/dalek/issues" 55 | }, 56 | "keywords": [ 57 | "dalekjs" 58 | ], 59 | "author": { 60 | "name": "Sebastian Golasch", 61 | "email": "public@asciidisco.com", 62 | "url": "http://asciidisco.com" 63 | }, 64 | "licenses": [ 65 | { 66 | "type": "MIT", 67 | "url": "https://github.com/dalekjs/dalek/blob/master/LICENSE-MIT" 68 | } 69 | ], 70 | "engine": { 71 | "node": ">=0.10" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/lib_dalek_TEST.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var Dalek = require('../lib/dalek.js'); 5 | 6 | describe('dalek', function () { 7 | 8 | it('should exist', function () { 9 | expect(Dalek).to.be.ok; 10 | }); 11 | 12 | it('can shutdown on empty tests', function () { 13 | var oldProcessExit = process.exit; 14 | process.exit = function (code) { 15 | expect(code).to.equal(127); 16 | }; 17 | 18 | var dalek = new Dalek({ 19 | tests: '', 20 | driver: [], 21 | reporter: [], 22 | browser: [], 23 | logLevel: 1, 24 | noColors: true, 25 | noSymbols: false, 26 | advanced: true 27 | }); 28 | 29 | dalek.driverEmitter.on('killAll', function () { 30 | expect(true).to.be.true; 31 | }); 32 | 33 | process.exit = oldProcessExit; 34 | }); 35 | 36 | it('can be initialized', function () { 37 | var dalek = new Dalek({ 38 | tests: ['test/dalek/basic.js'], 39 | driver: [], 40 | reporter: [], 41 | browser: [], 42 | logLevel: 1, 43 | noColors: false, 44 | noSymbols: false 45 | }); 46 | expect(dalek).to.be.ok; 47 | }); 48 | 49 | it('can fire run finished event', function () { 50 | var dalek = new Dalek({ 51 | tests: ['test/dalek/basic.js'], 52 | driver: [], 53 | reporter: [], 54 | browser: [], 55 | logLevel: 1, 56 | noColors: false, 57 | noSymbols: false 58 | }); 59 | 60 | dalek.reporterEvents.on('report:runner:finished', function (data) { 61 | expect(data).to.be.an('object'); 62 | }); 63 | 64 | dalek.reportRunFinished(); 65 | }); 66 | 67 | it('can fire tests complete event', function () { 68 | var dalek = new Dalek({ 69 | tests: ['test/dalek/basic.js'], 70 | driver: [], 71 | reporter: [], 72 | browser: [], 73 | logLevel: 1, 74 | noColors: false, 75 | noSymbols: false 76 | }); 77 | 78 | dalek.driverEmitter.on('tests:complete', function () { 79 | expect(true).to.be.ok; 80 | }); 81 | 82 | dalek.testsuitesFinished(); 83 | }); 84 | 85 | it('can update assertion state (success)', function () { 86 | var dalek = new Dalek({ 87 | tests: ['test/dalek/basic.js'], 88 | driver: [], 89 | reporter: [], 90 | browser: [], 91 | logLevel: 1, 92 | noColors: false, 93 | noSymbols: false 94 | }); 95 | 96 | dalek._onReportAssertion({success: true}); 97 | expect(dalek.assertionsPassed).to.equal(1); 98 | expect(dalek.assertionsFailed).to.equal(0); 99 | expect(dalek.runnerStatus).to.be.true; 100 | }); 101 | 102 | it('can update assertion state (failure)', function () { 103 | var dalek = new Dalek({ 104 | tests: ['test/dalek/basic.js'], 105 | driver: [], 106 | reporter: [], 107 | browser: [], 108 | logLevel: 1, 109 | noColors: false, 110 | noSymbols: false 111 | }); 112 | 113 | dalek._onReportAssertion({success: false}); 114 | expect(dalek.assertionsPassed).to.equal(0); 115 | expect(dalek.assertionsFailed).to.equal(1); 116 | expect(dalek.runnerStatus).to.be.false; 117 | }); 118 | 119 | it('can detect advanced options', function () { 120 | var dalek = new Dalek({ 121 | tests: ['test/dalek/basic.js'], 122 | driver: [], 123 | reporter: [], 124 | browser: [], 125 | logLevel: 1, 126 | noColors: false, 127 | noSymbols: false, 128 | advanced: true 129 | }); 130 | 131 | expect(dalek.advancedOptions).to.be.true; 132 | }); 133 | 134 | /*it('can run tests', function () { 135 | var EventEmitter = require('events').EventEmitter; 136 | var dalek = new Dalek({ 137 | tests: ['test/dalek/basic.js'], 138 | driver: [], 139 | reporter: [], 140 | browser: [], 141 | logLevel: 1, 142 | noColors: false, 143 | noSymbols: false 144 | }); 145 | 146 | dalek.reporterEvents = new EventEmitter(); 147 | dalek.reporterEvents.setMaxListeners(Infinity); 148 | dalek.reporterEvents.on('report:runner:started', function () { 149 | expect(true).to.be.true; 150 | }); 151 | dalek.run(); 152 | });*/ 153 | 154 | /*it('can catch runtime errors', function () { 155 | var dalek = new Dalek({ 156 | tests: ['test/dalek/basic.js'], 157 | driver: [], 158 | reporter: [], 159 | browser: [], 160 | logLevel: 1, 161 | noColors: false, 162 | noSymbols: false 163 | }); 164 | 165 | dalek.driverEmitter.on('killAll', function () { 166 | expect(true).to.be.true; 167 | }); 168 | 169 | try { 170 | dalek._shutdown({}); 171 | } catch (e) {} 172 | });*/ 173 | 174 | /*it('can doenst fire at appium runtime errors', function () { 175 | var dalek = new Dalek({ 176 | tests: ['test/dalek/basic.js'], 177 | driver: [], 178 | reporter: [], 179 | browser: [], 180 | logLevel: 1, 181 | noColors: false, 182 | noSymbols: false 183 | }); 184 | 185 | try { 186 | expect(dalek._shutdown({message: 'This socket has been ended by the other party'})).to.be.false; 187 | } catch (e) {} 188 | });*/ 189 | 190 | }); 191 | -------------------------------------------------------------------------------- /test/lib_dalek_actions_TEST.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //var expect = require('chai').expect; 4 | //var Actions = require('../lib/dalek/actions.js')({reporter: null}); 5 | 6 | describe('dalek-internal-actions', function() { 7 | 8 | /*it('should exist', function() { 9 | var actions = new Actions(); 10 | expect(actions).to.be.ok; 11 | });*/ 12 | 13 | }); 14 | -------------------------------------------------------------------------------- /test/lib_dalek_actions_screenshots_TEST.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var EventEmitter2 = require('eventemitter2').EventEmitter2; 5 | var Actions = require('../lib/dalek/actions.js')({reporter: null}); 6 | 7 | describe('dalek-internal-screenshot-action', function() { 8 | 9 | var actions; 10 | 11 | beforeEach(function () { 12 | actions = new Actions(); 13 | actions.reporter = new EventEmitter2(); 14 | actions.actionPromiseQueue = []; 15 | }); 16 | 17 | it('should exist', function() { 18 | expect(actions).to.be.ok; 19 | }); 20 | 21 | it('screenshot action should exist', function() { 22 | expect(actions.screenshot).to.be.ok; 23 | }); 24 | 25 | it('screenshot action should return link to this', function() { 26 | expect(actions.screenshot('test/screenshot/file')).to.equal(actions); 27 | }); 28 | 29 | it('screenshot action should add one callback function into action queue', function() { 30 | expect(actions.actionPromiseQueue.length).to.equal(0); 31 | actions.screenshot('test/screenshot/file'); 32 | expect(actions.actionPromiseQueue.length).to.equal(1); 33 | expect(actions.actionPromiseQueue[0]).to.be.an('function'); 34 | }); 35 | 36 | it('screenshot action with element should add two callback functions into action queue', function() { 37 | expect(actions.actionPromiseQueue.length).to.equal(0); 38 | actions.screenshot('test/screenshot/file', '#lga'); 39 | expect(actions.actionPromiseQueue.length).to.equal(2); 40 | expect(actions.actionPromiseQueue[1]).to.be.an('function'); 41 | }); 42 | 43 | }); 44 | -------------------------------------------------------------------------------- /test/lib_dalek_assertions_TEST.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var Assertions = require('../lib/dalek/assertions.js'); 5 | 6 | describe('dalek-internal-assertions', function() { 7 | 8 | it('should exist', function() { 9 | var assertions = new Assertions(); 10 | expect(assertions).to.be.ok; 11 | }); 12 | 13 | }); 14 | -------------------------------------------------------------------------------- /test/lib_dalek_config_TEST.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var Config = require('../lib/dalek/config.js'); 5 | 6 | describe('dalek-internal-config', function () { 7 | 8 | it('should exist', function () { 9 | expect(Config).to.be.ok; 10 | }); 11 | 12 | it('can be initialized', function () { 13 | var config = new Config({}, {tests: []}, {}); 14 | expect(config).to.be.ok; 15 | }); 16 | 17 | it('can get a value', function () { 18 | var config = new Config({foo: 'bar'}, {tests: []}, {}); 19 | expect(config.get('foo')).to.equal('bar'); 20 | }); 21 | 22 | it('can read & parse a yaml file', function () { 23 | var config = new Config({}, {tests: []}, {}); 24 | var fileContents = config.readyml(__dirname + '/mock/Dalekfile.yml'); 25 | expect(fileContents).to.include.keys('browsers'); 26 | expect(fileContents.browsers).to.be.an('array'); 27 | expect(fileContents.browsers[0]).to.be.an('object'); 28 | expect(fileContents.browsers[0]).to.include.keys('chrome'); 29 | expect(fileContents.browsers[0].chrome).to.include.keys('port'); 30 | expect(fileContents.browsers[0].chrome.port).to.equal(6000); 31 | }); 32 | 33 | it('can read & parse a json file', function () { 34 | var config = new Config({}, {tests: []}, {}); 35 | var fileContents = config.readjson(__dirname + '/mock/Dalekfile.json'); 36 | expect(fileContents).to.include.keys('browsers'); 37 | expect(fileContents.browsers).to.be.an('array'); 38 | expect(fileContents.browsers[0]).to.be.an('object'); 39 | expect(fileContents.browsers[0]).to.include.keys('chrome'); 40 | expect(fileContents.browsers[0].chrome).to.include.keys('port'); 41 | expect(fileContents.browsers[0].chrome.port).to.equal(6000); 42 | }); 43 | 44 | it('can read & parse a json5 file', function () { 45 | var config = new Config({}, {tests: []}, {}); 46 | var fileContents = config.readjson5(__dirname + '/mock/Dalekfile.json5'); 47 | expect(fileContents).to.include.keys('browsers'); 48 | expect(fileContents.browsers).to.be.an('array'); 49 | expect(fileContents.browsers[0]).to.be.an('object'); 50 | expect(fileContents.browsers[0]).to.include.keys('chrome'); 51 | expect(fileContents.browsers[0].chrome).to.include.keys('port'); 52 | expect(fileContents.browsers[0].chrome.port).to.equal(6000); 53 | }); 54 | 55 | it('can read & parse a js file', function () { 56 | var config = new Config({}, {tests: []}, {}); 57 | var fileContents = config.readjs(__dirname + '/mock/Dalekfile.js'); 58 | expect(fileContents).to.include.keys('browsers'); 59 | expect(fileContents.browsers).to.be.an('array'); 60 | expect(fileContents.browsers[0]).to.be.an('object'); 61 | expect(fileContents.browsers[0]).to.include.keys('chrome'); 62 | expect(fileContents.browsers[0].chrome).to.include.keys('port'); 63 | expect(fileContents.browsers[0].chrome.port).to.equal(6000); 64 | }); 65 | 66 | it('can read & parse a coffee-script file', function () { 67 | var config = new Config({}, {tests: []}, {}); 68 | var fileContents = config.readcoffee(__dirname + '/mock/Dalekfile.coffee'); 69 | expect(fileContents).to.include.keys('browsers'); 70 | expect(fileContents.browsers).to.be.an('array'); 71 | expect(fileContents.browsers[0]).to.be.an('object'); 72 | expect(fileContents.browsers[0]).to.include.keys('chrome'); 73 | expect(fileContents.browsers[0].chrome).to.include.keys('port'); 74 | expect(fileContents.browsers[0].chrome.port).to.equal(6000); 75 | }); 76 | 77 | it('can check the avilability of a config file', function () { 78 | var config = new Config({}, {tests: []}, {}); 79 | var path = __dirname + '/mock/Dalekfile.coffee'; 80 | expect(config.checkAvailabilityOfConfigFile(path)).to.equal(path); 81 | }); 82 | 83 | it('can verify a reporter', function () { 84 | var config = new Config({}, {tests: []}, {}); 85 | var reporter = { 86 | isReporter: function () { 87 | return true; 88 | } 89 | }; 90 | 91 | var reporters = ['foobar']; 92 | expect(config.verifyReporters(reporters, reporter)[0]).to.equal(reporters[0]); 93 | }); 94 | 95 | it('can verify a driver', function () { 96 | var config = new Config({}, {tests: []}, {}); 97 | var driver = { 98 | isDriver: function () { 99 | return true; 100 | } 101 | }; 102 | 103 | var drivers = ['foobar']; 104 | expect(config.verifyDrivers(drivers, driver)[0]).to.equal(drivers[0]); 105 | }); 106 | 107 | it('can return the previous filename if the _checkFile iterator foudn a file', function () { 108 | var config = new Config({}, {tests: []}, {}); 109 | expect(config._checkFile('foobarbaz', '', '', '')).to.equal('foobarbaz'); 110 | }); 111 | 112 | it('can check the existance of default config files', function () { 113 | var config = new Config({}, {tests: []}, {}); 114 | config.defaultFilename = __dirname + '/mock/Dalekfile'; 115 | expect(config._checkFile('js', 'coffee', 0, ['js', 'coffee'])).to.equal(__dirname + '/mock/Dalekfile.js'); 116 | }); 117 | 118 | it('can check the existance of default config files (1st in row doesnt exist, snd. does)', function () { 119 | var config = new Config({}, {tests: []}, {}); 120 | config.defaultFilename = __dirname + '/mock/Dalekfile'; 121 | expect(config._checkFile('txt', 'coffee', 0, ['txt', 'coffee'])).to.equal(__dirname + '/mock/Dalekfile.coffee'); 122 | }); 123 | 124 | }); 125 | -------------------------------------------------------------------------------- /test/lib_dalek_driver_TEST.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var Driver = require('../lib/dalek/driver.js'); 5 | 6 | describe('dalek-internal-driver', function() { 7 | 8 | it('should exist', function() { 9 | expect(Driver).to.be.ok; 10 | }); 11 | 12 | }); 13 | -------------------------------------------------------------------------------- /test/lib_dalek_reporter_TEST.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //var expect = require('chai').expect; 4 | //var Reporter = require('../index.js'); 5 | 6 | describe('dalek-internal-reporter', function() { 7 | 8 | it('should exist', function() { 9 | //expect(Reporter).to.be.ok; 10 | }); 11 | 12 | }); 13 | -------------------------------------------------------------------------------- /test/lib_dalek_suite_TEST.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var Suite = require('../lib/dalek/suite.js'); 5 | 6 | describe('dalek-internal-suite', function() { 7 | 8 | it('should exist', function() { 9 | expect(Suite).to.be.ok; 10 | }); 11 | 12 | }); 13 | -------------------------------------------------------------------------------- /test/lib_dalek_timer_TEST.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var Timer = require('../lib/dalek/timer.js'); 5 | 6 | describe('dalek-internal-timer', function () { 7 | 8 | it('should exist', function () { 9 | expect(Timer).to.be.ok; 10 | }); 11 | 12 | it('can be instanciated', function () { 13 | var timer = new Timer(); 14 | expect(timer).to.be.ok; 15 | }); 16 | 17 | it('can be started', function () { 18 | var timer = new Timer(); 19 | expect(timer.timerData[0]).to.equal(0); 20 | expect(timer.timerData[1]).to.equal(0); 21 | timer.start(); 22 | expect(timer.timerData[0]).to.be.above(0); 23 | expect(timer.timerData[1]).to.be.above(0); 24 | }); 25 | 26 | /*it('can be stopped', function (done) { 27 | var timer = new Timer(); 28 | timer.start(); 29 | expect(timer.interval[0]).to.equal(0); 30 | expect(timer.interval[1]).to.equal(0); 31 | setTimeout(function () { 32 | timer.stop(); 33 | expect(timer.interval[1]).to.be.above(0); 34 | done(); 35 | }, 200); 36 | }); 37 | 38 | it('can return elapsed time in seconds', function () { 39 | var timer = new Timer(); 40 | timer.interval = [0, 100769]; 41 | expect(timer.getElapsedTime()).to.be.above(1); 42 | }); 43 | 44 | /*it('can return formatted time object (seconds)', function (done) { 45 | var timer = new Timer(); 46 | timer.interval = [0, 20000729769]; 47 | var elapsedTime = timer.getElapsedTimeFormatted(); 48 | expect(elapsedTime.seconds).to.be.above(1); 49 | expect(elapsedTime.minutes).to.be.undefined; 50 | expect(elapsedTime.hours).to.be.undefined; 51 | }); 52 | 53 | it('can return formatted time object (minutes)', function () { 54 | var timer = new Timer(); 55 | timer.interval = [0, 1653]; 56 | var elapsedTime = timer.getElapsedTimeFormatted(); 57 | expect(elapsedTime.minutes).to.be.above(1); 58 | }); 59 | 60 | it('can return formatted time object (hours)', function () { 61 | var timer = new Timer(); 62 | timer.interval = [0, 9884759729357435616536595610]; 63 | var elapsedTime = timer.getElapsedTimeFormatted(); 64 | expect(elapsedTime.hours).to.be.above(1); 65 | });*/ 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /test/lib_dalek_unit_TEST.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | //var EventEmitter = require('events').EventEmitter; 5 | //var test = require('../index.js'); 6 | 7 | describe('dalek-internal-test', function() { 8 | 9 | it('should exist', function() { 10 | /*var testInst = test({ 11 | reporter: new EventEmitter(), 12 | driver: {start: function () {}} 13 | });*/ 14 | expect(true).to.be.ok; 15 | }); 16 | 17 | }); 18 | -------------------------------------------------------------------------------- /test/lib_dalek_uuid_TEST.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | //var EventEmitter = require('events').EventEmitter; 5 | var uuid = require('../lib/dalek/uuid.js'); 6 | 7 | describe('dalek-internal-test', function() { 8 | 9 | it('should exist', function() { 10 | expect(uuid).to.be.ok; 11 | }); 12 | 13 | }); 14 | -------------------------------------------------------------------------------- /test/mock/Dalekfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | browsers: [ 3 | chrome: 4 | port: 6000 5 | ] -------------------------------------------------------------------------------- /test/mock/Dalekfile.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | browsers: [{ 3 | chrome: { 4 | port: 6000 5 | } 6 | }] 7 | }; -------------------------------------------------------------------------------- /test/mock/Dalekfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "browsers": [{ 3 | "chrome": { 4 | "port": 6000 5 | } 6 | }] 7 | } -------------------------------------------------------------------------------- /test/mock/Dalekfile.json5: -------------------------------------------------------------------------------- 1 | { 2 | browsers: [{ 3 | chrome: { 4 | port: 6000 5 | } 6 | }] 7 | } -------------------------------------------------------------------------------- /test/mock/Dalekfile.yml: -------------------------------------------------------------------------------- 1 | browsers: 2 | - chrome: 3 | port: 6000 --------------------------------------------------------------------------------