├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── bower.json ├── changelog.md ├── docs ├── backbone.radio.md ├── basics.md ├── classes.md ├── common.md ├── dom.api.md ├── dom.interactions.md ├── dom.prerendered.md ├── events.class.md ├── events.entity.md ├── events.md ├── features.md ├── installation.md ├── marionette.application.md ├── marionette.behavior.md ├── marionette.collectionview.md ├── marionette.mnobject.md ├── marionette.region.md ├── marionette.view.md ├── readme.md ├── routing.md ├── upgrade-v2-v3.md ├── upgrade-v3-v4.md ├── utils.md ├── view.lifecycle.md └── view.rendering.md ├── lib ├── backbone.marionette.esm.js ├── backbone.marionette.js ├── backbone.marionette.js.map ├── backbone.marionette.min.js └── backbone.marionette.min.js.map ├── license.txt ├── marionette-logo.png ├── package.json ├── readme.md ├── rollup.config.js ├── src ├── application.js ├── backbone.marionette.js ├── behavior.js ├── child-view-container.js ├── collection-view.js ├── common │ ├── bind-events.js │ ├── bind-requests.js │ ├── build-region.js │ ├── get-option.js │ ├── merge-options.js │ ├── monitor-view-events.js │ ├── normalize-methods.js │ ├── trigger-method.js │ └── view.js ├── config │ ├── dom.js │ ├── features.js │ └── renderer.js ├── mixins │ ├── behaviors.js │ ├── common.js │ ├── delegate-entity-events.js │ ├── destroy.js │ ├── events.js │ ├── radio.js │ ├── regions.js │ ├── template-render.js │ ├── triggers.js │ ├── ui.js │ └── view.js ├── object.js ├── region.js ├── utils │ ├── deprecate.js │ ├── error.js │ ├── extend.js │ ├── get-namespaced-event-name.js │ ├── invoke.js │ └── proxy.js └── view.js ├── test ├── .eslintrc ├── .globals.json ├── .mocharc.json ├── browsersync.html ├── rollup.config.js ├── runner.html ├── setup │ ├── browser.js │ ├── node.js │ └── setup.js └── unit │ ├── README.md │ ├── application.spec.js │ ├── backbone.marionette.spec.js │ ├── behavior.spec.js │ ├── child-view-container.spec.js │ ├── collection-view │ ├── collection-view-children.spec.js │ ├── collection-view-childviewcontainer.sepc.js │ ├── collection-view-data.spec.js │ ├── collection-view-empty.spec.js │ ├── collection-view-filtering.spec.js │ ├── collection-view-sorting.spec.js │ ├── collection-view-viewmixin.spec.js │ └── collection-view.spec.js │ ├── common │ ├── bind-events.spec.js │ ├── bind-request.spec.js │ ├── build-region.spec.js │ ├── get-option.spec.js │ ├── merge-options.spec.js │ ├── monitor-view-events.js │ ├── normalize-methods.spec.js │ ├── trigger-method.spec.js │ └── view.spec.js │ ├── config │ ├── dom.js │ ├── features.spec.js │ └── renderer.spec.js │ ├── destroying-views.spec.js │ ├── get-immediate-children.spec.js │ ├── mixins │ ├── behaviors.spec.js │ ├── common.spec.js │ ├── delegate-entity-events.spec.js │ ├── destroy.spec.js │ ├── radio.spec.js │ ├── template-render.spec.js │ ├── ui.spec.js │ └── view.spec.js │ ├── object.spec.js │ ├── on-attach.spec.js │ ├── on-dom-refresh.spec.js │ ├── on-dom-remove.spec.js │ ├── region.spec.js │ ├── utils │ ├── deprecate.spec.js │ ├── error.spec.js │ ├── get-namespaced-event-name.spec.js │ └── proxy.spec.js │ ├── view.child-views.spec.js │ ├── view.dynamic-regions.spec.js │ ├── view.renderer.js │ ├── view.spec.js │ ├── view.triggers.spec.js │ ├── view.ui-bindings.spec.js │ └── view.ui-event-and-triggers.spec.js ├── trigger-deploy-mn-com.js ├── upgradeGuide.md └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env"], 3 | "env": { 4 | "test": { 5 | "plugins": ["istanbul"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true; 4 | 5 | [*] 6 | # Ensure there's no lingering whitespace 7 | trim_trailing_whitespace = true 8 | # Ensure a newline at the end of each file 9 | insert_final_newline = true 10 | 11 | [*.js] 12 | charset = utf-8 13 | indent_style = space 14 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module" 6 | }, 7 | "env": { 8 | "browser": true, 9 | "node": true 10 | }, 11 | "rules": { 12 | "array-bracket-spacing": [ 2, "never" ], 13 | "block-scoped-var": 2, 14 | "brace-style": [ 2, "1tbs", { "allowSingleLine": true } ], 15 | "camelcase": [ 2, { "properties": "always" } ], 16 | "curly": [ 2, "all" ], 17 | "dot-notation": [ 2, { "allowKeywords": true } ], 18 | "eol-last": 2, 19 | "eqeqeq": [ 2, "allow-null" ], 20 | "guard-for-in": 2, 21 | "indent": [ 2, 2, { "SwitchCase": 1 } ], 22 | "key-spacing": [ 2, 23 | { 24 | "beforeColon": false, 25 | "afterColon": true 26 | } 27 | ], 28 | "keyword-spacing": [ 2 ], 29 | "new-cap": 2, 30 | "no-bitwise": 2, 31 | "no-caller": 2, 32 | "no-eval": 2, 33 | "no-extend-native": 2, 34 | "no-iterator": 2, 35 | "no-loop-func": 2, 36 | "no-multi-spaces": "error", 37 | "no-multi-str": 2, 38 | "no-multiple-empty-lines": 2, 39 | "no-new": 2, 40 | "no-proto": 2, 41 | "no-script-url": 2, 42 | "no-sequences": 2, 43 | "no-shadow": 2, 44 | "no-spaced-func": 2, 45 | "no-trailing-spaces": 2, 46 | "no-unused-vars": [ 1, { "args": "none" } ], 47 | "no-var": 2, 48 | "no-with": 2, 49 | "object-shorthand": [ 2, "methods" ], 50 | "operator-linebreak": [ 2, "after" ], 51 | "quotes": [ 2, "single" ], 52 | "semi": [ 0, "never" ], 53 | "space-before-blocks": [ 2, "always" ], 54 | "space-before-function-paren": [ 2, "never" ], 55 | "space-in-parens": [ 2, "never" ], 56 | "space-infix-ops": 2, 57 | "space-unary-ops": [ 2, 58 | { 59 | "nonwords": false, 60 | "overrides": {} 61 | } 62 | ], 63 | "strict": 0, 64 | "valid-jsdoc": 2, 65 | "wrap-iife": [ 2, "inside" ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | *.swo 4 | *.orig 5 | .idea 6 | ext/ 7 | 8 | # Logs 9 | logs 10 | *.log 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | 17 | # Dependency directory 18 | node_modules 19 | 20 | # Testing 21 | coverage 22 | test/tmp 23 | .nyc_output 24 | 25 | # Users Environment Variables 26 | .lock-wscript 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | coverage 3 | docs 4 | src 5 | test 6 | tmp 7 | .babelrc 8 | .editorconfig 9 | .eslintrc 10 | .gitignore 11 | .nyc_output 12 | .travis.yml 13 | bower.json 14 | CONTRIBUTING.md 15 | ISSUE_TEMPLATE.md 16 | marionette-logo.png 17 | PULL_REQUEST_TEMPLATE.md 18 | trigger-deploy-mn-com.js 19 | upgradeGuide.md 20 | yarn.lock 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | cache: yarn 5 | env: 6 | - TEST_SUITE=coverage 7 | - TEST_SUITE=browser 8 | - TEST_SUITE=lodash USE_LODASH=1 9 | after_install: 10 | - yarn install travis-ci 11 | script: 12 | - if [[ $TEST_SUITE = "coverage" ]]; then yarn run coveralls; fi 13 | - if [[ $TEST_SUITE = "browser" ]] && [[ $SAUCE_USERNAME ]]; then yarn run test-cross-browser; fi 14 | - if [[ $TEST_SUITE = "lodash" ]]; then yarn run test-lodash; fi 15 | after_success: 16 | - if [[ $TRAVIS_BRANCH = "master" ]] && [[ $TRAVIS_PULL_REQUEST = "false" ]]; then node trigger-deploy-mn-com.js; fi 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Marionette has a few guidelines to facilitate your contribution and streamline 2 | the process of getting changes merged in and released. 3 | 4 | 1. [Setting up Marionette locally](#setting-up-marionette-locally) 5 | 2. [Reporting a bug](#reporting-a-bug) 6 | 3. [Submitting patches and fixes](#submitting-patches-and-fixes) 7 | 4. [Running Tests](#running-tests) 8 | 9 | 10 | ## Setting up Marionette locally 11 | 12 | * Fork the Marionette repo. 13 | * `git clone` your fork onto your computer. 14 | * Run `yarn install` to make sure you have all Marionette dependencies locally. 15 | * Run `yarn build` to build source files. 16 | 17 | ## Reporting a bug 18 | 19 | In order to best help out with bugs, we need to know the following information 20 | in your bug submission: 21 | 22 | * Marionette version #. 23 | * Backbone version #. 24 | 25 | Including this information in a submission will help us test the problem and 26 | ensure that the bug is both reproduced and corrected on the platforms / 27 | versions that you are having issues with. 28 | 29 | **Provide A Meaningful Description** 30 | 31 | It is very important to provide a meaningful description with your bug reports 32 | and pull requests. A good format for these descriptions will include the 33 | following things: 34 | 35 | 1. The problem you are facing (in as much detail as is necessary to describe 36 | the problem to someone who doesn't know anything about the system you're 37 | building) 38 | 39 | 2. A summary of the proposed solution 40 | 41 | 3. A description of how this solution solves the problem, in more detail than 42 | item #2 43 | 44 | 4. Any additional discussion on possible problems this might introduce, 45 | questions that you have related to the changes, etc. 46 | 47 | For a PR, we need at least the first 2 items to understand why you are changing 48 | the code. If not, we will ask that you add the necessary information. 49 | 50 | Please refrain from giving code examples in altJS languages like CoffeeScript, 51 | etc. Marionette is written in plain-old JavaScript and is generally easier for all 52 | members in the community to read. 53 | 54 | ### When you don't have a bug fix 55 | 56 | If you are stuck in a scenario that fails in your app, but you don't know how to 57 | fix it, submit a failing spec to show the failing scenario. Follow the 58 | guidelines for a pull request submission, but don't worry about fixing the 59 | problem. A failing spec to show that a problem exists is a very very very 60 | helpful pull request for us. 61 | 62 | We'll even accept a failing test pasted into the ticket description instead of a 63 | PR. That would at least get us started on creating the failing test in the code. 64 | 65 | ## Submitting patches and fixes 66 | 67 | See [Github's documentation for pull 68 | requests](https://help.github.com/articles/using-pull-requests). 69 | 70 | Pull requests are by far the best way to contribute to Marionette. They are by 71 | far the easiest way to demonstrate issues and your proposed resolution. To 72 | really help us evaluate your pull request and bring it into Marionette, please 73 | provide as much information as possible and follow the guidelines below: 74 | 75 | 1. Determine the branch as your base: `next` or `master` 76 | 2. Provide a brief summary of what your pull request is doing 77 | 3. Reference any relevant Github issue numbers 78 | 4. Include any extra detail you feel will help provide context 79 | 80 | ### Determining your branch 81 | 82 | When submitting your pull request, you need to determine whether to base off 83 | `next` or `master`: 84 | 85 | * If you're submitting a bug fix, base off `next` 86 | * If you're submitting a new feature, base off `next` 87 | * If you're submitting documentation for a new feature, base off `next` 88 | * If you're submitting documentation for the current release, base off `master` 89 | 90 | ### Submitting a Great Patch 91 | 92 | We want Marionette to provide a great experience to developers and help you 93 | write great applications using it. To help us achieve this goal, please follow 94 | these guidelines when submitting your patches. 95 | 96 | #### Solving Issues 97 | 98 | When you're submitting a bug fix, include spec tests, where applicable, showing 99 | the issue and the resolution. We strive to maintain 100% code coverage in our 100 | testing. 101 | 102 | #### Coding Guidelines 103 | 104 | The Marionette coding conventions are provided in the ESLint configuration 105 | included in the repository. Most IDEs and text editors will provide, or allow 106 | for, a plugin for ESLint to read the `.eslintrc` file. 107 | For areas where the configuration provides no guidance, try to stick to the 108 | conventions in the file you're editing. 109 | 110 | #### How we Approve Pull Requests 111 | 112 | We utilise Github's review approach. When receiving your pull request, we will 113 | comment inline and provide guidance to help you get your pull request merged 114 | into Marionette. This is not a one-way process and we're more than happy to 115 | discuss the context of your decisions. 116 | 117 | Once two Marionette.js members approve the pull request, we will then merge it 118 | into the base branch. 119 | 120 | Please remember that Marionette is a community-maintained project and, as such, 121 | many of us are working on this in our spare time. If we haven't commented on 122 | your pull request, please be patient. We may be available on our Gitter channel 123 | to discuss further. 124 | 125 | ## Running Tests 126 | 127 | * via command-line by running `yarn test` 128 | * in the browser by running `yarn test-browser` 129 | 130 | To see the test matrix - run `yarn coverage` 131 | 132 | ## Writing Tests and Code Style 133 | 134 | [More information]('test/unit/README.md') 135 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 1. The problem you are facing (in as much detail as is necessary to describe the problem to someone who doesn't know anything about the system you're building) 4 | 2. A summary of the proposed solution 5 | 3. A description of how this solution solves the problem, in more detail than item #2 6 | 4. Any additional discussion on possible problems this might introduce, questions that you have related to the changes, etc. 7 | 8 | ### Expected behavior 9 | 10 | Tell us what you think should happen. 11 | 12 | ### Actual behavior 13 | 14 | If possible, please create a small demo that demonstrates the issue. 15 | You can fork https://jsfiddle.net/marionettejs/adhv48ky/ for quick demo setup. 16 | Please refrain from giving code examples in altJS languages like CoffeeScript, etc. Marionette is written in plain-old JavaScript and is generally easier for all members in the community to read. 17 | 18 | ### Environment 19 | 20 | 1. Marionette version: 21 | 2. Backbone version: 22 | 3. Additional build tools, etc: 23 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Proposed changes 2 | - 3 | - 4 | - 5 | 6 | Link to the issue: 7 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone.marionette", 3 | "description": "The Backbone Framework", 4 | "homepage": "https://marionettejs.com/", 5 | "version": "4.1.3", 6 | "main": "./lib/backbone.marionette.js", 7 | "license": "MIT", 8 | "keywords": [ 9 | "backbone", 10 | "framework", 11 | "client", 12 | "browser", 13 | "composite" 14 | ], 15 | "author": { 16 | "name": "Derick Bailey", 17 | "email": "derickbailey@gmail.com" 18 | }, 19 | "ignore": [ 20 | "docs", 21 | "src", 22 | "test", 23 | ".babelrc", 24 | ".editorconfig", 25 | ".eslintrc", 26 | ".gitignore", 27 | ".jscsrc", 28 | ".npmignore", 29 | ".travis.yml", 30 | "CONTRIBUTING.md", 31 | "upgradeGuide.md" 32 | ], 33 | "dependencies": { 34 | "backbone.radio": "^2.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docs/classes.md: -------------------------------------------------------------------------------- 1 | # Marionette Classes 2 | 3 | Marionette follows Backbone's [pseudo-class architecture](./basics.md#class-based-inheritance). 4 | This documentation is meant to provide a comprehensive listing of those classes so that 5 | the reader can have a high-level view and understand functional similarities between the classes. 6 | All of these classes share a [common set of functionality](./common.md). 7 | 8 | ### [Marionette.View](./marionette.view.md) 9 | 10 | A `View` is used for managing portions of the DOM via a single parent DOM element or `el`. 11 | It provides a consistent interface for managing the content of the `el` which is typically 12 | administered by serializing a `Backbone.Model` or `Backbone.Collection` and rendering 13 | a template with the serialized data into the `View`s `el`. 14 | 15 | The `View` provides event delegation for capturing and handling DOM interactions as well as 16 | the ability to separate concerns into smaller, managed child views. 17 | 18 | `View` includes: 19 | - [The DOM API](./dom.api.md) 20 | - [Class Events](./events.class.md#view-events) 21 | - [DOM Interactions](./dom.interactions.md) 22 | - [Child Event Bubbling](./events.md#event-bubbling) 23 | - [Entity Events](./events.entity.md) 24 | - [View Rendering](./view.rendering.md) 25 | - [Prerendered Content](./dom.prerendered.md) 26 | - [View Lifecycle](./view.lifecycle.md) 27 | 28 | A `View` can have [`Region`s](#marionetteregion) and [`Behavior`s](#marionettebehavior) 29 | 30 | ### [Marionette.CollectionView](./marionette.collectionview.md) 31 | 32 | A `CollectionView` like `View` manages a portion of the DOM via a single parent DOM element 33 | or `el`. This view manages an ordered set of child views that are shown within the view's `el`. 34 | These children are most often created to match the models of a `Backbone.Collection` though a 35 | `CollectionView` does not require a `collection` and can manage any set of views. 36 | 37 | `CollectionView` includes: 38 | - [The DOM API](./dom.api.md) 39 | - [Class Events](./events.class.md#collectionview-events) 40 | - [DOM Interactions](./dom.interactions.md) 41 | - [Child Event Bubbling](./events.md#event-bubbling) 42 | - [Entity Events](./events.entity.md) 43 | - [View Rendering](./view.rendering.md) 44 | - [Prerendered Content](./dom.prerendered.md) 45 | - [View Lifecycle](./view.lifecycle.md) 46 | 47 | A `CollectionView` can have [`Behavior`s](#marionettebehavior). 48 | 49 | ### [Marionette.Region](./marionette.region.md) 50 | 51 | Regions provide consistent methods to manage, show and destroy views in your 52 | applications and views. 53 | 54 | `Region` includes: 55 | - [Class Events](./events.class.md#region-events) 56 | - [The DOM API](./dom.api.md) 57 | 58 | ### [Marionette.Behavior](marionette.behavior.md) 59 | 60 | A `Behavior` provides a clean separation of concerns to your view logic, allowing you to 61 | share common user-facing operations between your views. 62 | 63 | `Behavior` includes: 64 | - [Class Events](./events.class.md#behavior-events) 65 | - [DOM Interactions](./dom.interactions.md) 66 | - [Entity Events](./events.entity.md) 67 | 68 | ### [Marionette.Application](marionette.application.md) 69 | 70 | An `Application` provides hooks for organizing and initiating other elements and a view tree. 71 | 72 | `Application` includes: 73 | - [Class Events](./events.class.md#application-events) 74 | - [Radio API](./backbone.radio.md#marionette-integration) 75 | - [MnObject's API](./marionette.mnobject.md) 76 | 77 | An `Application` can have a single [region](./marionette.application.md#application-region). 78 | 79 | ### [Marionette.MnObject](marionette.mnobject.md) 80 | 81 | `MnObject` incorporates backbone conventions `initialize`, `cid` and `extend`. 82 | 83 | `MnObject` includes: 84 | - [Class Events](./events.class.md#mnobject-events) 85 | - [Radio API](./backbone.radio.md#marionette-integration). 86 | 87 | ## Routing in Marionette 88 | 89 | Users of versions of Marionette prior to v4 will notice that a router is no longer bundled. 90 | The [Marionette.AppRouter](https://github.com/marionettejs/marionette.approuter) was extracted 91 | and the core library will no longer hold an opinion on routing. 92 | 93 | [Continue Reading](./routing.md) about routing in Marionette. 94 | -------------------------------------------------------------------------------- /docs/dom.api.md: -------------------------------------------------------------------------------- 1 | # The DOM API 2 | 3 | With the release of Marionette 3.2, developers can remove the dependency on 4 | jQuery and integrate with the DOM using a custom api. 5 | 6 | ## API Methods 7 | 8 | The DOM API manages the DOM on behalf of [each view class and `Region`](./classes.md). 9 | It defines the methods that actually attach and remove views and children. 10 | 11 | [The default API](#the-default-api) depends on Backbone's jQuery `$` object however it does not 12 | rely on jQuery-specific behavior. This should make it easier to develop your own 13 | API. You will, however, [need to also handle Backbone's jQuery integration](#backbone-jquery-integration). 14 | 15 | ### `createBuffer()` 16 | 17 | Returns a new HTML DOM node instance. The resulting node can be passed into the 18 | other DOM functions. 19 | 20 | ### `getDocumentEl(el)` 21 | 22 | Look up the top level element of `el`. Used by Marionette to determine attachment. 23 | 24 | ```javascript 25 | const elIsAttached = this.Dom.hasEl(this.Dom.getDocumentEl(this.el), this.el); 26 | ``` 27 | 28 | ### `getEl(selector)` 29 | 30 | Lookup the `selector` string withing the DOM. The `selector` may also be a DOM element. 31 | It should return an array-like object of the node. 32 | 33 | ### `findEl(el, selector)` 34 | 35 | Lookup the `selector` string within the DOM node `el`. It should return an array-like object of nodes. 36 | 37 | ### `hasEl(el, childEl)` 38 | 39 | Returns true if the el contains the node childEl 40 | 41 | ### `detachEl(el)` 42 | 43 | Detach `el` from the DOM without removing listeners. 44 | 45 | ### `replaceEl(newEl, oldEl)` 46 | 47 | Remove `oldEl` from the DOM and put `newEl` in its place. 48 | 49 | ### `swapEl(el1, el2)` 50 | 51 | Swaps the location of `el1` and `el2` in the DOM. 52 | Both els must have a parentNode to be able to swap. 53 | 54 | ### `setContents(el, html)` 55 | 56 | Replace the contents of `el` with the HTML string of `html`. Unlike other DOM 57 | functions, this only takes a literal string for its second argument. 58 | 59 | ### `appendContents(el, contents)` 60 | 61 | Takes the DOM node `el` and appends the DOM node `contents` to the end of the 62 | element's contents. 63 | 64 | ### `hasContents(el)` 65 | 66 | Returns a boolean indicating if the `el` has child nodes. 67 | 68 | ### `detachContents(el)` 69 | 70 | Remove the inner contents of `el` from the DOM while leaving `el` itself in the 71 | DOM. 72 | 73 | ## The default API 74 | 75 | The API used by Marionette by default is attached as `Marionette.DomApi`. 76 | This is useful if you [change the API](#providing-your-own-dom-api) globally, 77 | but want to reuse the default in certain cases. 78 | 79 | ```javascript 80 | import { setDomApi, DomApi } from 'backbone.marionette'; 81 | 82 | import MyDOMApi from './mydom'; 83 | 84 | setDomApi(MyDOMApi); 85 | 86 | // Use MyDOMApi everywhere but `Marionette.View` 87 | View.setDomApi(DomApi); 88 | ``` 89 | 90 | ## Providing Your Own DOM API 91 | 92 | To implement your own DOM API use `setDomApi`: 93 | 94 | ```javascript 95 | import { setDomApi } from 'backbone.marionette'; 96 | import MyDOMApi from './mydom'; 97 | 98 | setDomApi(MyDOMApi); 99 | ``` 100 | 101 | You can also implement a different DOM API for a particular class: 102 | 103 | ```javascript 104 | import { View } from 'backbone.marionette'; 105 | 106 | View.setDomApi(MyDOMApi); 107 | ``` 108 | 109 | `CollectionView`, `Region`, and `View` 110 | all have `setDomApi`. Each extended class may have their own DOM API. 111 | 112 | Additionally a DOM API can be partially set: 113 | 114 | ```javascript 115 | import { View } from 'backbone.marionette'; 116 | 117 | const MyView = View.extend(); 118 | 119 | MyView.setDomApi({ 120 | setContents(el, html) { 121 | el.innerHTML = html; 122 | } 123 | }); 124 | ``` 125 | 126 | ### Backbone jQuery Integration 127 | 128 | Backbone.js is tied to jQuery's API for managing DOM manipulation. If you want 129 | to completely remove jQuery from your Marionette app, you'll also have to 130 | provide your own versions of the following methods: 131 | 132 | * [`_setAttributes`](http://backbonejs.org/docs/backbone.html#section-170) 133 | * [`delegate`](http://backbonejs.org/docs/backbone.html#section-165) 134 | * [`undelegate`](http://backbonejs.org/docs/backbone.html#section-167) 135 | 136 | #### See Also 137 | 138 | The DOM API takes care of the other DOM manipulation methods for you. The 139 | [Backbone Wiki](https://github.com/jashkenas/backbone/wiki/using-backbone-without-jquery) 140 | has a good reference for removing jQuery from the app, including Browserify and 141 | Webpack configuration hooks. 142 | -------------------------------------------------------------------------------- /docs/dom.prerendered.md: -------------------------------------------------------------------------------- 1 | # Prerendered Content 2 | 3 | [View classes](./classes.md) can be initialized with pre-rendered DOM. 4 | 5 | This can be HTML that's currently in the DOM: 6 | 7 | ```javascript 8 | import { View } from 'backbone.marionette'; 9 | 10 | const myView = new View({ el: $('#foo-selector') }); 11 | 12 | myView.isRendered(); // true if '#foo-selector` exists and has content 13 | myView.isAttached(); // true if '#foo-selector` is in the DOM 14 | ``` 15 | 16 | Or it can be DOM created in memory: 17 | 18 | ```javascript 19 | import { View } from 'backbone.marionette'; 20 | 21 | const $inMemoryHtml = $('
Hello World!
'); 22 | 23 | const myView = new View({ el: $inMemoryHtml }); 24 | ``` 25 | 26 | [Live example](https://jsfiddle.net/marionettejs/b2yz38gj/) 27 | 28 | In both of the cases at instantiation the view will determine 29 | [its state](./view.lifecycle.md) as to whether the el is rendered 30 | or attached. 31 | 32 | **Note** `render` and `attach` events will not fire for the initial 33 | state as the state is set already at instantiation and is not changing. 34 | 35 | ## Managing `View` children 36 | 37 | With [`View`](./marionette.view.md) in most cases the [`render` event](./events.class.md#render-and-beforerender-events) 38 | is the best place to show child views [for best performance](./marionette.view.md#efficient-nested-view-structures). 39 | 40 | However with pre-rendered DOM you may need to show child views in `initialize` 41 | as the view will already be rendered. 42 | 43 | ```javascript 44 | import { View } from 'backbone.marionette'; 45 | import HeaderView from './header-view'; 46 | 47 | const MyBaseLayout = View.extend({ 48 | regions: { 49 | header: '#header-region', 50 | content: '#content-region' 51 | }, 52 | el: $('#base-layout'), 53 | initialize() { 54 | this.showChildView('header', new HeaderView()); 55 | } 56 | }); 57 | ``` 58 | 59 | ### Managing a Pre-existing View Tree. 60 | 61 | It may be the case that you need child views of already existing DOM as well. 62 | To set this up you'll need to query for `el`s down the tree: 63 | 64 | ```javascript 65 | import { View } from 'backbone.marionette'; 66 | import HeaderView from './header-view'; 67 | 68 | const MyBaseLayout = View.extend({ 69 | regions: { 70 | header: '#header-region', 71 | content: '#content-region' 72 | }, 73 | el: $('#base-layout'), 74 | initialize() { 75 | this.showChildView('header', new HeaderView({ 76 | el: this.getRegion('header').$el.contents() 77 | })); 78 | } 79 | }); 80 | ``` 81 | 82 | The same can be done with [`CollectionView`](./marionette.collectionview.md): 83 | 84 | ```javascript 85 | import { CollectionView } from 'backbone.marionette'; 86 | import ItemView from './item-view'; 87 | 88 | const MyList = CollectionView.extend({ 89 | el: $('#base-table'), 90 | childView: ItemView, 91 | childViewContainer: 'tbody', 92 | buildChildView(model, ChildView) { 93 | const index = this.collection.indexOf(model); 94 | const childEl = this.$('tbody').contents()[index]; 95 | 96 | return new ChildView({ 97 | model, 98 | el: childEl 99 | }); 100 | } 101 | }); 102 | 103 | const myList = new MyList({ collection: someCollection }); 104 | 105 | // Unlike `View`, `CollectionView` should be rendered to build the `children` 106 | myList.render(); 107 | ``` 108 | 109 | https://github.com/marionettejs/backbone.marionette/issues/3128 110 | 111 | ## Re-rendering children of a view with preexisting DOM. 112 | 113 | You may be instantiating a `View` with existing HTML, but if you re-render the view, 114 | like any other view, your view will render the `template` into the view's `el` and 115 | any children will need to be re-shown. 116 | 117 | So your view will need to be prepared to handle both scenarios. 118 | 119 | ```javascript 120 | import _ from 'underscore'; 121 | import { View } from 'backbone.marionette'; 122 | import HeaderView from './header-view'; 123 | 124 | const MyBaseLayout = View.extend({ 125 | regions: { 126 | header: '#header-region', 127 | content: '#content-region' 128 | }, 129 | el: $('#base-layout'), 130 | initialize() { 131 | this.showChildView('header', new HeaderView({ 132 | el: this.getRegion('header').$el.contents() 133 | })); 134 | }, 135 | template: _.template('
'), 136 | onRender() { 137 | this.showChildView('header', new HeaderView()); 138 | } 139 | }); 140 | ``` 141 | -------------------------------------------------------------------------------- /docs/events.entity.md: -------------------------------------------------------------------------------- 1 | # Entity events 2 | 3 | The [`View`, `CollectionView` and `Behavior`](./classes.md) can bind to events that occur on attached models and 4 | collections - this includes both [standard backbone-events](http://backbonejs.org/#Events-catalog) and custom events. 5 | 6 | Event handlers are called with the same arguments as if listening to the entity directly 7 | and called with the context of the view instance. 8 | 9 | ### Model Events 10 | 11 | For example, to listen to a model's events: 12 | 13 | ```javascript 14 | import { View } from 'backbone.marionette'; 15 | 16 | const MyView = View.extend({ 17 | modelEvents: { 18 | 'change:attribute': 'onChangeAttribute' 19 | }, 20 | 21 | onChangeAttribute(model, value) { 22 | console.log('New value: ' + value); 23 | } 24 | }); 25 | ``` 26 | 27 | [Live example](https://jsfiddle.net/marionettejs/auvk4hps/) 28 | 29 | The `modelEvents` attribute passes through all the arguments that are passed 30 | to `model.trigger('event', arguments)`. 31 | 32 | The `modelEvents` attribute can also take a 33 | [function returning an object](basics.md#functions-returning-values). 34 | 35 | #### Function Callback 36 | 37 | You can also bind a function callback directly in the `modelEvents` attribute: 38 | 39 | ```javascript 40 | import { View } from 'backbone.marionette'; 41 | 42 | const MyView = View.extend({ 43 | modelEvents: { 44 | 'change:attribute'() { 45 | console.log('attribute was changed'); 46 | } 47 | } 48 | }); 49 | ``` 50 | 51 | [Live example](https://jsfiddle.net/marionettejs/zaxLe6au/) 52 | 53 | ### Collection Events 54 | 55 | Collection events work exactly the same way as [`modelEvents`](#model-events) 56 | with their own `collectionEvents` key: 57 | 58 | ```javascript 59 | import { View } from 'backbone.marionette'; 60 | 61 | const MyView = View.extend({ 62 | collectionEvents: { 63 | sync: 'onSync' 64 | }, 65 | 66 | onSync(collection) { 67 | console.log('Collection was synchronised with the server'); 68 | } 69 | }); 70 | ``` 71 | 72 | [Live example](https://jsfiddle.net/marionettejs/7qyfeh9r/) 73 | 74 | The `collectionEvents` attribute can also take a 75 | [function returning an object](basics.md#functions-returning-values). 76 | 77 | Just as in `modelEvents`, you can bind function callbacks directly inside the 78 | `collectionEvents` object: 79 | 80 | ```javascript 81 | import { View } from 'backbone.marionette'; 82 | 83 | const MyView = View.extend({ 84 | collectionEvents: { 85 | 'update'() { 86 | console.log('the collection was updated'); 87 | } 88 | } 89 | }); 90 | ``` 91 | 92 | [Live example](https://jsfiddle.net/marionettejs/ze8po0x5/) 93 | 94 | ### Listening to Both 95 | 96 | If your view has a `model` and `collection` attached, it will listen for events 97 | on both: 98 | 99 | ```javascript 100 | import { View } from 'backbone.marionette'; 101 | 102 | const MyView = View.extend({ 103 | 104 | modelEvents: { 105 | 'change:someattribute': 'onChangeSomeattribute' 106 | }, 107 | 108 | collectionEvents: { 109 | 'update': 'onCollectionUpdate' 110 | }, 111 | 112 | onChangeSomeattribute() { 113 | console.log('someattribute was changed'); 114 | }, 115 | 116 | onCollectionUpdate() { 117 | console.log('models were added or removed in the collection'); 118 | } 119 | }); 120 | ``` 121 | 122 | [Live example](https://jsfiddle.net/marionettejs/h9ub5hp3/) 123 | 124 | In this case, Marionette will bind event handlers to both. 125 | -------------------------------------------------------------------------------- /docs/features.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | Marionette Features are opt-in functionality that you can enable by utilizing [`setEnabled`](#setting-a-feature-flag) in your app. 4 | It is a good practice to set these flags only once prior to instantiating any Marionette class. 5 | 6 | ## Documentation Index 7 | 8 | * [Goals](#goals) 9 | * [Checking a Feature Flag state](#checking-a-feature-flag-state) 10 | * [Setting a Feature Flag](#setting-a-feature-flag) 11 | * [Current Features](#current-features) 12 | 13 | ## Goals: 14 | + make it possible to add breaking changes in a minor release 15 | + give community members a chance to provide feedback for new functionality 16 | 17 | ## Checking a Feature Flag State 18 | 19 | Use `isEnabled` if you need to know the state of a feature flag programmatically. 20 | 21 | ```javascript 22 | import { isEnabled } from 'backbone.marionette'; 23 | 24 | isEnabled('fooFlag'); // false 25 | ``` 26 | 27 | ## Setting a Feature Flag 28 | 29 | Use `setEnabled` to change the value of a flag. 30 | While setting a flag at any point may work, these flags are designed to be set before 31 | any functionality of Marionette is used. Change flags after at your own risk. 32 | 33 | ```javascript 34 | import { setEnabled } from 'backbone.marionette'; 35 | 36 | setEnabled('fooFlag', true); 37 | 38 | const myApp = new MyApp({ 39 | region: '#app-hook' 40 | }); 41 | 42 | myApp.start(); 43 | ``` 44 | 45 | ## Current Features 46 | 47 | ### `childViewEventPrefix` 48 | 49 | *Default:* `false` 50 | 51 | This flag indicates whether [`childViewEventPrefix`](./events.md#a-child-views-event-prefix) 52 | for all views will return the default value of `'childview'` or if it will return `false` 53 | disabling [automatic event bubbling](./events.md#event-bubbling). 54 | 55 | ### `triggersPreventDefault` 56 | 57 | *Default:* `true` 58 | 59 | It indicates the whether or not [`View.triggers` will call `event.preventDefault()`](./dom.interactions.md#view-triggers-event-object) if not explicitly defined by the trigger. 60 | The default has been true, but for a future version [`false` is being considered](https://github.com/marionettejs/backbone.marionette/issues/2926). 61 | 62 | ### `triggersStopPropagating` 63 | 64 | *Default:* `true` 65 | 66 | It indicates the whether or not [`View.triggers` will call `event.stopPropagating()`](./dom.interactions.md#view-triggers-event-object) if not explicitly defined by the trigger. 67 | The default has been true, but for a future version [`false` is being considered](https://github.com/marionettejs/backbone.marionette/issues/2926). 68 | 69 | ### DEV_MODE 70 | 71 | *Default:* `false` 72 | 73 | If `true`, deprecation console warnings are issued at runtime. 74 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installing Marionette 2 | 3 | As with all JavaScript libraries, there are a number of ways to get started with 4 | a Marionette application. In this section we'll cover the most common ways. 5 | While some integrations are listed here, more resources are available in the integrations repo: 6 | [marionette-integrations](https://github.com/marionettejs/marionette-integrations) 7 | 8 | ## Documentation Index 9 | 10 | * [NPM and Webpack](#quick-start-using-npm-and-webpack) 11 | * [NPM and Brunch](#quick-start-using-npm-and-brunch) 12 | * [NPM and Browserify](#quick-start-using-npm-and-browserify) 13 | * [Browserify and Grunt](#browserify-and-grunt) 14 | * [Browserify and Gulp](#browserify-and-gulp) 15 | * [Getting Started](./basics.md) 16 | 17 | ## Quick start using NPM and Webpack 18 | [NPM](https://www.npmjs.com/) is the package manager for JavaScript. 19 | 20 | Installing with NPM through command-line interface 21 | ``` 22 | npm install backbone.marionette 23 | ``` 24 | 25 | [Webpack][webpack] is a build tool that makes it easy to pull your dependencies 26 | together into a single bundle to be delivered to your browser's ` 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /test/rollup.config.js: -------------------------------------------------------------------------------- 1 | import easySauce from 'easy-sauce'; 2 | 3 | import babel from 'rollup-plugin-babel'; 4 | import browsersync from 'rollup-plugin-browsersync'; 5 | import commonjs from 'rollup-plugin-commonjs'; 6 | import { eslint } from 'rollup-plugin-eslint'; 7 | import json from 'rollup-plugin-json'; 8 | import multiEntry from 'rollup-plugin-multi-entry'; 9 | import nodeResolve from 'rollup-plugin-node-resolve'; 10 | import nodeGlobals from 'rollup-plugin-node-globals'; 11 | 12 | const footer = 'this && this.Marionette && (this.Mn = this.Marionette);'; 13 | 14 | const isSauce = process.env.NODE_ENV === 'sauce'; 15 | 16 | function runSauce() { 17 | easySauce({ 18 | name: 'Marionette.js', 19 | username: process.env.SAUCE_USERNAME, 20 | key: process.env.SAUCE_ACCESS_KEY, 21 | port: '8080', 22 | testPath: '/test/runner.html', 23 | framework: 'mocha', 24 | platforms: [ 25 | ['Windows 10', 'internet explorer', 'latest'], 26 | ['Windows 10', 'MicrosoftEdge', 'latest'], 27 | ['Windows 10', 'chrome', 'latest'], 28 | ['macOS 10.13', 'chrome', 'latest'], 29 | ['macOS 10.13', 'firefox', 'latest'] 30 | ], 31 | service: 'sauce-connect' 32 | }) 33 | .on('message', message => { 34 | // eslint-disable-next-line 35 | console.log(message); 36 | }) 37 | .on('update', job => { 38 | // eslint-disable-next-line 39 | console.log(job.status); 40 | }) 41 | .on('done', (passed, jobs) => { 42 | if (passed) { 43 | // eslint-disable-next-line 44 | console.log('All tests passed!'); 45 | process.exit(0); 46 | } else { 47 | // eslint-disable-next-line 48 | console.error('Failures: ' + JSON.stringify(jobs, false, 2)); 49 | process.exit(1); 50 | } 51 | }) 52 | .on('error', error => { 53 | // eslint-disable-next-line 54 | console.error(error.message); 55 | process.exit(1); 56 | }); 57 | } 58 | 59 | export default { 60 | input: ['./test/setup/browser.js', './test/unit/**/*.js'], 61 | output: [ 62 | { 63 | file: './test/tmp/__spec-build.js', 64 | format: 'umd', 65 | name: 'Marionette', 66 | exports: 'named', 67 | sourcemap: true, 68 | footer 69 | } 70 | ], 71 | plugins: [ 72 | eslint({ exclude: ['./package.json'] }), 73 | commonjs(), 74 | multiEntry(), 75 | nodeGlobals(), 76 | nodeResolve(), 77 | json(), 78 | babel(), 79 | browsersync({ 80 | server: { 81 | baseDir: ['test', 'test/tmp', 'node_modules'], 82 | index: 'browsersync.html' 83 | }, 84 | open: !isSauce, 85 | callbacks: { 86 | ready(err, bs) { 87 | if (!isSauce) { return; } 88 | 89 | runSauce(); 90 | } 91 | } 92 | }) 93 | ] 94 | } 95 | -------------------------------------------------------------------------------- /test/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /test/setup/browser.js: -------------------------------------------------------------------------------- 1 | const mochaGlobals = require('../.globals.json').globals; 2 | 3 | global.mocha.setup('bdd'); 4 | global.onload = function() { 5 | global.mocha.checkLeaks(); 6 | global.mocha.globals(Object.keys(mochaGlobals)); 7 | 8 | const runner = global.mocha.run(); 9 | 10 | mochaResults(runner); 11 | require('./setup')(); 12 | }; 13 | 14 | const mochaResults = function(runner) { 15 | const failedTests = []; 16 | 17 | runner.on('end', function() { 18 | global.mochaResults = runner.stats; 19 | global.mochaResults.reports = failedTests; 20 | }); 21 | 22 | runner.on('fail', function(test, err) { 23 | failedTests.push({ 24 | title: test.title, 25 | fullTitle: test.fullTitle(), 26 | error: { 27 | message: err.message, 28 | stack: err.stack 29 | } 30 | }); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /test/setup/node.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | var sinonChai = require('sinon-chai'); 4 | var chaiJq = require('chai-jq'); 5 | 6 | chai.use(sinonChai); 7 | chai.use(chaiJq); 8 | 9 | global.chai = chai; 10 | global.sinon = sinon; 11 | 12 | if (!global.document || !global.window) { 13 | const JSDOM = require('jsdom').JSDOM; 14 | 15 | const opts = { 16 | runScripts: 'dangerously', 17 | url: 'http://localhost' 18 | }; 19 | 20 | const dom = new JSDOM(` 21 | 22 | 23 | 24 | 25 | `, opts); 26 | 27 | global.window = dom.window; 28 | global.document = global.window.document; 29 | global.navigator = global.window.navigator; 30 | 31 | } 32 | require('./setup')(); 33 | -------------------------------------------------------------------------------- /test/setup/setup.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | 3 | if (process.env.USE_LODASH) { 4 | const pathScore = require.resolve('underscore'); 5 | const pathDash = require.resolve('lodash'); 6 | require(pathDash); 7 | 8 | require.cache[pathScore] = require.cache[pathDash]; 9 | } 10 | 11 | const lib = process.env.USE_LODASH ? 'lodash' : 'underscore'; 12 | 13 | const _ = require('underscore'); 14 | 15 | // eslint-disable-next-line 16 | console.log('Using ' + lib + ': ' + _.VERSION); 17 | 18 | const Backbone = require('backbone'); 19 | const jQuery = require('jquery'); 20 | Backbone.$ = jQuery; 21 | Backbone.Radio = require('backbone.radio'); 22 | let Marionette = require('../../src/backbone.marionette'); 23 | 24 | Marionette = 'default' in Marionette ? Marionette.default : Marionette; 25 | 26 | global.$ = global.jQuery = jQuery; 27 | global._ = _; 28 | global.Backbone = Backbone; 29 | global.Marionette = Backbone.Marionette = Marionette; 30 | 31 | global.expect = global.chai.expect; 32 | 33 | let $fixtures; 34 | 35 | function setFixtures() { 36 | _.each(arguments, function(content) { 37 | $fixtures.append(content); 38 | }); 39 | } 40 | 41 | function clearFixtures() { 42 | $fixtures.empty(); 43 | } 44 | 45 | before(function() { 46 | $fixtures = $('
'); 47 | $('body').append($fixtures); 48 | this.setFixtures = setFixtures; 49 | this.clearFixtures = clearFixtures; 50 | }); 51 | 52 | beforeEach(function() { 53 | this.sinon = global.sinon.createSandbox(); 54 | }); 55 | 56 | afterEach(function() { 57 | this.sinon.restore(); 58 | window.location.hash = ''; 59 | Backbone.history.stop(); 60 | Backbone.history.handlers.length = 0; 61 | clearFixtures(); 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /test/unit/README.md: -------------------------------------------------------------------------------- 1 | ### Unit Tests 2 | 3 | 4 | ### Running unit tests 5 | 6 | 1. Running just unit tests - `yarn test` 7 | 8 | 2. Running coverage reporter - `yarn coverage`. 9 | To check coverage, open `./coverage/lcov-report/index.html`. 10 | 11 | 3. Running tests in browser - `yarn test-browser`. 12 | 13 | 14 | ### Common concepts for writing tests 15 | 16 | 1. Test suites should cover public API. 17 | 18 | > In most cases it will be public API testing, 19 | but sometimes we should test something like: When models added to collection, 20 | hence, in this case we are adding needed suites. 21 | 22 | 2. Code style. 23 | 24 | - Each `describe` should have name of tested method. 25 | 26 | > If it's possible, there should be not more then one nested describe for one method. 27 | 28 | _Wrong way_ 29 | 30 | ```javascript 31 | describe('#MyClass', function() { 32 | describe('some events in myMethod', function() { 33 | describe('some logic', function() { 34 | it('do something', function() { 35 | ... 36 | }); 37 | 38 | describe('some other logic', function() { 39 | // other nested describes 40 | }); 41 | }); 42 | }); 43 | }); 44 | ``` 45 | 46 | 47 | **Correct way** 48 | 49 | ```javascript 50 | describe('#MyClass', function() { 51 | describe('#myMethod', function() { 52 | describe('when some logic', function() { 53 | it('should do something', function() { 54 | ... 55 | }); 56 | }); 57 | 58 | describe('when some other logic', function() { 59 | it('should do something', function() { 60 | ... 61 | }); 62 | }); 63 | }); 64 | }); 65 | ``` 66 | 67 | - In case of testing some behavior 68 | 69 | > behavior means not Marionette Behavior class 70 | 71 | **Correct way** 72 | 73 | ```javascript 74 | describe('#MyClass', function() { 75 | describe('when some data was changed', function() { 76 | it('should do something', function() { 77 | ... 78 | }); 79 | }); 80 | }); 81 | ``` 82 | 83 | - `before/beforeEach` should consist only some preparation logic 84 | but inside it should not present calling methods you expect to test. 85 | 86 | _Wrong way_ 87 | 88 | ```javascript 89 | describe('#MyClass', function() { 90 | describe('#myMethod', function() { 91 | let myInstance; 92 | 93 | beforeEach(function() { 94 | myInstance = new MyClass({ 95 | render: this.sinon.spy 96 | }); 97 | myInstance.render(); 98 | }); 99 | 100 | it('should do something', function() { 101 | expect(myInstance.render).to.have.been.calledOnce; 102 | }); 103 | }); 104 | }); 105 | ``` 106 | 107 | 108 | **Correct way** 109 | 110 | ```javascript 111 | describe('#MyClass', function() { 112 | describe('#myMethod', function() { 113 | let myInstance; 114 | let renderSpy; 115 | 116 | beforeEach(function() { 117 | renderSpy = this.sinon.spy(); 118 | 119 | myInstance = new MyClass({ 120 | render: renderSpy 121 | }); 122 | }); 123 | 124 | it('should do something', function() { 125 | myInstance.render(); 126 | 127 | expect(renderSpy).to.have.been.calledOnce; 128 | }); 129 | }); 130 | }); 131 | ``` 132 | -------------------------------------------------------------------------------- /test/unit/application.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'underscore'; 4 | import Application from '../../src/application'; 5 | import View from '../../src/view'; 6 | 7 | describe('Marionette Application', function() { 8 | 9 | describe('#initialize', () => { 10 | describe('when instantiating an app with specified options', function() { 11 | let app; 12 | let appOptions; 13 | let initializeStub; 14 | 15 | beforeEach(function() { 16 | appOptions = {fooOption: 'foo'}; 17 | initializeStub = this.sinon.stub(Application.prototype, 'initialize'); 18 | this.sinon.spy(Application.prototype, '_initRadio'); 19 | }); 20 | 21 | it('should pass all arguments to the initialize method', function() { 22 | app = new Application(appOptions, 'fooArg'); 23 | 24 | expect(initializeStub).to.have.been.calledOn(app).and.calledWith(appOptions, 'fooArg'); 25 | }); 26 | 27 | it('should have a cidPrefix', function() { 28 | app = new Application(appOptions); 29 | 30 | expect(app.cidPrefix).to.equal('mna'); 31 | }); 32 | 33 | it('should have a cid', function() { 34 | app = new Application(appOptions); 35 | 36 | expect(app.cid).to.exist; 37 | }); 38 | 39 | it('should init the RadioMixin', function() { 40 | app = new Application(appOptions); 41 | 42 | expect(app._initRadio).to.have.been.called; 43 | }); 44 | }); 45 | }); 46 | 47 | describe('#start', function() { 48 | let app; 49 | let fooOptions; 50 | 51 | beforeEach(function() { 52 | fooOptions = {foo: 'bar'}; 53 | app = new Application(); 54 | }); 55 | 56 | it('should return current application context', function() { 57 | const result = app.start(fooOptions); 58 | 59 | expect(result).to.have.been.equal(app); 60 | }); 61 | }); 62 | 63 | describe('#onBeforeStart', function() { 64 | let fooApp; 65 | let fooOptions; 66 | let beforeStartStub; 67 | let onBeforeStartStub; 68 | 69 | beforeEach(function() { 70 | fooOptions = {foo: 'bar'}; 71 | beforeStartStub = this.sinon.stub(); 72 | onBeforeStartStub = this.sinon.stub(); 73 | 74 | const FooApp = Application.extend({ 75 | onBeforeStart: onBeforeStartStub 76 | }); 77 | 78 | fooApp = new FooApp(); 79 | fooApp.on('before:start', beforeStartStub); 80 | }); 81 | 82 | it('should run the onBeforeStart callback', function() { 83 | fooApp.start(fooOptions); 84 | 85 | expect(beforeStartStub).to.have.been.called; 86 | expect(onBeforeStartStub).to.have.been.called; 87 | }); 88 | 89 | it('should pass the startup option to the onBeforeStart callback', function() { 90 | fooApp.start(fooOptions); 91 | 92 | expect(beforeStartStub).to.have.been.calledOnce.and.calledWith(fooApp, fooOptions); 93 | expect(onBeforeStartStub).to.have.been.calledOnce.and.calledWith(fooApp, fooOptions); 94 | }); 95 | }); 96 | 97 | describe('#onStart', function() { 98 | let fooApp; 99 | let fooOptions; 100 | let startStub; 101 | let onStartStub; 102 | 103 | beforeEach(function() { 104 | fooOptions = {foo: 'bar'}; 105 | startStub = this.sinon.stub(); 106 | onStartStub = this.sinon.stub(); 107 | 108 | const FooApp = Application.extend({ 109 | onStart: onStartStub 110 | }); 111 | 112 | fooApp = new FooApp(); 113 | fooApp.on('start', startStub); 114 | }); 115 | 116 | it('should run the onStart callback', function() { 117 | fooApp.start(fooOptions); 118 | 119 | expect(startStub).to.have.been.called; 120 | expect(onStartStub).to.have.been.called; 121 | }); 122 | 123 | it('should pass the startup option to the callback', function() { 124 | fooApp.start(fooOptions); 125 | 126 | expect(startStub).to.have.been.calledOnce.and.calledWith(fooApp, fooOptions); 127 | expect(onStartStub).to.have.been.calledOnce.and.calledWith(fooApp, fooOptions); 128 | }); 129 | }); 130 | 131 | describe('#getRegion', function() { 132 | let app; 133 | let fooOptions; 134 | 135 | beforeEach(function() { 136 | fooOptions = { 137 | region: '#fixtures' 138 | }; 139 | app = new Application(fooOptions); 140 | }); 141 | 142 | it('should get the region selector with getRegion', function() { 143 | expect(app.getRegion().$el).to.have.length(1); 144 | }); 145 | }); 146 | 147 | describe('#showView', function() { 148 | let app; 149 | let view; 150 | let appRegion; 151 | let fooOptions; 152 | let showViewInRegionSpy; 153 | 154 | beforeEach(function() { 155 | fooOptions = { 156 | region: '#fixtures' 157 | }; 158 | view = new View({ 159 | template: _.template('ohai') 160 | }); 161 | app = new Application(fooOptions); 162 | 163 | appRegion = app.getRegion(); 164 | 165 | showViewInRegionSpy = this.sinon.spy(appRegion, 'show'); 166 | }); 167 | 168 | describe('when additional arguments was passed', function() { 169 | let fooArgs; 170 | 171 | beforeEach(function() { 172 | fooArgs = {foo: 'bar'}; 173 | }); 174 | 175 | it('should call show method in region with additional arguments', function() { 176 | app.showView(view, fooArgs); 177 | 178 | expect(showViewInRegionSpy).to.have.been.calledWith(view, fooArgs); 179 | }); 180 | }); 181 | 182 | describe('when just view as argument was passed', function() { 183 | it('should call show method in region', function() { 184 | app.showView(view); 185 | 186 | expect(showViewInRegionSpy).to.have.been.called; 187 | }); 188 | }); 189 | }); 190 | 191 | describe('#getView', function() { 192 | let app; 193 | let view; 194 | let fooOptions; 195 | 196 | beforeEach(function() { 197 | fooOptions = { 198 | region: '#fixtures' 199 | }; 200 | view = new View({ 201 | template: _.template('ohai') 202 | }); 203 | app = new Application(fooOptions); 204 | }); 205 | 206 | it('should return View which was shown', function() { 207 | app.showView(view); 208 | 209 | expect(app.getView()).to.have.deep.equal(view); 210 | }); 211 | }); 212 | }); 213 | -------------------------------------------------------------------------------- /test/unit/backbone.marionette.spec.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | import * as Mn from '../../src/backbone.marionette'; 4 | import Marionette from '../../src/backbone.marionette'; 5 | 6 | import {version} from '../../package.json'; 7 | 8 | import extend from '../../src/utils/extend'; 9 | 10 | import monitorViewEvents from '../../src/common/monitor-view-events'; 11 | 12 | import Events from '../../src/mixins/events'; 13 | 14 | import MnObject from '../../src/object'; 15 | import View from '../../src/view'; 16 | import CollectionView from '../../src/collection-view'; 17 | import Behavior from '../../src/behavior'; 18 | import Region from '../../src/region'; 19 | import Application from '../../src/application'; 20 | 21 | import DomApi from '../../src/config/dom'; 22 | 23 | import { 24 | isEnabled, 25 | setEnabled 26 | } from '../../src/config/features'; 27 | 28 | 29 | describe('backbone.marionette', function() { 30 | describe('Named Exports', function() { 31 | const namedExports = { 32 | View, 33 | CollectionView, 34 | MnObject, 35 | Region, 36 | Behavior, 37 | Application, 38 | isEnabled, 39 | setEnabled, 40 | monitorViewEvents, 41 | Events, 42 | extend, 43 | DomApi, 44 | }; 45 | 46 | _.each(namedExports, (val, key) => { 47 | it(`should have named export ${ key }`, function() { 48 | expect(Mn[key]).to.equal(val); 49 | }); 50 | }); 51 | }); 52 | 53 | describe('Default Export', function() { 54 | const namedExports = { 55 | View, 56 | CollectionView, 57 | MnObject, 58 | Region, 59 | Behavior, 60 | Application, 61 | isEnabled, 62 | setEnabled, 63 | monitorViewEvents, 64 | Events, 65 | extend, 66 | DomApi, 67 | }; 68 | 69 | _.each(namedExports, (val, key) => { 70 | it(`should have key ${ key }`, function() { 71 | expect(Marionette[key]).to.equal(val); 72 | }); 73 | }); 74 | 75 | it('should have key Object', function() { 76 | expect(Marionette.Object).to.equal(MnObject); 77 | }); 78 | }); 79 | 80 | describe('VERSION', function() { 81 | it('should attach the package.json version', function() { 82 | expect(Mn.VERSION).to.equal(version); 83 | }); 84 | }); 85 | 86 | describe('Proxied Utilities', function() { 87 | let context; 88 | 89 | beforeEach(function() { 90 | context = new MnObject(); 91 | }); 92 | 93 | it('should proxy bindEvents', function() { 94 | const entity = new MnObject(); 95 | const eventHandler = this.sinon.stub(); 96 | const events = { 'foo': eventHandler }; 97 | 98 | Mn.bindEvents(context, entity, events); 99 | entity.trigger('foo'); 100 | 101 | expect(eventHandler) 102 | .to.have.been.calledOnce 103 | .and.calledOn(context); 104 | }); 105 | 106 | it('should proxy unbindEvents', function() { 107 | this.sinon.spy(context, 'stopListening'); 108 | 109 | const entity = new MnObject(); 110 | context.listenTo(entity, 'foo', _.noop); 111 | 112 | Mn.unbindEvents(context, entity); 113 | 114 | expect(context.stopListening) 115 | .to.have.been.calledOnce 116 | .and.calledOn(context) 117 | .and.calledWith(entity); 118 | }); 119 | 120 | it('should proxy bindRequests', function() { 121 | const replyFooStub = this.sinon.stub(); 122 | const channel = { reply: this.sinon.stub() }; 123 | 124 | Mn.bindRequests(context, channel, {'foo': replyFooStub}); 125 | 126 | expect(channel.reply) 127 | .to.have.been.calledOnce 128 | .and.calledWith({'foo': replyFooStub}, context); 129 | }); 130 | 131 | it('should proxy unbindRequests', function() { 132 | const channel = { stopReplying: this.sinon.stub() }; 133 | 134 | Mn.unbindRequests(context, channel); 135 | 136 | expect(channel.stopReplying) 137 | .to.have.been.calledOnce 138 | .and.calledWith(null, null, context); 139 | }); 140 | 141 | it('should proxy mergeOptions', function() { 142 | context.foo = 'bar'; 143 | 144 | Mn.mergeOptions(context, { foo: 'baz' }, ['foo']); 145 | 146 | expect(context.foo).to.equal('baz'); 147 | }); 148 | 149 | it('should proxy getOption', function() { 150 | context.options.foo = 'bar'; 151 | 152 | expect(Mn.getOption(context, 'foo')).to.equal('bar'); 153 | }); 154 | 155 | it('should proxy normalizeMethods', function() { 156 | context.onFoo = this.sinon.stub(); 157 | 158 | expect(Mn.normalizeMethods(context, { foo: 'onFoo' })).to.deep.equal({ foo: context.onFoo }); 159 | }); 160 | 161 | it('should proxy triggerMethod', function() { 162 | context.onFoo = this.sinon.stub(); 163 | 164 | Mn.triggerMethod(context, 'foo', 'bar'); 165 | 166 | expect(context.onFoo) 167 | .to.have.been.calledOnce 168 | .and.calledOn(context) 169 | .and.calledWith('bar'); 170 | }); 171 | }); 172 | 173 | describe('#setDomApi', function() { 174 | const DomClasses = { 175 | CollectionView, 176 | Region, 177 | View 178 | }; 179 | 180 | const fakeDomApi = { 181 | foo: 'bar' 182 | }; 183 | 184 | _.each(DomClasses, function(Class, key) { 185 | it(`should setDomApi on ${ key }`, function() { 186 | this.sinon.spy(Class, 'setDomApi'); 187 | Mn.setDomApi(fakeDomApi); 188 | 189 | expect(Class.setDomApi) 190 | .to.be.calledOnce 191 | .and.calledWith(fakeDomApi); 192 | }); 193 | }); 194 | }); 195 | 196 | describe('#setRenderer', function() { 197 | let renderer; 198 | 199 | beforeEach(function() { 200 | renderer = View.prototype._renderHtml; 201 | }); 202 | 203 | afterEach(function() { 204 | Mn.setRenderer(renderer); 205 | }); 206 | 207 | const RendererClasses = { 208 | CollectionView, 209 | View 210 | }; 211 | 212 | const fakeRenderer = function() {}; 213 | 214 | _.each(RendererClasses, function(Class, key) { 215 | it(`should setRenderer on ${ key }`, function() { 216 | this.sinon.spy(Class, 'setRenderer'); 217 | 218 | Mn.setRenderer(fakeRenderer); 219 | expect(Class.setRenderer) 220 | .to.be.calledOnce 221 | .and.calledWith(fakeRenderer); 222 | }); 223 | }); 224 | }); 225 | }); 226 | -------------------------------------------------------------------------------- /test/unit/collection-view/collection-view-childviewcontainer.sepc.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import Backbone from 'backbone'; 3 | import CollectionView from '../../../src/collection-view'; 4 | import View from '../../../src/view'; 5 | 6 | describe('CollectionView - childViewContainer', function() { 7 | let MyCollectionView; 8 | let ChildView; 9 | let template; 10 | let collection; 11 | 12 | beforeEach(function() { 13 | collection = new Backbone.Collection([{ foo: 'bar' }, { foo: 'baz' }]); 14 | 15 | template = _.template('bazinga'); 16 | 17 | ChildView = View.extend({ 18 | tagName: 'li', 19 | template: _.template('<%=foo%>') 20 | }); 21 | 22 | MyCollectionView = CollectionView.extend({ 23 | childView: ChildView 24 | }); 25 | }); 26 | 27 | describe('when childViewContainer is undefined', function() { 28 | it('should set the $container to the $el', function() { 29 | const myCollectionView = new MyCollectionView({ collection }); 30 | myCollectionView.render(); 31 | 32 | expect(myCollectionView.$container).to.equal(myCollectionView.$el); 33 | }); 34 | }); 35 | 36 | describe('when childViewContainer is defined', function() { 37 | describe('when a selector within the el', function() { 38 | it('should should put the children within the found $container', function() { 39 | const myCollectionView = new MyCollectionView({ 40 | collection, 41 | template, 42 | childViewContainer: '#foo' 43 | }); 44 | myCollectionView.render(); 45 | 46 | expect(myCollectionView.$container).to.have.$text('barbaz'); 47 | }); 48 | }); 49 | 50 | describe('when a selector not within the el', function() { 51 | it('should should throw an error', function() { 52 | const myCollectionView = new MyCollectionView({ 53 | collection, 54 | template, 55 | childViewContainer: '#bar' 56 | }); 57 | 58 | expect(myCollectionView.render.bind(myCollectionView)) 59 | .to.throw('The specified "childViewContainer" was not found: #bar'); 60 | }); 61 | }); 62 | 63 | describe('when a function', function() { 64 | it('should should put the children within the found $container', function() { 65 | const myCollectionView = new MyCollectionView({ 66 | collection, 67 | template, 68 | childViewContainer: _.constant('#foo') 69 | }); 70 | myCollectionView.render(); 71 | 72 | expect(myCollectionView.$container).to.have.$text('barbaz'); 73 | }); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/unit/collection-view/collection-view-viewmixin.spec.js: -------------------------------------------------------------------------------- 1 | // Anything testing the integration of the ViewMixin, but not the ViewMixin itself. 2 | 3 | import _ from 'underscore'; 4 | import Backbone from 'backbone'; 5 | import CollectionView from '../../../src/collection-view'; 6 | import View from '../../../src/view'; 7 | 8 | describe('CollectionView - ViewMixin', function() { 9 | 10 | describe('when initializing a CollectionView', function() { 11 | let collectionView; 12 | let initBehaviorsSpy; 13 | let initializeSpy; 14 | let delegateEntityEventsSpy; 15 | 16 | const mergeOptions = { 17 | behaviors: {}, 18 | childViewEventPrefix: 'child', 19 | childViewEvents: {}, 20 | childViewTriggers: {}, 21 | collectionEvents: {}, 22 | modelEvents: {}, 23 | triggers: {}, 24 | ui: {} 25 | }; 26 | 27 | beforeEach(function() { 28 | const MyCollectionView = CollectionView.extend(); 29 | 30 | initBehaviorsSpy = this.sinon.spy(MyCollectionView.prototype, '_initBehaviors'); 31 | initializeSpy = this.sinon.spy(MyCollectionView.prototype, 'initialize'); 32 | delegateEntityEventsSpy = this.sinon.spy(MyCollectionView.prototype, 'delegateEntityEvents'); 33 | 34 | collectionView = new MyCollectionView(mergeOptions); 35 | }); 36 | 37 | _.each(mergeOptions, function(value, key) { 38 | it(`should merge ViewMixin option ${ key }`, function() { 39 | expect(collectionView[key]).to.equal(value); 40 | }); 41 | }); 42 | 43 | it('should call _initBehaviors', function() { 44 | expect(initBehaviorsSpy) 45 | .to.have.been.calledOnce 46 | .and.calledBefore(initializeSpy); 47 | }); 48 | 49 | it('should call delegateEntityEvents', function() { 50 | expect(delegateEntityEventsSpy) 51 | .to.have.been.calledOnce 52 | .and.calledAfter(initializeSpy); 53 | }); 54 | }); 55 | 56 | // _childViewEventHandler 57 | describe('when an event is triggered on a childView', function() { 58 | let collectionView; 59 | let handlerSpy; 60 | 61 | const eventArg = 'foo'; 62 | const dataArg = 'bar'; 63 | 64 | beforeEach(function() { 65 | const MyCollectionView = CollectionView.extend({ 66 | childView: View.extend({ template: _.noop }) 67 | }); 68 | const collection = new Backbone.Collection([{}, {}]); 69 | 70 | collectionView = new MyCollectionView({ collection, childViewEventPrefix: 'childview' }); 71 | 72 | handlerSpy = this.sinon.spy(collectionView, '_childViewEventHandler'); 73 | 74 | collectionView.render(); 75 | }); 76 | 77 | it('should call _childViewEventHandler', function() { 78 | const childView = collectionView.children.findByIndex(0); 79 | 80 | handlerSpy.resetHistory(); 81 | 82 | childView.triggerMethod(eventArg, dataArg); 83 | 84 | expect(handlerSpy) 85 | .to.be.calledOnce 86 | .and.calledWith(eventArg, dataArg); 87 | }); 88 | 89 | describe('when the childView is removed from the collectionView', function() { 90 | it('should not call _childViewEventHandler', function() { 91 | const childView = collectionView.children.findByIndex(0); 92 | 93 | collectionView.removeChildView(childView); 94 | 95 | handlerSpy.resetHistory(); 96 | 97 | childView.triggerMethod(eventArg, dataArg); 98 | 99 | expect(handlerSpy).to.not.be.called; 100 | }); 101 | }); 102 | }); 103 | 104 | describe('#_getImmediateChildren', function() { 105 | let collectionView; 106 | 107 | describe('when empty', function() { 108 | beforeEach(function() { 109 | collectionView = new CollectionView(); 110 | }); 111 | 112 | it('should return an empty array for getImmediateChildren', function() { 113 | expect(collectionView._getImmediateChildren()) 114 | .to.be.instanceof(Array) 115 | .and.to.have.length(0); 116 | }); 117 | }); 118 | 119 | describe('when there are children', function() { 120 | let childOne; 121 | let childTwo; 122 | 123 | beforeEach(function() { 124 | collectionView = new CollectionView({ 125 | collection: new Backbone.Collection([{}, {}]), 126 | childView: View.extend({ template: _.noop }) 127 | }); 128 | collectionView.render(); 129 | 130 | const children = collectionView.children; 131 | 132 | childOne = children.findByIndex(0); 133 | childTwo = children.findByIndex(1); 134 | }); 135 | 136 | it('should return an empty array for getImmediateChildren', function() { 137 | expect(collectionView._getImmediateChildren()) 138 | .to.be.instanceof(Array) 139 | .and.to.have.length(2) 140 | .and.to.contain(childOne) 141 | .and.to.contain(childTwo); 142 | }); 143 | }); 144 | }); 145 | 146 | describe('#_removeChildren', function() { 147 | let collectionView; 148 | let childOne; 149 | let childTwo; 150 | 151 | beforeEach(function() { 152 | collectionView = new CollectionView({ 153 | collection: new Backbone.Collection([{}, {}]), 154 | childView: View.extend({ template: _.noop }) 155 | }); 156 | collectionView.render(); 157 | 158 | const children = collectionView.children; 159 | 160 | childOne = children.findByIndex(0); 161 | childTwo = children.findByIndex(1); 162 | 163 | collectionView._removeChildren(); 164 | }); 165 | 166 | it('should empty the children', function() { 167 | expect(collectionView.children).to.have.lengthOf(0); 168 | }); 169 | 170 | it('should have destroyed all of the children', function() { 171 | expect(childOne.isDestroyed()).to.be.true; 172 | expect(childTwo.isDestroyed()).to.be.true; 173 | }); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /test/unit/common/bind-events.spec.js: -------------------------------------------------------------------------------- 1 | import { bindEvents, unbindEvents } from '../../../src/common/bind-events'; 2 | 3 | describe('bind-events', function() { 4 | let entity; 5 | let target; 6 | 7 | beforeEach(function() { 8 | entity = this.sinon.stub(); 9 | 10 | target = { 11 | handleFoo: this.sinon.stub(), 12 | listenTo: this.sinon.stub(), 13 | stopListening: this.sinon.stub(), 14 | bindEvents, 15 | unbindEvents 16 | }; 17 | 18 | this.sinon.spy(target, 'bindEvents'); 19 | this.sinon.spy(target, 'unbindEvents'); 20 | }); 21 | 22 | describe('bindEvents', function() { 23 | describe('when entity isnt passed', function() { 24 | beforeEach(function() { 25 | target.bindEvents(false, { 'foo': 'handleFoo' }); 26 | }); 27 | 28 | it('shouldnt bind any events', function() { 29 | expect(target.listenTo).not.to.have.been.called; 30 | }); 31 | 32 | it('should return the target', function() { 33 | expect(target.bindEvents).to.have.returned(target); 34 | }); 35 | }); 36 | 37 | describe('when bindings isnt passed', function() { 38 | beforeEach(function() { 39 | target.bindEvents(entity, null); 40 | }); 41 | 42 | it('shouldnt bind any events', function() { 43 | expect(target.listenTo).not.to.have.been.called; 44 | }); 45 | 46 | it('should return the target', function() { 47 | expect(target.bindEvents).to.have.returned(target); 48 | }); 49 | }); 50 | 51 | describe('when bindings is an object with an event handler hash', function() { 52 | it('should return the target', function() { 53 | target.bindEvents(entity, { 'foo': 'handleFoo' }); 54 | expect(target.bindEvents).to.have.returned(target); 55 | }); 56 | 57 | describe('when handler is a function', function() { 58 | it('should bind an event to targets handler', function() { 59 | const handleBar = this.sinon.stub(); 60 | target.bindEvents(entity, { 'bar': handleBar }); 61 | expect(target.listenTo) 62 | .to.have.been.calledOnce 63 | .and.calledWith(entity, { 'bar': handleBar }); 64 | }); 65 | }); 66 | 67 | describe('when handler is a string', function() { 68 | it('should bind an event to targets handler', function() { 69 | target.bindEvents(entity, { 'foo': 'handleFoo' }); 70 | expect(target.listenTo) 71 | .to.have.been.calledOnce 72 | .and.calledWith(entity, { 'foo': target.handleFoo }); 73 | }); 74 | }); 75 | }); 76 | 77 | describe('when bindings is not an object', function() { 78 | it('should error', function() { 79 | expect(function() { 80 | target.bindEvents(entity, 'handleFoo'); 81 | }).to.throw('Bindings must be an object.'); 82 | }); 83 | }); 84 | }); 85 | 86 | describe('unbindEvents', function() { 87 | describe('when entity isnt passed', function() { 88 | beforeEach(function() { 89 | target.unbindEvents(false, { 'foo': 'handleFoo' }); 90 | }); 91 | 92 | it('shouldnt unbind any events', function() { 93 | expect(target.stopListening).not.to.have.been.called; 94 | }); 95 | 96 | it('should return the target', function() { 97 | expect(target.unbindEvents).to.have.returned(target); 98 | }); 99 | }); 100 | 101 | describe('when bindings isnt passed', function() { 102 | beforeEach(function() { 103 | target.unbindEvents(entity, null); 104 | }); 105 | 106 | it('should unbind all events', function() { 107 | expect(target.stopListening) 108 | .to.have.been.calledOnce 109 | .and.calledWith(entity); 110 | }); 111 | 112 | it('should return the target', function() { 113 | expect(target.unbindEvents).to.have.returned(target); 114 | }); 115 | }); 116 | 117 | describe('when bindings is an object with an event handler hash', function() { 118 | it('should return the target', function() { 119 | target.unbindEvents(entity, { 'foo': 'handleFoo' }) 120 | expect(target.unbindEvents).to.have.returned(target); 121 | }); 122 | 123 | describe('when handler is a function', function() { 124 | it('should unbind an event', function() { 125 | const handleBar = this.sinon.stub(); 126 | target.unbindEvents(entity, { 'bar': handleBar }); 127 | expect(target.stopListening) 128 | .to.have.been.calledOnce 129 | .and.calledWith(entity, { 'bar': handleBar }); 130 | }); 131 | }); 132 | 133 | describe('when handler is a string', function() { 134 | describe('when one handler is passed', function() { 135 | it('should unbind an event', function() { 136 | target.unbindEvents(entity, { 'foo': 'handleFoo' }); 137 | expect(target.stopListening) 138 | .to.have.been.calledOnce 139 | .and.calledWith(entity, { 'foo': target.handleFoo }); 140 | }); 141 | }); 142 | }); 143 | }); 144 | 145 | describe('when bindings is not an object', function() { 146 | it('should error', function() { 147 | expect(function() { 148 | target.unbindEvents(entity, 'handleFoo'); 149 | }).to.throw('Bindings must be an object.'); 150 | }); 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /test/unit/common/bind-request.spec.js: -------------------------------------------------------------------------------- 1 | import { bindRequests, unbindRequests } from '../../../src/common/bind-requests'; 2 | 3 | describe('bind-requests', function() { 4 | let channel; 5 | let target; 6 | 7 | beforeEach(function() { 8 | channel = { 9 | reply: this.sinon.stub(), 10 | stopReplying: this.sinon.stub() 11 | }; 12 | 13 | target = { 14 | replyFoo: this.sinon.stub(), 15 | bindRequests, 16 | unbindRequests 17 | }; 18 | 19 | this.sinon.spy(target, 'bindRequests'); 20 | this.sinon.spy(target, 'unbindRequests') 21 | }); 22 | 23 | describe('bindRequests', function() { 24 | describe('when channel isnt passed', function() { 25 | beforeEach(function() { 26 | target.bindRequests(false, { 'foo': 'replyFoo' }); 27 | }); 28 | 29 | it('shouldnt bind any requests', function() { 30 | expect(channel.reply).not.to.have.been.called; 31 | }); 32 | 33 | it('should return the target', function() { 34 | expect(target.bindRequests).to.have.returned(target); 35 | }); 36 | }); 37 | 38 | describe('when bindings isnt passed', function() { 39 | beforeEach(function() { 40 | target.bindRequests(channel, null); 41 | }); 42 | 43 | it('shouldnt bind any requests', function() { 44 | expect(channel.reply).not.to.have.been.called; 45 | }); 46 | 47 | it('should return the target', function() { 48 | expect(target.bindRequests).to.have.returned(target); 49 | }); 50 | }); 51 | 52 | describe('when bindings is an object with an event handler hash', function() { 53 | it('should return the target', function() { 54 | target.bindRequests(channel, { 'foo': 'replyFoo' }) 55 | expect(target.bindRequests).to.have.returned(target); 56 | }); 57 | 58 | describe('when handler is a function', function() { 59 | it('should bind a request to targets handler', function() { 60 | const replyBar = this.sinon.stub(); 61 | target.bindRequests(channel, { 'bar': replyBar }); 62 | expect(channel.reply) 63 | .to.have.been.calledOnce 64 | .and.calledWith({ 'bar': replyBar }); 65 | }); 66 | }); 67 | 68 | describe('when handler is a string', function() { 69 | describe('when one handler is passed', function() { 70 | it('should bind a request to targets handler', function() { 71 | target.bindRequests(channel, { 'foo': 'replyFoo' }); 72 | expect(channel.reply) 73 | .to.have.been.calledOnce 74 | .and.calledWith({ 'foo': target.replyFoo }); 75 | }); 76 | }); 77 | }); 78 | }); 79 | 80 | describe('when bindings is not an object', function() { 81 | it('should error', function() { 82 | expect(function() { 83 | target.bindRequests(channel, 'replyFoo'); 84 | }).to.throw('Bindings must be an object.'); 85 | }); 86 | }); 87 | }); 88 | 89 | describe('unbindRequests', function() { 90 | describe('when channel isnt passed', function() { 91 | beforeEach(function() { 92 | target.unbindRequests(false, { 'foo': 'replyFoo' }); 93 | }); 94 | 95 | it('shouldnt unbind any request', function() { 96 | expect(channel.stopReplying).not.to.have.been.called; 97 | }); 98 | 99 | it('should return the target', function() { 100 | expect(target.unbindRequests).to.have.returned(target); 101 | }); 102 | }); 103 | 104 | describe('when bindings isnt passed', function() { 105 | beforeEach(function() { 106 | target.unbindRequests(channel, null); 107 | }); 108 | 109 | it('should unbind all requests', function() { 110 | expect(channel.stopReplying) 111 | .to.have.been.calledOnce 112 | .and.calledWith(null, null, target); 113 | }); 114 | 115 | it('should return the target', function() { 116 | expect(target.unbindRequests).to.have.returned(target); 117 | }); 118 | }); 119 | 120 | describe('when bindings is an object with an event handler hash', function() { 121 | it('should return the target', function() { 122 | target.unbindRequests(channel, { 'foo': 'replyFoo' }); 123 | expect(target.unbindRequests).to.have.returned(target); 124 | }); 125 | 126 | describe('when handler is a function', function() { 127 | it('should unbind an request', function() { 128 | const replyBar = this.sinon.stub(); 129 | target.unbindRequests(channel, { 'bar': replyBar }) 130 | expect(channel.stopReplying) 131 | .to.have.been.calledOnce 132 | .and.calledWith({ 'bar': replyBar }); 133 | }); 134 | }); 135 | 136 | describe('when handler is a string', function() { 137 | describe('when one handler is passed', function() { 138 | it('should unbind an request', function() { 139 | target.unbindRequests(channel, { 'foo': 'replyFoo' }); 140 | expect(channel.stopReplying) 141 | .to.have.been.calledOnce 142 | .and.calledWith({ 'foo': target.replyFoo }); 143 | }); 144 | }); 145 | }); 146 | }); 147 | 148 | describe('when bindings is not an object', function() { 149 | it('should error', function() { 150 | expect(function() { 151 | target.unbindRequests(channel, 'replyFoo'); 152 | }).to.throw('Bindings must be an object.'); 153 | }); 154 | }); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /test/unit/common/get-option.spec.js: -------------------------------------------------------------------------------- 1 | import getOption from '../../../src/common/get-option'; 2 | 3 | describe('get option', function() { 4 | describe('when calling without arguments', function() { 5 | it('should return undefined', function() { 6 | expect(getOption()).to.be.undefined; 7 | }); 8 | }); 9 | 10 | describe('when an object only has the option set on the definition', function() { 11 | it('should return that definitions option', function() { 12 | const target = { 13 | foo: 'bar', 14 | getOption 15 | }; 16 | 17 | expect(target.getOption('foo')).to.equal(target.foo); 18 | }); 19 | }); 20 | 21 | describe('when an object only has the option set on the options', function() { 22 | it('should return value from the options', function() { 23 | const target = { 24 | options: {foo: 'bar'}, 25 | getOption 26 | }; 27 | 28 | expect(target.getOption('foo')).to.equal(target.options.foo); 29 | }); 30 | }); 31 | 32 | describe('when an object has the option set on the options, and it is a "falsey" value', function() { 33 | it('should return value from the options', function() { 34 | const target = { 35 | options: {foo: false}, 36 | getOption 37 | }; 38 | 39 | expect(target.getOption('foo')).to.equal(target.options.foo); 40 | }); 41 | }); 42 | 43 | describe('when an object has the option set on the options, and it is a "undefined" value', function() { 44 | it('should return the objects value', function() { 45 | const target = { 46 | foo: 'bar', 47 | options: {foo: undefined}, 48 | getOption 49 | }; 50 | 51 | expect(target.getOption('foo')).to.equal(target.foo); 52 | }); 53 | }); 54 | 55 | describe('when an object has the option set on both the definition and options', function() { 56 | it('should return that value from the options', function() { 57 | const target = { 58 | foo: 'bar', 59 | options: {foo: 'baz'}, 60 | getOption 61 | }; 62 | 63 | expect(target.getOption('foo')).to.equal(target.options.foo); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/unit/common/merge-options.spec.js: -------------------------------------------------------------------------------- 1 | import mergeOptions from '../../../src/common/merge-options'; 2 | 3 | describe('mergeOptions', function() { 4 | let target; 5 | 6 | beforeEach(function() { 7 | target = { 8 | myOptions: ['color', 'size'], 9 | mergeOptions, 10 | initialize(options) { 11 | this.mergeOptions(options, this.myOptions); 12 | } 13 | }; 14 | }); 15 | 16 | describe('when calling with undefined options', function() { 17 | it('should return instantly without merging anything', function() { 18 | expect(mergeOptions()).to.be.undefined; 19 | }); 20 | }); 21 | 22 | describe('when no matching the keys', function() { 23 | it('should not merge any of those options', function() { 24 | target.initialize({ 25 | hungry: true, 26 | country: 'USA' 27 | }); 28 | 29 | expect(target).to.not.contain.keys('hungry', 'country'); 30 | }); 31 | }); 32 | 33 | describe('when some matching the keys', function() { 34 | beforeEach(function() { 35 | target.initialize({ 36 | hungry: true, 37 | country: 'USA', 38 | color: 'blue' 39 | }); 40 | }); 41 | 42 | it('should not merge the ones that do not match', function() { 43 | expect(target).to.not.contain.keys('hungry', 'country'); 44 | }); 45 | 46 | it('should merge the ones that match', function() { 47 | expect(target).to.contain.keys('color'); 48 | }); 49 | }); 50 | 51 | describe('when all matching the keys', function() { 52 | it('should merge all of the options', function() { 53 | target.initialize({ 54 | size: 'large', 55 | color: 'blue' 56 | }); 57 | 58 | expect(target).to.contain.keys('color', 'size'); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/unit/common/monitor-view-events.js: -------------------------------------------------------------------------------- 1 | import View from '../../../src/view'; 2 | import monitorViewEvents from '../../../src/common/monitor-view-events'; 3 | 4 | describe('monitorViewEvents', function() { 5 | describe('when the monitor is disabled', function() { 6 | let view; 7 | 8 | beforeEach(function() { 9 | const NonMonitoredView = View.extend({ 10 | monitorViewEvents: false 11 | }); 12 | 13 | view = new NonMonitoredView(); 14 | 15 | this.sinon.spy(view, 'on'); 16 | }); 17 | 18 | it('should not attach events', function() { 19 | monitorViewEvents(view); 20 | expect(view.on).to.not.have.been.called; 21 | }); 22 | }); 23 | 24 | describe('when the view is already monitored', function() { 25 | let view; 26 | 27 | beforeEach(function() { 28 | view = new View(); 29 | 30 | monitorViewEvents(view); 31 | 32 | this.sinon.spy(view, 'on'); 33 | }); 34 | 35 | it('should not attach events', function() { 36 | monitorViewEvents(view); 37 | expect(view.on).to.not.have.been.called; 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/unit/common/normalize-methods.spec.js: -------------------------------------------------------------------------------- 1 | import View from '../../../src/view'; 2 | 3 | describe('normalizeMethods', function() { 4 | 'use strict'; 5 | 6 | let view; 7 | 8 | beforeEach(function() { 9 | const MyView = View.extend({ 10 | foo: this.sinon.stub() 11 | }); 12 | view = new MyView(); 13 | }); 14 | 15 | describe('when called with no value', function() { 16 | it('should return nothing', function() { 17 | expect(view.normalizeMethods()).to.be.undefined; 18 | }); 19 | }); 20 | 21 | describe('when called with a hash of functions and strings', function() { 22 | let normalizedHash; 23 | let hash; 24 | 25 | beforeEach(function() { 26 | hash = { 27 | 'foo': 'foo', 28 | 'bar': 'bar' 29 | }; 30 | normalizedHash = view.normalizeMethods(hash); 31 | }); 32 | 33 | it('should convert the strings that exist as functions to functions', function() { 34 | expect(normalizedHash).to.have.property('foo'); 35 | }); 36 | 37 | it('should ignore strings that dont exist as functions on the context', function() { 38 | expect(normalizedHash).not.to.have.property('bar'); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/unit/common/trigger-method.spec.js: -------------------------------------------------------------------------------- 1 | import triggerMethod from '../../../src/common/trigger-method'; 2 | 3 | describe('triggerMethod', function() { 4 | let target; 5 | 6 | beforeEach(function() { 7 | target = { 8 | trigger: this.sinon.stub(), 9 | triggerMethod 10 | }; 11 | 12 | this.sinon.spy(target, 'triggerMethod'); 13 | }); 14 | 15 | describe('when no onEventName method matcheds the event', function() { 16 | beforeEach(function() { 17 | target.triggerMethod('event:name', 'foo', 'bar'); 18 | }); 19 | 20 | it('should trigger all arguments', function() { 21 | expect(target.trigger) 22 | .to.have.been.calledOnce 23 | .and.calledOn(target) 24 | .and.calledWith('event:name', 'foo', 'bar'); 25 | }); 26 | 27 | it('should return undefined', function() { 28 | expect(target.triggerMethod).to.have.returned(undefined); 29 | }); 30 | }); 31 | 32 | describe('when an onEventName method on the target matches the event', function() { 33 | beforeEach(function() { 34 | target.onEventName = this.sinon.stub().returns('baz'); 35 | target.triggerMethod('event:name', 'foo', 'bar'); 36 | }); 37 | 38 | it('should trigger all arguments', function() { 39 | expect(target.trigger) 40 | .to.have.been.calledOnce 41 | .and.calledOn(target) 42 | .and.calledWith('event:name', 'foo', 'bar'); 43 | }); 44 | 45 | it('should call onEventName methods on the target', function() { 46 | expect(target.onEventName) 47 | .to.have.been.calledOnce 48 | .and.calledWith('foo', 'bar'); 49 | }); 50 | 51 | it('should return baz', function() { 52 | expect(target.triggerMethod).to.have.returned('baz'); 53 | }); 54 | }); 55 | 56 | describe('when an onEventName method on the target options matches the event', function() { 57 | beforeEach(function() { 58 | target.options = { 59 | onEventName: this.sinon.stub().returns('baz') 60 | }; 61 | target.triggerMethod('event:name', 'foo', 'bar'); 62 | }); 63 | 64 | it('should trigger all arguments', function() { 65 | expect(target.trigger) 66 | .to.have.been.calledOnce 67 | .and.calledWith('event:name', 'foo', 'bar'); 68 | }); 69 | 70 | it('should call onEventName methods on the target', function() { 71 | expect(target.options.onEventName) 72 | .to.have.been.calledOnce 73 | .and.calledWith('foo', 'bar') 74 | .and.calledOn(target); 75 | }); 76 | 77 | it('should return baz', function() { 78 | expect(target.triggerMethod).to.have.returned('baz'); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/unit/config/features.spec.js: -------------------------------------------------------------------------------- 1 | import { setEnabled, isEnabled } from '../../../src/config/features'; 2 | 3 | describe('features', function() { 4 | it('enabled when its present and true', function() { 5 | setEnabled('foo', true); 6 | expect(isEnabled('foo')).to.be.true; 7 | }); 8 | 9 | it('disabled when its present and false', function() { 10 | setEnabled('foo', false); 11 | expect(isEnabled('foo')).to.be.false; 12 | }); 13 | 14 | it('disabled when not present', function() { 15 | expect(isEnabled('foo')).to.be.false; 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/unit/config/renderer.spec.js: -------------------------------------------------------------------------------- 1 | import { setRenderer } from '../../../src/config/renderer'; 2 | 3 | describe('#setRenderer', function() { 4 | let MyObject; 5 | 6 | beforeEach(function() { 7 | MyObject = function() {}; 8 | MyObject.setRenderer = setRenderer; 9 | }); 10 | 11 | it('should return the current class', function() { 12 | expect(MyObject.setRenderer()).to.be.eq(MyObject); 13 | }); 14 | 15 | it('should set _renderHtml on the class', function() { 16 | const renderer = function() {}; 17 | MyObject.setRenderer(renderer); 18 | expect(MyObject.prototype._renderHtml).to.equal(renderer); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/unit/destroying-views.spec.js: -------------------------------------------------------------------------------- 1 | import View from '../../src/view'; 2 | 3 | 4 | describe('destroying views', function() { 5 | 'use strict'; 6 | 7 | describe('when destroying a Marionette.View multiple times', function() { 8 | let onDestroyStub; 9 | let view; 10 | 11 | beforeEach(function() { 12 | onDestroyStub = this.sinon.spy(function() { 13 | return this.isRendered(); 14 | }); 15 | 16 | view = new View(); 17 | view.onDestroy = onDestroyStub; 18 | 19 | view.destroy(); 20 | view.destroy(); 21 | }); 22 | 23 | it('should only run the destroying code once', function() { 24 | expect(onDestroyStub).to.have.been.calledOnce; 25 | }); 26 | 27 | it('should mark the view as destroyed', function() { 28 | expect(view).to.have.property('_isDestroyed', true); 29 | }); 30 | }); 31 | 32 | describe('when destroying a Marionette.View multiple times', function() { 33 | let onBeforeDestroyStub; 34 | let itemView; 35 | 36 | beforeEach(function() { 37 | onBeforeDestroyStub = this.sinon.stub(); 38 | 39 | itemView = new View(); 40 | itemView.onBeforeDestroy = onBeforeDestroyStub; 41 | 42 | itemView.destroy(); 43 | itemView.destroy(); 44 | }); 45 | 46 | it('should only run the destroying code once', function() { 47 | expect(onBeforeDestroyStub).to.have.been.calledOnce; 48 | }); 49 | 50 | it('should mark the view as destroyed', function() { 51 | expect(itemView).to.have.property('_isDestroyed', true); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/unit/get-immediate-children.spec.js: -------------------------------------------------------------------------------- 1 | import View from '../../src/view'; 2 | 3 | describe('_getImmediateChildren', function() { 4 | let BaseView; 5 | 6 | beforeEach(function() { 7 | // A suitable view to use as a child 8 | BaseView = View.extend({ 9 | template: _.noop 10 | }); 11 | }); 12 | 13 | describe('Marionette.View', function() { 14 | let view; 15 | 16 | beforeEach(function() { 17 | view = new View(); 18 | }); 19 | it('should return an empty array for getImmediateChildren', function() { 20 | expect(view._getImmediateChildren()) 21 | .to.be.instanceof(Array) 22 | .and.to.have.length(0); 23 | }); 24 | 25 | describe('without regions', function() { 26 | let layoutView; 27 | 28 | beforeEach(function() { 29 | layoutView = new View({ 30 | template: _.noop 31 | }); 32 | }); 33 | it('should return an empty array for getImmediateChildren', function() { 34 | expect(layoutView._getImmediateChildren()) 35 | .to.be.instanceof(Array) 36 | .and.to.have.length(0); 37 | }); 38 | }); 39 | 40 | describe('when there are empty regions', function() { 41 | let layoutView; 42 | 43 | beforeEach(function() { 44 | layoutView = new View({ 45 | template: _.template('
'), 46 | regions: { 47 | main: '.main', 48 | footer: '.footer' 49 | } 50 | }); 51 | layoutView.render(); 52 | }); 53 | it('should return an empty array for getImmediateChildren', function() { 54 | expect(layoutView._getImmediateChildren()) 55 | .to.be.instanceof(Array) 56 | .and.to.have.length(0); 57 | }); 58 | }); 59 | 60 | describe('when there are non-empty regions', function() { 61 | let layoutView; 62 | let childOne; 63 | let childTwo; 64 | 65 | beforeEach(function() { 66 | layoutView = new View({ 67 | template: _.template('
'), 68 | regions: { 69 | main: 'main', 70 | footer: 'footer' 71 | } 72 | }); 73 | layoutView.render(); 74 | childOne = new BaseView(); 75 | childTwo = new BaseView(); 76 | layoutView.getRegion('main').show(childOne); 77 | layoutView.getRegion('footer').show(childTwo); 78 | }); 79 | it('should return an empty array for getImmediateChildren', function() { 80 | expect(layoutView._getImmediateChildren()) 81 | .to.be.instanceof(Array) 82 | .and.to.have.length(2) 83 | .and.to.contain(childOne) 84 | .and.to.contain(childTwo); 85 | }); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/unit/mixins/common.spec.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import CommonMixin from '../../../src/mixins/common'; 3 | 4 | describe('Common Mixin', function() { 5 | describe('#setOptions', function() { 6 | let object; 7 | const classOptions = []; 8 | const options = { 9 | foo: 'baz', 10 | baz: 'baz' 11 | }; 12 | 13 | beforeEach(function() { 14 | object = _.extend({ 15 | options() { 16 | return { 17 | foo: 'bar', 18 | bar: 'baz' 19 | }; 20 | } 21 | }, CommonMixin); 22 | 23 | this.sinon.spy(object, 'mergeOptions'); 24 | 25 | object._setOptions(options, classOptions); 26 | }); 27 | 28 | it('should not mutate the options argument', function() { 29 | expect(options).to.eql({ 30 | foo: 'baz', 31 | baz: 'baz' 32 | }) 33 | }); 34 | 35 | // This test covers merge order and options as a function 36 | it('should set options on the context', function() { 37 | expect(object.options).to.eql({ 38 | foo: 'baz', 39 | bar: 'baz', 40 | baz: 'baz' 41 | }); 42 | }); 43 | 44 | it('should call mergeOptions', function() { 45 | expect(object.mergeOptions) 46 | .to.have.been.calledOnce 47 | .and.calledWith(options, classOptions); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/unit/mixins/delegate-entity-events.spec.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import Backbone from 'backbone'; 3 | import DelegateEntityEventsMixin from '../../../src/mixins/delegate-entity-events'; 4 | 5 | describe('delegate entity events mixin', function() { 6 | let obj; 7 | let model; 8 | let collection; 9 | 10 | beforeEach(function() { 11 | obj = _.extend({ 12 | bindEvents: this.sinon.stub(), 13 | unbindEvents: this.sinon.stub(), 14 | }, DelegateEntityEventsMixin); 15 | 16 | model = new Backbone.Model(); 17 | collection = new Backbone.Collection(); 18 | }); 19 | 20 | describe('#_delegateEntityEvents', function() { 21 | describe('when passed a model', function() { 22 | describe('when modelEvents is an object', function() { 23 | beforeEach(function() { 24 | obj.modelEvents = { foo: 'onFoo' }; 25 | obj._delegateEntityEvents(model); 26 | }); 27 | 28 | it('should cache modelEvents', function() { 29 | expect(obj._modelEvents).to.equal(obj.modelEvents); 30 | }); 31 | 32 | it('should call bindEvents', function() { 33 | expect(obj.bindEvents) 34 | .to.have.been.calledOnce 35 | .to.have.been.calledWith(model, obj.modelEvents); 36 | }); 37 | }); 38 | 39 | describe('when modelEvents is a method', function() { 40 | const modelEvents = { foo: 'onFoo' }; 41 | 42 | beforeEach(function() { 43 | obj.modelEvents = this.sinon.stub().returns(modelEvents); 44 | obj._delegateEntityEvents(model); 45 | }); 46 | 47 | it('should cache modelEvents', function() { 48 | expect(obj._modelEvents).to.equal(modelEvents); 49 | }); 50 | 51 | it('should call bindEvents', function() { 52 | expect(obj.bindEvents) 53 | .to.have.been.calledOnce 54 | .to.have.been.calledWith(model, modelEvents); 55 | }); 56 | }); 57 | }); 58 | 59 | describe('when passed a collection', function() { 60 | describe('when collectionEvents is an object', function() { 61 | beforeEach(function() { 62 | obj.collectionEvents = { foo: 'onFoo' }; 63 | obj._delegateEntityEvents(null, collection); 64 | }); 65 | 66 | it('should cache collectionEvents', function() { 67 | expect(obj._collectionEvents).to.equal(obj.collectionEvents); 68 | }); 69 | 70 | it('should call bindEvents', function() { 71 | expect(obj.bindEvents) 72 | .to.have.been.calledOnce 73 | .to.have.been.calledWith(collection, obj.collectionEvents); 74 | }); 75 | }); 76 | 77 | describe('when collectionEvents is a method', function() { 78 | const collectionEvents = { foo: 'onFoo' }; 79 | 80 | beforeEach(function() { 81 | obj.collectionEvents = this.sinon.stub().returns(collectionEvents); 82 | obj._delegateEntityEvents(null, collection); 83 | }); 84 | 85 | it('should cache modelEvents', function() { 86 | expect(obj._collectionEvents).to.equal(collectionEvents); 87 | }); 88 | 89 | it('should call bindEvents', function() { 90 | expect(obj.bindEvents) 91 | .to.have.been.calledOnce 92 | .to.have.been.calledWith(collection, collectionEvents); 93 | }); 94 | }); 95 | }); 96 | 97 | describe('when entities are not passed', function() { 98 | beforeEach(function() { 99 | obj._delegateEntityEvents(); 100 | }); 101 | 102 | it('should not call bindEvents', function() { 103 | expect(obj.bindEvents).to.not.have.been.called; 104 | }); 105 | 106 | it('should not cache event handlers', function() { 107 | expect(obj).to.not.have.property('_modelEvents'); 108 | expect(obj).to.not.have.property('_collectionEvents'); 109 | }); 110 | }); 111 | }); 112 | 113 | describe('#_undelegateEntityEvents', function() { 114 | describe('when modelEvents have been cached', function() { 115 | beforeEach(function() { 116 | obj._modelEvents = 'foo'; 117 | obj._undelegateEntityEvents(model, collection); 118 | }); 119 | 120 | it('should call unbindEvents', function() { 121 | expect(obj.unbindEvents) 122 | .to.have.been.calledOnce 123 | .to.have.been.calledWith(model, 'foo'); 124 | }); 125 | 126 | it('should remove the cache', function() { 127 | expect(obj).to.not.have.property('_modelEvents'); 128 | }); 129 | }); 130 | 131 | describe('when collectionEvents have been cached', function() { 132 | beforeEach(function() { 133 | obj._collectionEvents = 'foo'; 134 | obj._undelegateEntityEvents(model, collection); 135 | }); 136 | 137 | it('should call unbindEvents', function() { 138 | expect(obj.unbindEvents) 139 | .to.have.been.calledOnce 140 | .to.have.been.calledWith(collection, 'foo'); 141 | }); 142 | 143 | it('should remove the cache', function() { 144 | expect(obj).to.not.have.property('_collectionEvents'); 145 | }); 146 | }); 147 | 148 | describe('when no events are cached', function() { 149 | it('should not call unbindEvents', function() { 150 | obj._undelegateEntityEvents(model, collection); 151 | expect(obj.unbindEvents).to.not.have.been.called; 152 | }); 153 | }); 154 | }); 155 | 156 | describe('_deleteEntityEventHandlers', function() { 157 | it('should remove cached handlers', function() { 158 | obj._modelEvents = 'foo'; 159 | obj._collectionEvents = 'bar'; 160 | obj._deleteEntityEventHandlers(); 161 | 162 | expect(obj).to.not.have.property('_modelEvents'); 163 | expect(obj).to.not.have.property('_collectionEvents'); 164 | }); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /test/unit/mixins/destroy.spec.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import DestroyMixin from '../../../src/mixins/destroy'; 3 | 4 | describe('Destroy Mixin', function() { 5 | let obj; 6 | 7 | beforeEach(function() { 8 | obj = _.extend({ 9 | triggerMethod: this.sinon.stub(), 10 | stopListening: this.sinon.stub() 11 | }, DestroyMixin); 12 | 13 | this.sinon.spy(obj, 'destroy'); 14 | }); 15 | 16 | it('should not be destroyed by default', function() { 17 | expect(obj.isDestroyed()).to.be.false; 18 | }); 19 | 20 | describe('when destroying', function() { 21 | beforeEach(function() { 22 | obj.destroy({ foo: 'bar' }); 23 | }); 24 | 25 | it('should be destroyed', function() { 26 | expect(obj.isDestroyed()).to.be.true; 27 | }); 28 | 29 | it('should trigger destroy events', function() { 30 | expect(obj.triggerMethod) 31 | .to.have.been.calledTwice 32 | .and.calledWith('before:destroy', obj, { foo: 'bar' }) 33 | .and.calledWith('destroy', obj, { foo: 'bar' }); 34 | }); 35 | 36 | it('should stopListening', function() { 37 | expect(obj.stopListening) 38 | .to.have.been.calledOnce 39 | .and.not.calledBefore(obj.triggerMethod); 40 | }); 41 | 42 | it('should return the instance', function() { 43 | expect(obj.destroy).to.have.returned(obj); 44 | }); 45 | }); 46 | 47 | describe('when destroying a destroyed object', function() { 48 | beforeEach(function() { 49 | obj.destroy(); 50 | obj.triggerMethod.reset(); 51 | obj.destroy(); 52 | }); 53 | 54 | it('should not trigger any events', function() { 55 | expect(obj.triggerMethod).to.not.have.been.called; 56 | }); 57 | 58 | it('should return the instance', function() { 59 | expect(obj.destroy).to.have.returned(obj); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/unit/mixins/radio.spec.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import Backbone from 'backbone'; 3 | import Radio from 'backbone.radio'; 4 | import RadioMixin from '../../../src/mixins/radio'; 5 | 6 | describe('Radio Mixin on Marionette.Object', function() { 7 | let radioObject; 8 | let channelFoo; 9 | 10 | beforeEach(function() { 11 | radioObject = _.extend({ 12 | // Simulate implementation 13 | initialize() { 14 | this._initRadio(); 15 | }, 16 | bindEvents: this.sinon.stub(), 17 | bindRequests: this.sinon.stub(), 18 | }, Backbone.Events, RadioMixin); 19 | 20 | channelFoo = Radio.channel('foo'); 21 | }); 22 | 23 | describe('when a channelName is not defined', function() { 24 | beforeEach(function() { 25 | radioObject.initialize(); 26 | }); 27 | 28 | it('should not have a Radio channel', function() { 29 | expect(radioObject.getChannel()).to.be.undefined; 30 | }); 31 | 32 | it('should not bind radioEvents', function() { 33 | expect(radioObject.bindEvents).to.not.have.been.called; 34 | }); 35 | 36 | it('should not bind radioRequests', function() { 37 | expect(radioObject.bindRequests).to.not.have.been.called; 38 | }); 39 | }); 40 | 41 | describe('when a channelName is defined', function() { 42 | describe('on the object', function() { 43 | it('should have the named Radio channel', function() { 44 | radioObject.channelName = 'foo'; 45 | radioObject.initialize(); 46 | 47 | expect(radioObject.getChannel()).to.eql(channelFoo); 48 | }); 49 | }); 50 | 51 | describe('as a function', function() { 52 | it('should have the named Radio channel', function() { 53 | radioObject.channelName = this.sinon.stub().returns('foo'); 54 | radioObject.initialize(); 55 | 56 | expect(radioObject.getChannel()).to.eql(channelFoo); 57 | }); 58 | }); 59 | }); 60 | 61 | describe('when a radioEvents is defined', function() { 62 | beforeEach(function() { 63 | radioObject.channelName = 'foo'; 64 | }); 65 | 66 | describe('on the object', function() { 67 | it('should bind events to the channel', function() { 68 | radioObject.radioEvents = {'bar': 'onBar'}; 69 | radioObject.initialize(); 70 | 71 | expect(radioObject.bindEvents).to.have.been.calledOnce 72 | .and.to.have.been.calledWith(channelFoo, {'bar': 'onBar'}); 73 | }); 74 | }); 75 | 76 | describe('as a function', function() { 77 | it('should bind events to the channel', function() { 78 | radioObject.radioEvents = this.sinon.stub().returns({'bar': 'onBar'}); 79 | radioObject.initialize(); 80 | 81 | expect(radioObject.bindEvents).to.have.been.calledOnce 82 | .and.to.have.been.calledWith(channelFoo, {'bar': 'onBar'}); 83 | }); 84 | }); 85 | }); 86 | 87 | describe('when a radioRequests is defined', function() { 88 | beforeEach(function() { 89 | radioObject.channelName = 'foo'; 90 | }); 91 | 92 | describe('on the object', function() { 93 | it('should bind requests to the channel', function() { 94 | radioObject.radioRequests = {'baz': 'getBaz'}; 95 | radioObject.initialize(); 96 | 97 | expect(radioObject.bindRequests).to.have.been.calledOnce 98 | .and.to.have.been.calledWith(channelFoo, {'baz': 'getBaz'}); 99 | }); 100 | }); 101 | 102 | describe('as a function', function() { 103 | it('should bind requests to the channel', function() { 104 | radioObject.radioRequests = this.sinon.stub().returns({'baz': 'getBaz'}); 105 | radioObject.initialize(); 106 | 107 | expect(radioObject.bindRequests).to.have.been.calledOnce 108 | .and.to.have.been.calledWith(channelFoo, {'baz': 'getBaz'}); 109 | }); 110 | }); 111 | }); 112 | 113 | describe('when an Object is destroyed', function() { 114 | let fooChannel; 115 | 116 | beforeEach(function() { 117 | radioObject.channelName = 'foo' 118 | radioObject.initialize(); 119 | 120 | fooChannel = radioObject.getChannel(); 121 | 122 | this.sinon.spy(fooChannel, 'stopReplying'); 123 | 124 | radioObject.trigger('destroy'); 125 | }); 126 | 127 | it('should stopReplying to the object', function() { 128 | expect(fooChannel.stopReplying).to.have.been.calledOnce 129 | .and.to.have.been.calledWith(null, null, radioObject); 130 | }); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /test/unit/mixins/ui.spec.js: -------------------------------------------------------------------------------- 1 | import View from '../../../src/view'; 2 | 3 | describe('normalizeUIKeys', function() { 4 | 'use strict'; 5 | 6 | describe('When creating a generic View class without a ui hash, and creating two generic view sublcasses with a ui hash', function() { 7 | let GenericView; 8 | let genericViewSubclass1Instance; 9 | let genericViewSubclass2Instance; 10 | 11 | beforeEach(function() { 12 | GenericView = View.extend({ 13 | events: {'change @ui.someUi': 'onSomeUiChange'}, 14 | onSomeUiChange: sinon.stub() 15 | }); 16 | const GenericViewSubclass1 = GenericView.extend({ 17 | template: _.template('
'), 18 | ui: {someUi: '.subclass-1-ui'} 19 | }); 20 | const GenericViewSubclass2 = GenericView.extend({ 21 | template: _.template('
'), 22 | ui: {someUi: '.subclass-2-ui'} 23 | }); 24 | genericViewSubclass1Instance = new GenericViewSubclass1(); 25 | genericViewSubclass2Instance = new GenericViewSubclass2(); 26 | genericViewSubclass1Instance.render(); 27 | genericViewSubclass2Instance.render(); 28 | }); 29 | 30 | describe('the 1st generic view subclass instance', function() { 31 | it('should have its registered event handler called when the ui DOM event is triggered', function() { 32 | genericViewSubclass1Instance.ui.someUi.trigger('change'); 33 | expect(genericViewSubclass1Instance.onSomeUiChange).to.be.calledOnce; 34 | }); 35 | }); 36 | 37 | describe('the 2nd generic view subclass instance', function() { 38 | it('should have its registered event handler called when the ui DOM event is triggered', function() { 39 | genericViewSubclass2Instance.ui.someUi.trigger('change'); 40 | expect(genericViewSubclass2Instance.onSomeUiChange).to.be.calledOnce; 41 | }); 42 | }); 43 | 44 | it('the generic view class should have its prototype events hash untouched and in its original form', function() { 45 | expect(GenericView.prototype.events).to.eql({'change @ui.someUi': 'onSomeUiChange'}); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/unit/object.spec.js: -------------------------------------------------------------------------------- 1 | import MnObject from '../../src/object'; 2 | 3 | describe('marionette object', function() { 4 | 5 | describe('when creating an object', function() { 6 | let object; 7 | let options; 8 | 9 | beforeEach(function() { 10 | const Obj = MnObject.extend({ 11 | initialize(opts) { 12 | this.bindEvents(opts.model, this.modelEvents); 13 | }, 14 | 15 | modelEvents: { 16 | 'bar': 'onBar' 17 | }, 18 | 19 | onBar: this.sinon.stub() 20 | }); 21 | 22 | this.sinon.spy(Obj.prototype, '_initRadio'); 23 | 24 | const model = new Backbone.Model(); 25 | 26 | options = { 27 | model, 28 | channelName: 'foo', 29 | radioEvents: {}, 30 | radioRequests: {} 31 | }; 32 | 33 | object = new Obj(options); 34 | }); 35 | 36 | it('should merge the class options to the object', function() { 37 | expect(object.channelName).to.equal(options.channelName); 38 | expect(object.radioEvents).to.equal(options.radioEvents); 39 | expect(object.radioRequests).to.equal(options.radioRequests); 40 | }); 41 | 42 | it('should maintain a reference to the options', function() { 43 | expect(object.options).to.deep.equal(options); 44 | }); 45 | 46 | it('should have a cidPrefix', function() { 47 | expect(object.cidPrefix).to.equal('mno'); 48 | }); 49 | 50 | it('should have a cid', function() { 51 | expect(object.cid).to.contain('mno'); 52 | }); 53 | 54 | it('should init the RadioMixin', function() { 55 | expect(object._initRadio).to.have.been.called; 56 | }); 57 | 58 | it('should support triggering events on itself', function() { 59 | const fooHandler = this.sinon.spy(); 60 | object.on('foo', fooHandler); 61 | 62 | object.trigger('foo', options); 63 | 64 | expect(fooHandler).to.have.been.calledOnce.and.calledWith(options); 65 | }); 66 | 67 | it('should support binding to evented objects', function() { 68 | options.model.trigger('bar', options); 69 | 70 | expect(object.onBar).to.have.been.calledOnce.and.calledWith(options); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/unit/on-dom-refresh.spec.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import Backbone from 'backbone'; 3 | import Events from '../../src/mixins/events'; 4 | import View from '../../src/view'; 5 | import Region from '../../src/region'; 6 | 7 | describe('onDomRefresh', function() { 8 | 'use strict'; 9 | 10 | let attachedRegion; 11 | let detachedRegion; 12 | let BbView; 13 | let MnView; 14 | 15 | beforeEach(function() { 16 | this.setFixtures($('
')); 17 | attachedRegion = new Region({el: '#region'}); 18 | detachedRegion = new Region({el: $('
')}); 19 | BbView = Backbone.View.extend({ 20 | onDomRefresh: this.sinon.stub() 21 | }); 22 | _.extend(BbView.prototype, Events); 23 | MnView = View.extend({ 24 | template: _.noop, 25 | onDomRefresh: this.sinon.stub() 26 | }); 27 | }); 28 | 29 | describe('when a Backbone view is shown detached from the DOM', function() { 30 | let bbView; 31 | 32 | beforeEach(function() { 33 | bbView = new BbView(); 34 | detachedRegion.show(bbView); 35 | }); 36 | 37 | it('should never trigger onDomRefresh', function() { 38 | expect(bbView.onDomRefresh).not.to.have.been.calledOnce; 39 | }); 40 | }); 41 | 42 | describe('when a Marionette view is shown detached from the DOM', function() { 43 | let mnView; 44 | 45 | beforeEach(function() { 46 | mnView = new MnView(); 47 | detachedRegion.show(mnView); 48 | }); 49 | 50 | it('should never trigger onDomRefresh', function() { 51 | expect(mnView.onDomRefresh).not.to.have.been.calledOnce; 52 | }); 53 | }); 54 | 55 | describe('when a Backbone view is shown attached to the DOM', function() { 56 | let bbView; 57 | 58 | beforeEach(function() { 59 | bbView = new BbView(); 60 | attachedRegion.show(bbView); 61 | }); 62 | 63 | it('should trigger onDomRefresh on the view', function() { 64 | expect(bbView.onDomRefresh).to.have.been.calledOnce; 65 | }); 66 | }); 67 | 68 | describe('when a Marionette view is shown attached to the DOM', function() { 69 | let mnView; 70 | 71 | beforeEach(function() { 72 | mnView = new MnView(); 73 | attachedRegion.show(mnView); 74 | }); 75 | 76 | it('should trigger onDomRefresh on the view', function() { 77 | expect(mnView.onDomRefresh).to.have.been.calledOnce; 78 | }); 79 | }); 80 | 81 | }); 82 | -------------------------------------------------------------------------------- /test/unit/on-dom-remove.spec.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import Backbone from 'backbone'; 3 | import Events from '../../src/mixins/events'; 4 | import View from '../../src/view'; 5 | import Region from '../../src/region'; 6 | 7 | describe('onDomRemove', function() { 8 | 'use strict'; 9 | 10 | let attachedRegion; 11 | let detachedRegion; 12 | let BbView; 13 | let MnView; 14 | 15 | beforeEach(function() { 16 | this.setFixtures($('
')); 17 | attachedRegion = new Region({el: '#region'}); 18 | detachedRegion = new Region({el: $('
')}); 19 | BbView = Backbone.View.extend({ 20 | onDomRemove: this.sinon.stub() 21 | }); 22 | _.extend(BbView.prototype, Events); 23 | MnView = View.extend({ 24 | template: _.noop, 25 | onDomRemove: this.sinon.stub() 26 | }); 27 | }); 28 | 29 | describe('when a Backbone view is shown detached from the DOM', function() { 30 | let bbView; 31 | 32 | beforeEach(function() { 33 | bbView = new BbView(); 34 | detachedRegion.show(bbView); 35 | detachedRegion.empty(); 36 | }); 37 | 38 | it('should never trigger onDomRemove', function() { 39 | expect(bbView.onDomRemove).not.to.have.been.called; 40 | }); 41 | }); 42 | 43 | describe('when a Marionette view is shown detached from the DOM', function() { 44 | let mnView; 45 | 46 | beforeEach(function() { 47 | mnView = new MnView(); 48 | detachedRegion.show(mnView); 49 | mnView.render(); 50 | detachedRegion.empty(); 51 | }); 52 | 53 | it('should never trigger onDomRemove', function() { 54 | expect(mnView.onDomRemove).not.to.have.been.called; 55 | }); 56 | }); 57 | 58 | describe('when a Backbone view is shown attached to the DOM', function() { 59 | let bbView; 60 | 61 | beforeEach(function() { 62 | bbView = new BbView(); 63 | attachedRegion.show(bbView); 64 | }); 65 | 66 | describe('when the region is emptied', function() { 67 | it('should trigger onDomRemove on the view', function() { 68 | attachedRegion.empty(); 69 | expect(bbView.onDomRemove).to.have.been.calledOnce.and.calledWith(bbView); 70 | }); 71 | }); 72 | }); 73 | 74 | describe('when a Marionette view is shown attached to the DOM', function() { 75 | let mnView; 76 | 77 | beforeEach(function() { 78 | mnView = new MnView(); 79 | attachedRegion.show(mnView); 80 | }); 81 | 82 | describe('when the region is emptied', function() { 83 | it('should trigger onDomRemove on the view', function() { 84 | attachedRegion.empty(); 85 | expect(mnView.onDomRemove).to.have.been.calledOnce.and.calledWith(mnView); 86 | }); 87 | }); 88 | 89 | describe('when the view is re-rendered', function() { 90 | it('should trigger onDomRemove on the view', function() { 91 | mnView.render(); 92 | expect(mnView.onDomRemove).to.have.been.calledOnce.and.calledWith(mnView); 93 | }); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /test/unit/utils/deprecate.spec.js: -------------------------------------------------------------------------------- 1 | import deprecate from '../../../src/utils/deprecate'; 2 | 3 | import {setEnabled} from '../../../src/config/features'; 4 | 5 | describe('deprecate', function() { 6 | beforeEach(function() { 7 | setEnabled('DEV_MODE', true); 8 | this.sinon.spy(deprecate, '_warn'); 9 | this.sinon.stub(deprecate._console, 'warn'); 10 | this.sinon.stub(deprecate._console, 'log'); 11 | 12 | deprecate._cache = {}; 13 | }); 14 | 15 | afterEach(function() { 16 | setEnabled('DEV_MODE', false); 17 | }); 18 | 19 | describe('#_warn', function() { 20 | it('should `console.warn` the message', function() { 21 | deprecate._warn('foo'); 22 | expect(deprecate._console.warn) 23 | .to.have.been.calledOnce 24 | .and.calledOn(deprecate._console) 25 | .and.calledWith('foo'); 26 | }); 27 | 28 | describe('when `console.warn` does not exist', function() { 29 | beforeEach(function() { 30 | deprecate._console.warn = null; 31 | }); 32 | 33 | it('should `console.log` the message', function() { 34 | deprecate._warn('foo'); 35 | expect(deprecate._console.log) 36 | .to.have.been.calledOnce 37 | .and.calledOn(deprecate._console) 38 | .and.calledWith('foo'); 39 | }); 40 | 41 | describe('when `console.log` does not exist', function() { 42 | it('should call `_.noop`', function() { 43 | deprecate._console.log = null; 44 | this.sinon.spy(_, 'noop'); 45 | deprecate._warn('foo'); 46 | 47 | expect(_.noop).to.have.been.calledOnce; 48 | }); 49 | }); 50 | }); 51 | }); 52 | 53 | describe('when calling with a message', function() { 54 | beforeEach(function() { 55 | deprecate('foo'); 56 | }); 57 | 58 | it('should `console.warn` the message', function() { 59 | expect(deprecate._warn) 60 | .to.have.been.calledOnce 61 | .and.calledWith('Deprecation warning: foo'); 62 | }); 63 | }); 64 | 65 | describe('when calling with an object', function() { 66 | beforeEach(function() { 67 | deprecate({ 68 | prev: 'foo', 69 | next: 'bar' 70 | }); 71 | }); 72 | 73 | it('should `console.warn` the message', function() { 74 | expect(deprecate._warn) 75 | .to.have.been.calledOnce 76 | .and.calledWith('Deprecation warning: foo is going to be removed in the future. Please use bar instead.'); 77 | }); 78 | }); 79 | 80 | describe('when calling with an object with a url', function() { 81 | beforeEach(function() { 82 | deprecate({ 83 | prev: 'foo', 84 | next: 'bar', 85 | url: 'baz' 86 | }); 87 | }); 88 | 89 | it('should `console.warn` the message', function() { 90 | expect(deprecate._warn) 91 | .to.have.been.calledOnce 92 | .and.calledWith('Deprecation warning: foo is going to be removed in the future. Please use bar instead. See: baz'); 93 | }); 94 | }); 95 | 96 | describe('when calling with a message and a falsy test', function() { 97 | beforeEach(function() { 98 | deprecate('bar', false); 99 | }); 100 | 101 | it('should `console.warn` the message', function() { 102 | expect(deprecate._warn) 103 | .to.have.been.calledOnce 104 | .and.calledWith('Deprecation warning: bar'); 105 | }); 106 | }); 107 | 108 | describe('when calling with a message and a truthy test', function() { 109 | beforeEach(function() { 110 | deprecate('Foo', true); 111 | }); 112 | 113 | it('should not `console.warn` the message', function() { 114 | expect(deprecate._warn).not.to.have.been.called; 115 | }); 116 | }); 117 | 118 | describe('when calling with the same message twice', function() { 119 | beforeEach(function() { 120 | deprecate('baz'); 121 | deprecate('baz'); 122 | }); 123 | 124 | it('should `console.warn` the message', function() { 125 | expect(deprecate._warn) 126 | .to.have.been.calledOnce 127 | .and.calledWith('Deprecation warning: baz'); 128 | }); 129 | }); 130 | 131 | describe('when calling in production mode', function() { 132 | beforeEach(function() { 133 | setEnabled('DEV_MODE', false); 134 | deprecate('baz'); 135 | }); 136 | 137 | it('should `console.warn` the message', function() { 138 | expect(deprecate._warn).to.not.have.been.called; 139 | }); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /test/unit/utils/error.spec.js: -------------------------------------------------------------------------------- 1 | import { VERSION } from '../../../src/backbone.marionette'; 2 | import MarionetteError from '../../../src/utils/error'; 3 | 4 | describe('MarionetteError', function() { 5 | it('should be subclass of native Error', function() { 6 | expect(new MarionetteError({ message: 'foo' })).to.be.instanceOf(Error); 7 | }); 8 | 9 | describe('when passed options', function() { 10 | let error; 11 | 12 | beforeEach(function() { 13 | error = new MarionetteError({ 14 | name: 'Foo', 15 | message: 'Bar' 16 | }); 17 | }); 18 | 19 | it('should contain the correct properties', function() { 20 | expect(error).to.contain({ 21 | name: 'Foo', 22 | message: 'Bar' 23 | }); 24 | }); 25 | 26 | it('should output the correct string', function() { 27 | expect(error.toString()).to.equal('Foo: Bar See: http://marionettejs.com/docs/v' + VERSION + '/'); 28 | }); 29 | }); 30 | 31 | describe('when passed options with a url', function() { 32 | let error; 33 | 34 | beforeEach(function() { 35 | error = new MarionetteError({ 36 | name: 'Foo', 37 | message: 'Bar', 38 | url: 'Baz' 39 | }); 40 | }); 41 | 42 | it('should contain the correct properties', function() { 43 | expect(error).to.contain({ 44 | name: 'Foo', 45 | message: 'Bar', 46 | url: 'http://marionettejs.com/docs/v' + VERSION + '/Baz' 47 | }); 48 | }); 49 | 50 | it('should output the correct string', function() { 51 | expect(error.toString()).to.equal('Foo: Bar See: http://marionettejs.com/docs/v' + VERSION + '/Baz'); 52 | }); 53 | }); 54 | 55 | describe('when passed valid error properties', function() { 56 | let props; 57 | let error; 58 | 59 | beforeEach(function() { 60 | props = { 61 | description: 'myDescription', 62 | fileName: 'myFileName', 63 | lineNumber: 'myLineNumber', 64 | name: 'myName', 65 | message: 'myMessage', 66 | number: 'myNumber' 67 | }; 68 | error = new MarionetteError(props); 69 | }); 70 | 71 | it('should contain all the valid error properties', function() { 72 | expect(error).to.contain(props); 73 | }); 74 | }); 75 | 76 | describe('when passed invalid error properties', function() { 77 | let props; 78 | let error; 79 | 80 | beforeEach(function() { 81 | props = { 82 | foo: 'myFoo', 83 | bar: 'myBar', 84 | baz: 'myBaz' 85 | }; 86 | error = new MarionetteError(props); 87 | }); 88 | 89 | it('should not contain invalid properties', function() { 90 | expect(error).not.to.contain(props); 91 | }); 92 | }); 93 | 94 | describe('when Error.captureStackTrace is unavailable', function() { 95 | let captureStackTrace = Error.captureStackTrace; 96 | 97 | beforeEach(function() { 98 | this.sinon.spy(MarionetteError.prototype, 'captureStackTrace'); 99 | Error.captureStackTrace = undefined; 100 | }); 101 | 102 | afterEach(function() { 103 | Error.captureStackTrace = captureStackTrace; 104 | }); 105 | 106 | it('should not captureStackTrace', function() { 107 | new MarionetteError({ message: 'foo' }); 108 | expect(MarionetteError.prototype.captureStackTrace).to.not.be.called; 109 | }); 110 | }) 111 | }); 112 | -------------------------------------------------------------------------------- /test/unit/utils/get-namespaced-event-name.spec.js: -------------------------------------------------------------------------------- 1 | import getNamespacedEventName from '../../../src/utils/get-namespaced-event-name'; 2 | 3 | describe('getNamespacedEventName', function() { 4 | it('should postfix a namespace to the event', function() { 5 | expect(getNamespacedEventName('click a.name', 'test')).to.equal('click.test a.name'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/unit/utils/proxy.spec.js: -------------------------------------------------------------------------------- 1 | import proxy from '../../../src/utils/proxy'; 2 | 3 | describe('proxy', function() { 4 | let method; 5 | 6 | beforeEach(function() { 7 | method = this.sinon.stub(); 8 | }); 9 | 10 | it('should return a function', function() { 11 | expect(proxy(method)).to.be.a('function'); 12 | }); 13 | 14 | describe('when calling the returned function', function() { 15 | it('should call the method on context with all arguments', function() { 16 | const context = {}; 17 | const proxiedMethod = proxy(method); 18 | 19 | proxiedMethod(context, 1, 2, 3); 20 | 21 | expect(method).to.be.calledOn(context).and.calledWith(1,2,3); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/unit/view.renderer.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import Backbone from 'backbone'; 3 | import View from '../../src/view'; 4 | 5 | describe('View.setRenderer', function() { 6 | let ViewClass; 7 | let ViewSubClass; 8 | let model; 9 | 10 | const template = 'fooTemplate'; 11 | const data = { foo: 'bar' }; 12 | 13 | beforeEach(function() { 14 | ViewClass = View.extend(); 15 | ViewSubClass = ViewClass.extend(); 16 | model = new Backbone.Model(data); 17 | }); 18 | 19 | describe('when setting a renderer on a View class', function() { 20 | it('should return the View class', function() { 21 | expect(ViewClass.setRenderer()).to.be.eq(ViewClass); 22 | }); 23 | }); 24 | 25 | describe('when changing a renderer on a View class', function() { 26 | let rendererStub; 27 | 28 | beforeEach(function() { 29 | rendererStub = this.sinon.stub(); 30 | 31 | ViewClass.setRenderer(rendererStub); 32 | 33 | const view = new ViewClass({ template, model }); 34 | 35 | view.render(); 36 | }); 37 | 38 | it('should use the custom renderer to render', function() { 39 | expect(rendererStub).to.have.been.calledOnce.and.calledWith(template, data); 40 | }); 41 | 42 | it('should not affect the renderer of the extended View', function() { 43 | rendererStub.reset(); 44 | 45 | const baseView = new View({ template: _.template('bar'), model }); 46 | baseView.render(); 47 | 48 | expect(rendererStub).to.not.have.been.called; 49 | }); 50 | 51 | describe('when inheriting from the view class', function() { 52 | it('should use the custom renderer', function() { 53 | rendererStub.reset(); 54 | 55 | const subView = new ViewSubClass({ template, model }); 56 | subView.render(); 57 | 58 | expect(rendererStub).to.have.been.calledOnce.and.calledWith(template, data); 59 | }); 60 | }); 61 | 62 | describe('when changing a renderer on an inherited class', function() { 63 | let subRendererStub; 64 | 65 | beforeEach(function() { 66 | subRendererStub = this.sinon.stub(); 67 | 68 | ViewSubClass.setRenderer(subRendererStub); 69 | 70 | rendererStub.reset(); 71 | 72 | const view = new ViewSubClass({ template, model }); 73 | 74 | view.render(); 75 | }); 76 | 77 | it('should use the custom renderer to render', function() { 78 | expect(subRendererStub).to.have.been.calledOnce.and.calledWith(template, data); 79 | }); 80 | 81 | it('should not use the custom renderer of the inherited class', function() { 82 | expect(rendererStub).to.not.have.been.called; 83 | }); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/unit/view.ui-bindings.spec.js: -------------------------------------------------------------------------------- 1 | describe('view ui elements', function() { 2 | 'use strict'; 3 | 4 | beforeEach(function() { 5 | this.templateFn = _.template('
'); 6 | this.uiHash = {foo: '#foo', bar: '#bar'}; 7 | this.model = this.model = new Backbone.Model({name: 'foo'}); 8 | this.View = Marionette.View.extend({ 9 | template: this.templateFn, 10 | ui: this.uiHash 11 | }); 12 | }); 13 | 14 | describe('when normalizing a ui string', function() { 15 | beforeEach(function() { 16 | this.view = new this.View({model: this.model}); 17 | this.view.render(); 18 | }); 19 | 20 | it('should return the string unmodified if it does not begin with @ui.', function() { 21 | expect(this.view.normalizeUIString('baz')).to.equal('baz'); 22 | }) 23 | 24 | it('should translate it if it can be found', function() { 25 | expect(this.view.normalizeUIString('@ui.foo')).to.equal('#foo'); 26 | }); 27 | 28 | it('should return undefined if it begins with @ui. but can not be found', function() { 29 | expect(this.view.normalizeUIString('@ui.baz')).to.equal('undefined'); 30 | }); 31 | }); 32 | 33 | describe('when accessing a ui element from the hash', function() { 34 | beforeEach(function() { 35 | this.view = new this.View({model: this.model}); 36 | this.view.render(); 37 | }); 38 | 39 | it('should return its jQuery selector if it can be found', function() { 40 | expect(this.view.ui.foo).to.be.instanceOf(jQuery).and.to.have.lengthOf(1); 41 | }); 42 | 43 | it('should return an empty jQuery object if it cannot be found', function() { 44 | expect(this.view.ui.bar).to.be.instanceOf(jQuery).and.to.have.lengthOf(0); 45 | }); 46 | 47 | it('should return its jQuery selector through getUI', function() { 48 | expect(this.view.getUI('foo')).to.be.instanceOf(jQuery).and.to.have.lengthOf(1); 49 | expect(this.view.getUI('bar')).to.be.instanceOf(jQuery).and.to.have.lengthOf(0); 50 | }); 51 | }); 52 | 53 | describe('when re-rendering a view with a UI element configuration', function() { 54 | beforeEach(function() { 55 | this.view = new this.View({model: this.model}); 56 | this.view.render(); 57 | this.view.model.set('name', 'bar'); 58 | this.view.render(); 59 | }); 60 | 61 | it('should return an up-to-date selector on subsequent renders', function() { 62 | expect(this.view.ui.foo).to.be.instanceOf(jQuery).and.to.have.lengthOf(0); 63 | expect(this.view.ui.bar).to.be.instanceOf(jQuery).and.to.have.lengthOf(1); 64 | }); 65 | 66 | it('should return an up-to-date selector through getUI', function() { 67 | expect(this.view.getUI('foo')).to.be.instanceOf(jQuery).and.to.have.lengthOf(0); 68 | expect(this.view.getUI('bar')).to.be.instanceOf(jQuery).and.to.have.lengthOf(1); 69 | }); 70 | }); 71 | 72 | describe('when the ui element is a function that returns a hash', function() { 73 | beforeEach(function() { 74 | this.View = this.View.extend({ 75 | ui: this.sinon.stub().returns(this.uiHash) 76 | }); 77 | 78 | this.view = new this.View({model: this.model}); 79 | this.view.render(); 80 | }); 81 | 82 | it('should return its jQuery selector if it can be found', function() { 83 | expect(this.view.ui.foo).to.be.instanceOf(jQuery).and.to.have.lengthOf(1); 84 | }); 85 | 86 | it('should return an empty jQuery object if it cannot be found', function() { 87 | expect(this.view.ui.bar).to.be.instanceOf(jQuery).and.to.have.lengthOf(0); 88 | }); 89 | 90 | it('should return an up-to-date selector on subsequent renders', function() { 91 | expect(this.view.ui.foo).to.be.instanceOf(jQuery).and.to.have.lengthOf(1); 92 | expect(this.view.ui.bar).to.be.instanceOf(jQuery).to.have.lengthOf(0); 93 | 94 | this.view.model.set('name', 'bar'); 95 | this.view.render(); 96 | 97 | expect(this.view.ui.foo).to.have.lengthOf(0); 98 | expect(this.view.ui.bar).to.have.lengthOf(1); 99 | }); 100 | 101 | it('should return its jQuery selector through getUI', function() { 102 | expect(this.view.getUI('foo')).to.be.instanceOf(jQuery).and.to.have.lengthOf(1); 103 | }); 104 | }); 105 | 106 | describe('when destroying a view that has not been rendered', function() { 107 | beforeEach(function() { 108 | this.viewOne = new this.View({model: this.model}); 109 | this.viewTwo = new this.View({model: this.model}); 110 | }); 111 | 112 | it('should not affect future ui bindings', function() { 113 | expect(this.viewTwo.ui).to.deep.equal(this.uiHash); 114 | }); 115 | }); 116 | 117 | describe('when destroying a view', function() { 118 | beforeEach(function() { 119 | this.view = new this.View({model: this.model}); 120 | this.view.render(); 121 | this.view.destroy(); 122 | }); 123 | 124 | it('should unbind UI elements and reset them to the selector', function() { 125 | expect(this.view.ui).to.deep.equal(this.uiHash); 126 | }); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /test/unit/view.ui-event-and-triggers.spec.js: -------------------------------------------------------------------------------- 1 | describe('view ui event trigger configuration', function() { 2 | 'use strict'; 3 | 4 | describe('@ui syntax within events and triggers', function() { 5 | beforeEach(function() { 6 | this.fooHandlerStub = this.sinon.stub(); 7 | this.barHandlerStub = this.sinon.stub(); 8 | this.notBarHandlerStub = this.sinon.stub(); 9 | this.fooBarBazHandlerStub = this.sinon.stub(); 10 | 11 | this.templateFn = _.template('
'); 12 | 13 | this.uiHash = { 14 | foo: '#foo', 15 | bar: '#bar', 16 | 'some-baz': '#baz' 17 | }; 18 | 19 | this.triggersHash = { 20 | 'click @ui.foo': 'fooHandler', 21 | 'click @ui.some-baz': 'bazHandler' 22 | }; 23 | 24 | this.eventsHash = { 25 | 'click @ui.bar': this.barHandlerStub, 26 | 'click div:not(@ui.bar)': this.notBarHandlerStub, 27 | 'click @ui.foo, @ui.bar, @ui.some-baz': this.fooBarBazHandlerStub 28 | }; 29 | }); 30 | 31 | describe('as objects', function() { 32 | beforeEach(function() { 33 | this.View = Marionette.View.extend({ 34 | template: this.templateFn, 35 | ui: this.uiHash, 36 | triggers: this.triggersHash, 37 | events: this.eventsHash 38 | }); 39 | this.view = new this.View(); 40 | this.view.render(); 41 | 42 | this.view.on('fooHandler', this.fooHandlerStub); 43 | }); 44 | 45 | it('should correctly trigger an event', function() { 46 | this.view.ui.foo.trigger('click'); 47 | expect(this.fooHandlerStub).to.have.been.calledOnce; 48 | expect(this.fooBarBazHandlerStub).to.have.been.calledOnce; 49 | }); 50 | 51 | it('should correctly trigger a complex event', function() { 52 | this.view.ui.bar.trigger('click'); 53 | expect(this.barHandlerStub).to.have.been.calledOnce; 54 | expect(this.fooBarBazHandlerStub).to.have.been.calledOnce; 55 | }); 56 | 57 | it('should correctly call an event', function() { 58 | this.view.ui['some-baz'].trigger('click'); 59 | expect(this.notBarHandlerStub).to.have.been.calledOnce; 60 | expect(this.fooBarBazHandlerStub).to.have.been.calledOnce; 61 | }); 62 | }); 63 | 64 | describe('as functions', function() { 65 | beforeEach(function() { 66 | this.View = Marionette.View.extend({ 67 | template: this.templateFn, 68 | ui: this.sinon.stub().returns(this.uiHash), 69 | triggers: this.sinon.stub().returns(this.triggersHash), 70 | events: this.sinon.stub().returns(this.eventsHash) 71 | }); 72 | this.view = new this.View(); 73 | this.view.render(); 74 | 75 | this.view.on('fooHandler', this.fooHandlerStub); 76 | }); 77 | 78 | it('should initialize events with context of the view', function() { 79 | expect(this.View.prototype.events).to.have.been.calledOn(this.view); 80 | }); 81 | 82 | it('should initialize triggers with context of the view', function() { 83 | expect(this.View.prototype.triggers).to.have.been.calledOn(this.view); 84 | }); 85 | 86 | it('should correctly trigger an event', function() { 87 | this.view.ui.foo.trigger('click'); 88 | expect(this.fooHandlerStub).to.have.been.calledOnce; 89 | expect(this.fooBarBazHandlerStub).to.have.been.calledOnce; 90 | }); 91 | 92 | it('should correctly trigger a complex event', function() { 93 | this.view.ui.bar.trigger('click'); 94 | expect(this.barHandlerStub).to.have.been.calledOnce; 95 | expect(this.fooBarBazHandlerStub).to.have.been.calledOnce; 96 | }); 97 | 98 | it('should correctly call an event', function() { 99 | this.view.ui['some-baz'].trigger('click'); 100 | expect(this.notBarHandlerStub).to.have.been.calledOnce; 101 | expect(this.fooBarBazHandlerStub).to.have.been.calledOnce; 102 | }); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /trigger-deploy-mn-com.js: -------------------------------------------------------------------------------- 1 | var Travis = require('travis-ci'); 2 | var repo = 'marionettejs/marionettejs.com'; 3 | var travis = new Travis({ 4 | version: '2.0.0', 5 | headers: { 6 | 'User-Agent': 'Travis/1.0' 7 | } 8 | }); 9 | 10 | var getLastMainBuildId = function(builds) { 11 | var id; 12 | 13 | for (var i = 0; i < builds.length; i++) { 14 | if (!builds[i].pull_request) { 15 | id = builds[i].id; 16 | break; 17 | } 18 | } 19 | 20 | if (!id) { 21 | throw new Error('Build id was not found'); 22 | } 23 | 24 | return id; 25 | }; 26 | 27 | travis.authenticate({ 28 | github_token: process.env.GH_TOKEN 29 | }, function (err, res) { 30 | if (err) { 31 | return console.error(err); 32 | } 33 | 34 | //get repo builds 35 | travis.repos(repo.split('/')[0], repo.split('/')[1]).builds.get(function (err, res) { 36 | if (err) { 37 | return console.error(err); 38 | } 39 | 40 | //rebuild latest build 41 | travis.requests.post({ 42 | build_id: getLastMainBuildId(res.builds) 43 | }, function (err, res) { 44 | if (err) { 45 | return console.error(err); 46 | } 47 | console.log(res.flash[0].notice); 48 | }); 49 | }); 50 | }); 51 | --------------------------------------------------------------------------------