├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── bower.json ├── duck-angular.js ├── duck-chai.js ├── karma.conf.js ├── package.json └── test ├── duckElement.html ├── duckElement.spec.js ├── test-main.js ├── trivialTest.html └── trivialTest.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | node_modules/ 3 | *.iml 4 | .idea/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to Duck Angular 2 | ============================ 3 | 4 | This guide will assume a developer wants to contribute to Duck Angular, and that is working on an environment that is absolutely a blank slate. 5 | 6 | Requirements: 7 | 8 | - NPM 9 | - git 10 | - Karma 11 | 12 | Install Duck Angular: 13 | 14 | - Clone the repository 15 | - Using npm, install Duck's dev dependencies with `npm install`. Special notes for Windows environments are below. 16 | - Run the test suite by running `karma start`. Note that if the 'karma' executable is not in your path, you may have to specify the full path. 17 | 18 | Now you have the tests installed and running, and can make changes. 19 | 20 | Important notes for Windows environments 21 | ========================================= 22 | 23 | * You'll need Node 0.10.x. This fixes a weird issue where the caret(^) versioning format is not recognised for the 'progress' NPM module. 24 | * You'll need Python 2.x. No, Python 3.x will not work, because node-gyp will fail. 25 | * You will want to install some version of Visual Studio for node-gyp to work. I've had success with [VS 2012 Express for Desktop](http://go.microsoft.com/?linkid=9816758). Note that you may have to specify the VS version when you run `npm install`. For the VS version noted above, I ran `npm install --msvs_version=2012`. 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Avishek Sen Gupta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | duck-angular 2 | ============ 3 | 4 | Guides to use this are at: 5 | 6 | * [Part One](http://avishek.net/blog/?p=1188) 7 | * [Part Two](http://avishek.net/blog/?p=1202) 8 | * [Part Three](http://avishek.net/blog/?p=1225) 9 | * [Part Four](http://avishek.net/blog/?p=1239) 10 | * [Part Five: Testing Directives](http://avishek.net/blog/?p=1489) 11 | * [A Quick Recap](http://avishek.net/blog/?p=1472) 12 | 13 | An example AngularJS app using RequireJS and Duck-Angular is at https://github.com/asengupta/AngularJS-RequireJS-Seed in two combinations: 14 | 15 | * [Mocha + RequireJS](https://github.com/asengupta/AngularJS-RequireJS-Seed/tree/master) 16 | * [Jasmine + RequireJS](https://github.com/asengupta/AngularJS-RequireJS-Seed/tree/karma-jasmine) 17 | 18 | duck-angular currently relies on RequireJS. 19 | 20 | duck-angular is a container for bootstrapping and testing AngularJS views and controllers in memory: no browser or external process needed. 21 | 22 | Setup 23 | ------ 24 | 25 | duck-angular is available as a Bower package. Install it using 'bower install duck-angular'. 26 | 27 | If you intend to set up Duck manually in an environment where RequireJS is not available, you'll need to make sure that the following libraries are available to Duck. 28 | 29 | * q.js 30 | * require.js 31 | * text.js 32 | * underscore.js 33 | * jquery.js 34 | 35 | If you are using RequireJS in your app, Duck will detect it and attempt to load "angular", "underscore", "jquery", and "Q". 36 | 37 | Your controller/service/object initialisation scripts need to have run before you use Duck-Angular. Put them in script tags, or load them using a script loader like RequireJS or Inject. 38 | If you're not using RequireJS in your app, see the example at: [Angular-Toy](https://github.com/kylehodgson/angular-toy). 39 | Here is an example taken from the [AngularJS-RequireJS Seed app](https://github.com/asengupta/AngularJS-RequireJS-Seed) 40 | 41 | // Using Mocha-as-Promised in this example 42 | it("can reflect data that is refreshed asynchronously", function () { 43 | return mother.createMvc("route2Controller", "../templates/route2.html", {}).then(function (mvc) { 44 | var dom = new DuckDOM(mvc.view, mvc.scope); 45 | var interaction = new UIInteraction(dom); 46 | expect(dom.element("#data")[0].innerText).to.eql("Some Data"); 47 | dom.interactWith("#changeLink"); 48 | expect(dom.element("#data")[0].innerText).to.eql("Some New Data"); 49 | return interaction.with("#refreshLink").waitFor(mvc.scope, "refreshData").then(function() { 50 | expect(dom.element("#data")[0].innerText).to.eql("Some Data"); 51 | }); 52 | }); 53 | }); 54 | 55 | 56 | If using RequireJS, including duck-angular as a dependency will expose duckCtor as a parameter you use in your tests. 57 | If including duck-angular using script tags, window.duckCtor will be available to you. 58 | 59 | Initialise the application container, like so: 60 | 61 | var duckFactory = duckCtor(_, angular, Q, $); 62 | var builder = duckFactory.ContainerBuilder; 63 | var container = builder.withDependencies(appLevelDependencies).build("MyModuleName", myModule, { baseUrl: "baseUrl/for/Duck/dependencies", textPluginPath: "path/to/text.js", multipleControllers: true}); 64 | 65 | The withDependencies(...) call is optional, unless you want to inject some dependency which the controller does not use directly. 66 | 67 | The third parameter has the key `multipleControllers`. Unless specified, this is false. Setting this to true, allows us to inject dependencies not only into the top-level controller, but also on any nested controllers. 68 | 69 | ###General Setup and Simpler Setup 70 | 71 | The fragment below is the most general form of a Duck-Angular setup. Note that most of the parameters are optional. The only ones that are absolutely needed are `module` and `moduleName`. If you're not using RequireJS, you'll need to specify `baseUrl` and `textPluginPath` as well. 72 | 73 | var duckFactory = duckCtor(_, angular, Q, $); 74 | var builder = duckFactory.ContainerBuilder; 75 | var DuckDOM = duckFactory.DOM; 76 | var DuckUIInteraction = duckFactory.UIInteraction; 77 | 78 | return builder.withDependencies(appLevelDependencies) 79 | .cacheTemplates(module, templatesToCache) 80 | .then(function (bldr) { 81 | return bldr.build(moduleName, module, 82 | {baseUrl: "/base", textPluginPath: "src/javascript_tests/lib/text", multipleControllers: isMultipleControllerSupportEnabled}); 83 | }) 84 | .then(function (container) { 85 | container.addViewProcessor(function(viewHTML) { /* Processor code */ }); 86 | return container.domMvc("controllerName", "path/to/view", controllerDependencies); 87 | .then(function(dom, mvc) { 88 | // Test code 89 | }); 90 | }); 91 | 92 | Accordingly, the simplest form would look like something below: 93 | 94 | var duckFactory = duckCtor(_, angular, Q, $); 95 | var builder = duckFactory.ContainerBuilder; 96 | var DuckDOM = duckFactory.DOM; 97 | var DuckUIInteraction = duckFactory.UIInteraction; 98 | 99 | return builder.build(moduleName, module, 100 | {baseUrl: "/base", textPluginPath: "src/javascript_tests/lib/text"); 101 | }) 102 | .then(function (container) { 103 | return container.domMvc("controllerName", "path/to/view", controllerDependencies) 104 | .then(function(dom, mvc) { 105 | // Test code 106 | }); 107 | }); 108 | 109 | ##ContainerBuilder API 110 | 111 | 112 | ###withDependencies() 113 | 114 | This method allows you to specify module-level dependencies, i.e., dependencies which will be overridden for the entire module. The dependencies are specified as simple key-value pairs, with the key reflecting the actual name of the Angular dependency. If the value is an object, it will be specified configured in Angular's DI via a provider. If the value is a function, it will be executed with two parameters, $provide and the module. This lets the developer override the dependency in whatever fashion is most appropriate. The function returns the `builder` object, so it can be chained, until `build()` is called. The exception is when `cacheTemplate()` or `cacheTemplates()` is called, in which case it returns a promise with the `builder` object, which you can continue to chain as usual. See [cacheTemplate()](#cacheTemplateSection). 115 | 116 | 117 | ###cacheTemplate(moduleName, declaredPathToDirectiveTemplate, actualPathToDirectiveTemplate) 118 | 119 | This is specifically to prevent template load errors when we specify templateUrl values for directives. This preloads the templateUrl into Angular's template cache. Note that this returns a promise. You will need to wait for the promise to be fulfilled, either right after the point of the call, or before the start of the test. Here's an example of how you could do this: 120 | 121 | var setup = function(appLevelDependencies) { 122 | return buildContainer(appLevelDependencies).then(function (container) { 123 | return container.domMvc("ControllerName", "path/to/view", controllerDependencies) 124 | }); 125 | }; 126 | 127 | var buildContainer = function (appLevelDependencies) { 128 | var builder = duckFactory.ContainerBuilder; 129 | return builder.withDependencies(appLevelDependencies). 130 | cacheTemplate(moduleUnderTest, "declared/path/to/directive/template", "actual/path/to/template"). 131 | then(function (bldr) { 132 | return bldr.build("Cinnamon", cinnamon, 133 | {baseUrl: "/base", textPluginPath: "src/javascript_tests/lib/text"}); 134 | }); 135 | }; 136 | 137 | 138 | ###cacheTemplates(templateMap) 139 | 140 | If you're caching multiple templates, it's somewhat inconvenient to have to chain multiple promises for all the templates. This method lets you cache multiple templates, which you pass in as a map, keyed to the template URLs. The example below reproduces the relevant part of the single-template example above. 141 | 142 | 143 | var buildContainer = function (appLevelDependencies) { 144 | var builder = duckFactory.ContainerBuilder; 145 | return builder.withDependencies(appLevelDependencies). 146 | cacheTemplates(moduleUnderTest, { 147 | "declared/path/to/directive/templateOne": "actual/path/to/template/One", 148 | "declared/path/to/directive/templateTwo": "actual/path/to/template/Two" 149 | }). 150 | then(function (bldr) { 151 | return bldr.build("Cinnamon", cinnamon, 152 | {baseUrl: "/base", textPluginPath: "src/javascript_tests/lib/text"}); 153 | }); 154 | }; 155 | 156 | Note that it is entirely possible for the declared `templateUrl` to be the same as the path to access it; however, it may be different if you're using a test runner like Karma, which could serve static assets from a different path. This also allows a cheap form of URL rewriting if the path to your template does not match the path it actually is served from, like in Karma. 157 | 158 | 159 | 160 | ###build(moduleName, module, featureOptions) 161 | 162 | This method will construct and return the Container. It takes in 3 parameters: 163 | * Module name: The module name will be the module under test. 164 | * Module object: This is the actual module object that will be bootstrapped. 165 | * Feature options: This option is required when the application is not using RequireJS. Because Duck-Angular uses the text plugin to load resources like views, it needs to know the path to the text plugin. This is where you specify both the baseUrl, and the path to the text plugin, like so: 166 | 167 | Assuming you have text.js somewhere, simply specify that and the baseUrl. 168 | 169 | var container = builder.withDependencies(appLevelDependencies).build("MyModuleName", myModule, { baseUrl: "baseUrl/for/Duck/dependencies", textPluginPath: "path/to/text.js"}); 170 | 171 | This method returns a Container object, whose API is discussed below. 172 | 173 | The feature options object also supports setting the `multipleControllers` option, which controls whether you can inject dependencies only into the top-level controller, or into nested controllers as well. This is discussed in the notes under `domMvc()` and `mvc()`. 174 | 175 | The dependencies are injected using an overriding module which is constructed dynamically. This preserves the original module's dependencies. 176 | 177 | ##Container API 178 | 179 | 180 | ###mvc(controllerName, viewUrl, [dependencies], [options]) 181 | 182 | This method sets up a controller and a view, with dependencies that you can inject. Any dependencies not overridden are fulfilled using the application's default dependencies. It returns an object which contains the controller, the view, and the scope. 183 | 184 | var options = { 185 | preBindHook: function(scope) {...}, // optional 186 | preRenderHook: function(injector, scope) {...}, // optional 187 | dontWait: false, // optional 188 | async: false, // optional 189 | controllerLoadedPromise: function(controller) {...} // optional, required if async is true 190 | }; 191 | 192 | return container.mvc(controllerName, viewUrl, dependencies, options).then(function(mvc) { 193 | var controller = mvc.controller; 194 | var view = mvc.view; 195 | var scope = mvc.scope; 196 | ... 197 | }); 198 | 199 | // preBindHook and preRenderHook are optional. 200 | // dontWait is optional, and has a default value of false. Set it to true, if you do not want to wait for nested ng-include partial resolution. 201 | // async is optional, and has a default value of false. Set it to true, if your controller has to run asynchronous code to finish initialising. If asynchronous initialisation happens, Duck expects your controller to expose a promise whose fulfilment signals completion of controller setup. 202 | // controllerLoadedPromise is required if async is true. If not provided in this situation, it will assume the controller exposes promise called loaded. 203 | 204 | 205 | ###controller(controllerName, [dependencies], [isAsync], [controllerLoadedPromise]) 206 | 207 | This method sets up only a controller without a view, with dependencies that you can inject. Any dependencies not overridden are fulfilled using the application's default dependencies. It returns the constructed controller. 208 | 209 | return controller(controllerName, dependencies, isAsync, controllerLoadedPromise).then(function(controller) { 210 | ... 211 | }); 212 | 213 | // isAsync is optional, and has a default value of false. Set it to true, if your controller has to run asynchronous code to finish initialising. If asynchronous initialisation happens, Duck expects your controller to expose a promise whose fulfilment signals completion of controller setup. 214 | // controllerLoadedPromise is required if isAsync is true. If not provided in this situation, it will assume the controller exposes promise called `loaded`. 215 | 216 | 217 | ###domMvc(controllerName, viewURL, [controllerDependencies], [options]) 218 | This is a convenience wrapper over the mvc() method. It also constructs a new DuckDOM object (discussed in the DuckDOM Interaction API), and returns both the DuckDOM object, and the MVC object, in that order. 219 | 220 | If you're using this method, remember to use spread() on the promise, instead of then() to spread the return value over the argument list, like so: 221 | 222 | return container.domMvc(controllerName, viewUrl, dependencies, options).spread(function(dom, mvc) { 223 | ... 224 | }; 225 | 226 | 227 | ###Important Notes about mvc() and domMvc(): 228 | 229 | The latest version of Duck-Angular has initial support for nested controllers. To allow independent injection for each controller, you need to set the `multipleControllers` key in the `featureOptions` parameter in the `build()` method to *true*, like so: 230 | 231 | var container = builder.withDependencies(appLevelDependencies).build("MyModuleName", myModule, { baseUrl: "baseUrl/for/Duck/dependencies", textPluginPath: "path/to/text.js", multipleControllers: true}); 232 | 233 | ####When `multipleControllers` is `false` or unspecified 234 | The structure of the `dependencies` parameter only supports injecting dependencies for the top-level controller, like so: 235 | 236 | var controllerDependencies = { 237 | // Top-level controller dependencies 238 | }; 239 | 240 | ####When `multipleControllers` is `true` 241 | The structure of the `dependencies` parameter is different in this scenario. If you have 3 controllers (one root, and 2 nested), your dependencies object will have this structure: 242 | 243 | 244 | var controllerDependencies = {controller1: { 245 | //...Controller1 dependencies 246 | }, 247 | controller2: { 248 | //...Controller2 dependencies 249 | }, 250 | controller3: { 251 | //...Controller3 dependencies 252 | }, 253 | }; 254 | 255 | You can still specify an optional $scope field directly inside `controllerDependencies`; this will become the scope of the root controller. This will be removed in future versions. 256 | 257 | 258 | ###get(dependencyName) 259 | 260 | This method lets you retrieve any wired Angular dependency by name, like so: 261 | 262 | container.get("$http") 263 | 264 | 265 | ###addViewProcessor(function(viewHTML) {...}) 266 | This lets you add a function which gives you the opportunity to do some preprocessing on the top-level view HTML when it's initially loaded. 267 | 268 | var buildContainer = function (appLevelDependencies, controllerDependencies) { 269 | var builder = duckFactory.ContainerBuilder; 270 | return builder.withDependencies(appLevelDependencies). 271 | cacheTemplates(moduleUnderTest, { 272 | "declared/path/to/directive/template/One": "actual/path/to/template/One", 273 | "declared/path/to/directive/template/Two": "actual/path/to/template/Two" 274 | }) 275 | .then(function (bldr) { 276 | return bldr.build("Cinnamon", cinnamon, 277 | {baseUrl: "/base", textPluginPath: "src/javascript_tests/lib/text"}); 278 | }) 279 | .then(function (container) { 280 | container.addViewProcessor(function(viewHTML) { /* Processor code */ }); 281 | return container.domMvc("controllerName", "path/to/view", controllerDependencies); 282 | }); 283 | }; 284 | 285 | 286 | ###addViewProcessors([function(html) {...}, function(html) {...}, ...]) 287 | This is simply a convenience function for passing in an array of view processors. 288 | 289 | ##Interaction API 290 | 291 | The DuckDOM/DuckUIInteraction API lets you interact with elements in your constructed view. This only makes sense when you've set up your context using the Container.mvc() method. 292 | 293 | 294 | ###element(selector) 295 | 296 | This lets you access any element inside the view using standard jQuery selectors/semantics. 297 | 298 | var DuckDOM = duckFactory.DOM; 299 | 300 | return container.mvc(controllerName, viewUrl, dependencies, options).then(function(mvc) { 301 | var controller = mvc.controller; 302 | var view = mvc.view; 303 | var scope = mvc.scope; 304 | var dom = new DuckDOM(mvc.view, mvc.scope); 305 | expect(dom.element("#someElement").isHidden()).to.eq(true); 306 | }); 307 | 308 | 309 | ###apply() 310 | 311 | This lets you call Angular's $scope.$apply() method in a safe fashion. 312 | 313 | 314 | ###on(selector, event) 315 | 316 | This lets you create a promise for an event on an element specified by the selector. This allows you to use promise notation without having to resort to callback mechanics. 317 | 318 | 319 | ###interactWith(selector, [value], [promise]) 320 | 321 | This lets you interact with elements whose controller behaviour is known to be synchronous. Note that $scope.$apply() is automatically invoked after each interaction, so there is no need to call it yourself. 322 | 323 | var DuckDOM = duckFactory.DOM; 324 | 325 | return container.mvc(controllerName, viewUrl, dependencies, options).then(function(mvc) { 326 | var controller = mvc.controller; 327 | var view = mvc.view; 328 | var scope = mvc.scope; 329 | var dom = new DuckDOM(mvc.view, mvc.scope); 330 | 331 | dom.interactWith("#emailAddress", "mojo@mojo.com"); 332 | expect(dom.element("#emailAddress").val()).to.eq("mojo@mojo.com"); 333 | }); 334 | 335 | The interactWith() method is 'overloaded' to understand what type of element you are interacting with, so you can simply pass the second parameter where appropriate. For example: 336 | 337 | dom.interactWith("#someButton"); 338 | dom.interactWith("#someDropdown", 2); 339 | dom.interactWith("#textField", "Some Text"); 340 | dom.interactWith("#someRadio", true); 341 | 342 | The interactWith() method can also take a third parameter `promise`, which it returns untouched, such that subsequent code can be chained asynchronously if needed. For example: 343 | 344 | return dom.interactWith("#emailAddress", "mojo@mojo.com", dom.on("#someElement", "someEvent")) 345 | .then(function() { 346 | // More assertions 347 | }); 348 | 349 | 350 | ###with(selector, [value]).waitFor(object, objectFunctionStringName) 351 | 352 | This call lets you interact with elements whose controller behaviour is known to be asynchronous. In such cases, you want to wait for the asynchronous behaviour to complete before proceeding with test assertions. This method assumes that the asynchronous logic returns a promise whose fulfilment indicates the completion of the user action. 353 | 354 | var DuckDOM = duckFactory.DOM; 355 | var UIInteraction = Duck.UIInteraction; 356 | 357 | return container.mvc(controllerName, viewUrl, dependencies, options).then(function(mvc) { 358 | var controller = mvc.controller; 359 | var view = mvc.view; 360 | var scope = mvc.scope; 361 | var dom = new DuckDOM(mvc.view, mvc.scope); 362 | var interaction = new UIInteraction(dom); 363 | return interaction.with("#refreshLink").waitFor(mvc.scope, "refreshData").then(function() { 364 | expect(dom.element("#data")[0].innerText).to.eql("Some Data"); 365 | }); 366 | }); 367 | 368 | The above example assumes that there is a method refreshData() present on the scope which returns a promise to indicate completion of the asynchronous code. The rest of the assertions will only continue after this promise as been fulfilled. 369 | 370 | 371 | ###trigger(selector, event) 372 | 373 | This is merely a wrapper over jQuery's trigger() method, for firing events on elements. If you're using the interaction API, you merely need to write something like this: 374 | 375 | dom.trigger("#someId", "someEvent"); 376 | 377 | 378 | License 379 | ---------- 380 | 381 | The MIT License (MIT) 382 | 383 | Copyright (c) 2013 Avishek Sen Gupta 384 | 385 | Permission is hereby granted, free of charge, to any person obtaining a copy 386 | of this software and associated documentation files (the "Software"), to deal 387 | in the Software without restriction, including without limitation the rights 388 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 389 | copies of the Software, and to permit persons to whom the Software is 390 | furnished to do so, subject to the following conditions: 391 | 392 | The above copyright notice and this permission notice shall be included in 393 | all copies or substantial portions of the Software. 394 | 395 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 396 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 397 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 398 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 399 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 400 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 401 | THE SOFTWARE. 402 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "duck-angular", 3 | "version": "0.0.0", 4 | "main": "duck-angular.js", 5 | "ignore": [ 6 | "**/.*", 7 | "node_modules", 8 | "components", 9 | "bower_components", 10 | "test", 11 | "tests" 12 | ], 13 | "dependencies": { 14 | "underscore": "latest", 15 | "requirejs": "latest", 16 | "requirejs-text": "latest", 17 | "q": "v1", 18 | "jquery": "latest" 19 | } 20 | } -------------------------------------------------------------------------------- /duck-angular.js: -------------------------------------------------------------------------------- 1 | // Duck-Angular MASTER 2 | /* 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2013 Avishek Sen Gupta 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | */ 25 | 26 | var logDebug = function(text) { 27 | if (typeof(DUCK_DEBUG) != "undefined" && DUCK_DEBUG) 28 | console.log("[Duck DEBUG] " + text); 29 | }; 30 | 31 | // Adapted from https://github.com/asengupta/requirejs-q 32 | function requireQ(modules, Q) { 33 | var deferred = Q.defer(); 34 | require(modules, function () { 35 | deferred.resolve(arguments); 36 | }); 37 | return deferred.promise; 38 | } 39 | 40 | var duckCtor = function (_, angular, Q, $) { 41 | var hackDependencies = {}; 42 | 43 | var Container = function Container(injector, app, featureOptions) { 44 | featureOptions = featureOptions || {} 45 | logDebug("Feature Options is : " + JSON.stringify(featureOptions)); 46 | if (featureOptions.baseUrl && featureOptions.textPluginPath) { 47 | logDebug("Configuring RequireJS with : " + featureOptions.baseUrl + ", " + featureOptions.textPluginPath); 48 | require.config({ 49 | baseUrl: featureOptions.baseUrl, 50 | paths: { text: featureOptions.textPluginPath} 51 | }); 52 | } 53 | 54 | var self = this; 55 | self.options = {}; 56 | self.injector = injector; 57 | self.controllerProvider = self.injector.get("$controller"); 58 | self.rootScope = self.injector.get("$rootScope"); 59 | self.compileService = self.injector.get("$compile"); 60 | self.viewProcessors = []; 61 | 62 | this.addViewProcessor = function(viewProcessor) { 63 | self.viewProcessors.push(viewProcessor); 64 | }; 65 | 66 | this.addViewProcessors = function(viewProcessors) { 67 | _.each(viewProcessors, function(viewProcessor) { 68 | logDebug("Adding viewProcessor: " + viewProcessor); 69 | self.viewProcessors.push(viewProcessor); 70 | }); 71 | }; 72 | 73 | this.newScope = function () { 74 | return self.rootScope.$new(); 75 | }; 76 | 77 | this.createElement = function (viewHTML) { 78 | var wrappingElement = angular.element("
"); 79 | wrappingElement.append(viewHTML); 80 | return wrappingElement; 81 | }; 82 | 83 | this.removeElementsBelongingToDifferentScope = function (element) { 84 | element.find("[modal]").removeAttr("modal"); 85 | element.find("[options]").removeAttr("options"); 86 | if (!multipleControllersFeature(featureOptions)) 87 | element.find("[ng-controller]").remove(); 88 | 89 | return element; 90 | }; 91 | 92 | this.get = function (dependencyName) { 93 | return injector.get(dependencyName); 94 | }; 95 | 96 | this.numPartials = function num(element) { 97 | var includes = element.find("[ng-include]"); 98 | if (includes.length === 0) { 99 | return Q.fcall(function () { 100 | return 1; 101 | }); 102 | } 103 | 104 | var includedTemplateName = function(elementWithNgInclude) { 105 | var e = angular.element(elementWithNgInclude); 106 | if (e.attr("src")) 107 | return e.attr("src").replace("'", "").replace("'", ""); 108 | return e.attr("ng-include").replace("'", "").replace("'", ""); 109 | }; 110 | 111 | var promises = _.map(includes, function (include) { 112 | var includeSource = includedTemplateName(include); 113 | var includePromise = requireQ(["text!" + includeSource], Q); 114 | return includePromise.spread(function (sourceText) { 115 | var child = self.removeElementsBelongingToDifferentScope(self.createElement(sourceText)); 116 | return num(child); 117 | }); 118 | }); 119 | return Q.all(promises).then(function (counts) { 120 | return 1 + _.reduce(counts, function (sum, count) { 121 | return sum + count; 122 | }, 0); 123 | }); 124 | }; 125 | 126 | this.compileTemplate = function (viewHTML, scope, preRenderBlock) { 127 | var wrappingElement = self.removeElementsBelongingToDifferentScope(self.createElement(viewHTML)); 128 | if (preRenderBlock) { 129 | logDebug("Running pre-render block with scope: " + scope); 130 | preRenderBlock(self.injector, scope); 131 | } 132 | self.allPartialsLoadedDeferred = Q.defer(); 133 | var c = self.numPartials(wrappingElement); 134 | return c.then(function (numberOfPartials) { 135 | self.numberOfPartials = numberOfPartials - 1; 136 | if (self.options.dontWait || !self.numberOfPartials || self.numberOfPartials === 0) { 137 | logDebug("All partials have been loaded."); 138 | self.allPartialsLoadedDeferred.resolve(); 139 | } 140 | logDebug("Number of partials = " + self.numberOfPartials); 141 | var counter = 0; 142 | scope.$on("$includeContentLoaded", function () { 143 | logDebug("Loading a partial."); 144 | counter++; 145 | if (counter === self.numberOfPartials) { 146 | self.allPartialsLoadedDeferred.resolve(); 147 | } 148 | }); 149 | }).then(function () { 150 | logDebug("Binding template to scope"); 151 | var compiledTemplate = self.compileService(wrappingElement)(scope); 152 | applySafely(scope); 153 | return compiledTemplate; 154 | }); 155 | }; 156 | 157 | var applySafely = function (scope) { 158 | if (!scope.$$phase) { 159 | logDebug("Running $digest loop"); 160 | scope.$apply(); 161 | } 162 | }; 163 | 164 | var processView = function(viewHTML) { 165 | _.each(self.viewProcessors, function(viewProcessor) { 166 | logDebug("Processing view through " + viewProcessor); 167 | viewHTML = viewProcessor(viewHTML); 168 | }); 169 | return viewHTML; 170 | }; 171 | 172 | var stubScope = function(duckScope, mockScope){ 173 | if(mockScope && mockScope.$parent) { 174 | mockScope.$parent = stubScope(duckScope.$parent, mockScope.$parent); 175 | } 176 | mockScope = _.extend(duckScope, mockScope || {}); 177 | return mockScope; 178 | }; 179 | 180 | var stubScopes = function(duckScope, dependencies, controllerName){ 181 | if(multipleControllersFeature(featureOptions)){ 182 | dependencies[controllerName].$scope = stubScope(duckScope, dependencies[controllerName].$scope); 183 | return dependencies; 184 | } else { 185 | return stubScope(duckScope, dependencies.$scope); 186 | } 187 | }; 188 | 189 | this.view = function (viewUrl, scope, preRenderBlock) { 190 | var deferred = Q.defer(); 191 | logDebug("Loading " + viewUrl); 192 | require(["text!" + viewUrl], function (viewHTML) { 193 | logDebug("Loaded " + viewUrl); 194 | // HACK to make sure that ng-controller directives don't cause template to be eaten up 195 | if (!multipleControllersFeature(featureOptions)) { 196 | logDebug("Multiple controller support not enabled."); 197 | viewHTML = viewHTML.replace("ng-controller", "no-controller"); 198 | } else { 199 | logDebug("Multiple controller support is enabled."); 200 | } 201 | viewHTML = processView(viewHTML); 202 | viewHTML = viewHTML.replace("ng-app", "no-app"); 203 | self.compileTemplate(viewHTML, scope, preRenderBlock).then(function (compiledTemplate) { 204 | deferred.resolve(compiledTemplate); 205 | }); 206 | }, function (err) { 207 | console.log("Bad things happened"); 208 | console.log(err); 209 | }); 210 | return deferred.promise; 211 | }; 212 | 213 | this.controller = function (controllerName, dependencies, isAsync, controllerLoadedPromise) { 214 | logDebug("Building controller"); 215 | var controller; 216 | dependencies = dependencies || {}; 217 | if (multipleControllersFeature(featureOptions)) { 218 | logDebug("Multiple controller support enabled, dependencies are: " + dependencies); 219 | hackDependencies = dependencies; 220 | hackDependencies.rootControllerName = controllerName; 221 | controller = self.controllerProvider(controllerName, { $scope: dependencies.$scope }); 222 | } else { 223 | controller = self.controllerProvider(controllerName, dependencies); 224 | } 225 | if (!isAsync) { 226 | logDebug("Async convention disabled, resolving early."); 227 | return Q({}); 228 | } 229 | logDebug("Async support enabled, searching for controller loaded promise"); 230 | var deferred = Q.defer(); 231 | controllerLoadedPromise = 232 | controllerLoadedPromise ? controllerLoadedPromise(controller) : controller.loaded; 233 | logDebug("Does controller loaded promise exist ? " + controllerLoadedPromise !== null); 234 | controllerLoadedPromise.then(function () { 235 | logDebug("Controller is loaded, resolving"); 236 | deferred.resolve(controller); 237 | }); 238 | return deferred.promise; 239 | }; 240 | 241 | this.directiveTemplate = function (element) { 242 | var deferred = Q.defer(); 243 | var scope = self.newScope(); 244 | self.compileTemplate(element, scope).then(function (template) { 245 | deferred.resolve([scope, template]); 246 | }); 247 | return deferred.promise; 248 | }; 249 | 250 | this.domMvc = function (controllerName, viewUrl, dependencies, options) { 251 | dependencies = dependencies || {}; 252 | return self.mvc(controllerName, viewUrl, dependencies, options) 253 | .then(function (scopeViewController) { 254 | var dom = new DuckDOM(scopeViewController.view, scopeViewController.scope); 255 | return [dom, scopeViewController]; 256 | }); 257 | }; 258 | 259 | this.mvc = function (controllerName, viewUrl, dependencies, options) { 260 | self.options = options || {dontWait: false, async: false, controllerLoadedPromise: null}; 261 | self.options.preBindHook = self.options.preBindHook || function () {}; 262 | self.options.preRenderHook = self.options.preRenderHook || function () {}; 263 | dependencies = dependencies || {}; 264 | var scope = self.newScope(); 265 | logDebug("START Pre-bind hook"); 266 | self.options.preBindHook(scope); 267 | logDebug("STOP Pre-bind hook"); 268 | 269 | // Mojo, we wrote a function that more thoroughly stubs out $scope.$parent recursively 270 | if(multipleControllersFeature(featureOptions)){ 271 | dependencies = stubScopes(scope, dependencies, controllerName); 272 | } else { 273 | dependencies.$scope = stubScopes(scope, dependencies, controllerName); 274 | } 275 | 276 | var controller = this.controller(controllerName, dependencies, self.options.async || false, 277 | self.options.controllerLoadedPromise); 278 | var template = this.view(viewUrl, scope, self.options.preRenderHook); 279 | return Q.spread([controller, template], function (controller, template) { 280 | return self.allPartialsLoadedDeferred.promise.then(function () { 281 | return { controller: controller, view: template, scope: scope }; 282 | }); 283 | }); 284 | }; 285 | }; 286 | 287 | var multipleControllersFeature = function(featureOptions) { 288 | return featureOptions && featureOptions.multipleControllers; 289 | }; 290 | 291 | var ContainerBuilder = { 292 | dependencies: {}, 293 | originalDependenciesCache: {}, 294 | originalProvide: null, 295 | getQ: function (url) { 296 | var defer = Q.defer(); 297 | var req = new XMLHttpRequest(); 298 | req.open("GET", url, true); 299 | req.onload = function (e) { 300 | var result = req.responseText; 301 | defer.resolve(result); 302 | }; 303 | req.onerror = function (e) { 304 | console.error("Putting failed", e); 305 | defer.reject(e); 306 | }; 307 | req.send(); 308 | return defer.promise; 309 | }, 310 | 311 | cacheTemplate: function (app, templateUrl, realTemplateUrl) { 312 | var self = this; 313 | 314 | function putTemplateIntoCache(templateText){ 315 | app.run(function ($templateCache) { 316 | logDebug("Caching template with URL " + templateUrl); 317 | $templateCache.put(templateUrl, templateText); 318 | }); 319 | return self; 320 | } 321 | 322 | function isHtml(val){ 323 | return /<[a-z][\s\S]*>/i.test(val); 324 | } 325 | 326 | if(isHtml(realTemplateUrl)){ 327 | // Stub the template with an html string 328 | return Q.when(realTemplateUrl).then(putTemplateIntoCache); 329 | } else { 330 | // Otherwise get real file into the cache 331 | return self.getQ(realTemplateUrl).then(putTemplateIntoCache); 332 | } 333 | }, 334 | 335 | cacheTemplates: function(app, templateMap) { 336 | if (_.isEmpty(templateMap)) return Q(this); 337 | var self = this; 338 | return Q.all(_.map(_.pairs(templateMap), function(templateKeyPair) { 339 | return self.cacheTemplate(app, templateKeyPair[0], templateKeyPair[1]); 340 | })).spread(function(bldr) { 341 | return bldr; 342 | }); 343 | }, 344 | 345 | withDependencies: function (appLevelDependencies) { 346 | this.dependencies = appLevelDependencies; 347 | return this; 348 | }, 349 | 350 | build: function (moduleName, app, featureOptions) { 351 | var self = this; 352 | 353 | var mockModule = angular.module("lool", [moduleName, "ng"]); 354 | mockModule.config(function($provide) { 355 | $provide.provider("$rootElement", function () { 356 | this.$get = function () { 357 | return $("#Moaha"); 358 | }; 359 | }); 360 | 361 | if (multipleControllersFeature(featureOptions)) { 362 | $provide.decorator("$controller", function($delegate) { 363 | return function(ctrlName, deps) { 364 | if (ctrlName === hackDependencies.rootControllerName) { 365 | logDebug("Resolving root controller " + ctrlName); 366 | 367 | if (hackDependencies[ctrlName]) return $delegate(ctrlName, _.extend({}, deps, hackDependencies[ctrlName], {$scope: _.extend(deps.$scope, hackDependencies.$scope)})); 368 | return $delegate(ctrlName, {$scope: _.extend(deps.$scope, hackDependencies.$scope)}) 369 | } 370 | logDebug("Resolving controller " + ctrlName); 371 | if (hackDependencies[ctrlName]) return $delegate(ctrlName, _.extend({}, deps, hackDependencies[ctrlName], {$scope: _.extend(deps.$scope, hackDependencies[ctrlName].$scope)})); 372 | return $delegate(ctrlName, deps); 373 | }; 374 | }); 375 | } 376 | 377 | _.each(_.keys(self.dependencies), function (appDependencyKey) { 378 | if (typeof self.dependencies[appDependencyKey] === "function") { 379 | logDebug("Using function to resolve dependency: " + self.dependencies[appDependencyKey]); 380 | 381 | var v = self.dependencies[appDependencyKey]($provide, mockModule); 382 | } else { 383 | $provide.provider(appDependencyKey, function () { 384 | this.$get = function () { 385 | return self.dependencies[appDependencyKey]; 386 | }; 387 | }); 388 | } 389 | }); 390 | }); 391 | 392 | var injector = angular.bootstrap($("#null" + new Date().getMilliseconds()), ["lool"]); 393 | return new Container(injector, mockModule, featureOptions); 394 | } 395 | }; 396 | 397 | var DuckUIInteraction = function DuckUIInteraction(duckDom) { 398 | var self = this; 399 | this.with = function (selector, value) { 400 | self.interaction = function () { 401 | duckDom.interactWith(selector, value); 402 | }; 403 | return self; 404 | }; 405 | 406 | this.run = function () { 407 | self.interaction(); 408 | return self; 409 | }; 410 | 411 | this.waitFor = function (o, fn) { 412 | var deferred = Q.defer(); 413 | var originalFn = o[fn]; 414 | o[fn] = function () { 415 | var originalPromise = originalFn.apply(o, arguments); 416 | 417 | function resolveOriginalFunction() { 418 | duckDom.apply(); 419 | o[fn] = originalFn; 420 | deferred.resolve(); 421 | } 422 | 423 | if (originalPromise && originalPromise.then) { 424 | originalPromise.then(function (result) { 425 | resolveOriginalFunction(); 426 | return result; 427 | }, function (errors) { 428 | duckDom.apply(); 429 | o[fn] = originalFn; 430 | deferred.reject(errors); 431 | }); 432 | } else { 433 | resolveOriginalFunction(); 434 | } 435 | }; 436 | self.run(); 437 | return deferred.promise; 438 | }; 439 | 440 | this.waitForSync = function (o, fn) { 441 | var deferred = Q.defer(); 442 | var originalFn = o[fn]; 443 | o[fn] = function () { 444 | var result = originalFn.apply(o, arguments); 445 | duckDom.apply(); 446 | deferred.resolve(); 447 | return result; 448 | }; 449 | self.run(); 450 | return deferred.promise; 451 | }; 452 | }; 453 | 454 | var DuckDOM = function DuckDOM(view, scope) { 455 | var self = this; 456 | var applySafely = function () { 457 | if (!scope.$$phase) { 458 | try { 459 | scope.$apply(); 460 | } catch (e) { 461 | console.log("Apply failed"); 462 | console.log(e); 463 | } 464 | } 465 | }; 466 | 467 | this.emit = function(ev, args) { 468 | scope.$emit(ev, args); 469 | applySafely(); 470 | }; 471 | 472 | this.broadcast = function(ev, args) { 473 | scope.$broadcast(ev, args); 474 | applySafely(); 475 | }; 476 | 477 | this.applyAndDo = function (command) { 478 | var deferred = Q.defer(); 479 | scope.$apply(function () { 480 | command(); 481 | deferred.resolve(); 482 | }); 483 | return deferred.promise; 484 | }; 485 | 486 | this.trigger = function(selector, event) { 487 | var elements = angular.element(selector, view); 488 | elements.trigger(event); 489 | }; 490 | 491 | this.on = function(selector, ev) { 492 | var defer = Q.defer(); 493 | self.element(selector).on(ev, function() { 494 | defer.resolve(); 495 | }); 496 | return defer.promise; 497 | }; 498 | 499 | this.interactWith = function (selector, value, promise) { 500 | 501 | if(selector.scope && typeof selector.scope == "function"){ 502 | var elements = selector; 503 | } else { 504 | var elements = angular.element(selector, view); 505 | } 506 | 507 | _.each(elements, function (element) { 508 | if (element.nodeName === "TEXTAREA" || (element.nodeName === "INPUT" && 509 | (element.type === "text" || 510 | element.type === "password" || 511 | element.type === "number" || 512 | element.type === "tel" || 513 | element.type === "email" || 514 | element.type === "date" ))) { 515 | elements.focus(); 516 | elements.val(value).trigger("input"); 517 | } 518 | else if (element.nodeName === "FORM") { 519 | var inputElement = angular.element("input[type='submit']"); 520 | inputElement.submit(); 521 | } 522 | else if (element.nodeName === "INPUT" && element.type === "button") { 523 | elements.trigger("click"); 524 | } 525 | else if (element.nodeName === "INPUT" && element.type === "submit") { 526 | if (elements.submit) elements.submit(); 527 | elements.trigger("click"); 528 | } 529 | else if (element.nodeName === "INPUT" && element.type === "checkbox" && value == null) { 530 | elements.click().trigger("click"); 531 | elements.prop("checked", !elements.prop("checked")); 532 | } 533 | else if (element.nodeName === "INPUT" && element.type === "radio") { 534 | elements.attr("checked", elements.attr("checked") ? null : "checked").click(); 535 | } 536 | else if (element.nodeName === "INPUT" && element.type === "checkbox" && value != null) { 537 | while (elements.prop("checked") != value) { 538 | elements.click().trigger("click"); 539 | elements.prop("checked", !elements.prop("checked")); 540 | } 541 | } 542 | else if (element.nodeName === "SELECT") { 543 | elements.prop("selectedIndex", value); 544 | elements.trigger("change"); 545 | } 546 | else if (element.nodeName === "A" || element.nodeName === "BUTTON" || element.getAttribute("ng-click") != undefined) { 547 | elements.click(); 548 | } 549 | }); 550 | applySafely(); 551 | if (promise) { 552 | return promise; 553 | } 554 | return Q(self); 555 | }; 556 | 557 | this.apply = function () { 558 | applySafely(); 559 | }; 560 | 561 | var duckElement = { 562 | isVisible: function () { 563 | return !this.isHidden(); 564 | }, 565 | 566 | isHidden: function () { 567 | if(this.size() <=0){ 568 | throw(new Error("Element does not exist")); 569 | } 570 | return this.hasClass("ng-hide") || this.css("display") === "none" || this.parent().css("display") === "none"; 571 | }, 572 | isFocused: function () { 573 | var deferred = Q.defer(); 574 | this.on("focus", function () { 575 | deferred.resolve(); 576 | }); 577 | return deferred.promise; 578 | }, 579 | isDisabled: function() { 580 | return this.attr("disabled") === "disabled" || this.attr("disabled") === "true"; 581 | }, 582 | isEnabled: function() { 583 | return !this.isDisabled(); 584 | }, 585 | isRemoved: function() { 586 | return !$.contains(view[0], this[0]); 587 | }, 588 | find: function(){ 589 | var elements = this.$find.apply(this, arguments); 590 | return extendElementWithDuckMethods(elements); 591 | } 592 | }; 593 | 594 | this.element = function (selector) { 595 | var element = angular.element(selector, view); 596 | return extendElementWithDuckMethods(element); 597 | }; 598 | 599 | function extendElementWithDuckMethods(element){ 600 | element.$find = element.find; 601 | return _.extend(element, duckElement); 602 | } 603 | }; 604 | return { Container: Container, UIInteraction: DuckUIInteraction, DOM: DuckDOM, ContainerBuilder: ContainerBuilder }; 605 | }; 606 | 607 | if (typeof define !== "undefined") { 608 | console.log("RequireJS is present, defining AMD module"); 609 | define(["underscore", "angular", "Q", "jquery"], duckCtor); 610 | } 611 | else { 612 | console.log("RequireJS is NOT present, defining globally"); 613 | window.duckCtor = duckCtor; 614 | } 615 | -------------------------------------------------------------------------------- /duck-chai.js: -------------------------------------------------------------------------------- 1 | (function (duckChai) { 2 | "use strict"; 3 | 4 | // Module systems magic dance. 5 | 6 | if (typeof require === "function" && typeof exports === "object" && typeof module === "object") { 7 | // NodeJS 8 | module.exports = duckChai(); 9 | } else if (typeof define === "function" && define.amd) { 10 | // AMD 11 | define(["jquery"], function ($) { 12 | return function (chai, utils) { 13 | return duckChai(chai, utils, $); 14 | }; 15 | }); 16 | } else { 17 | // Other environment (usually