diff --git a/docs/content/guide/e2e-testing.ngdoc b/docs/content/guide/e2e-testing.ngdoc index 290d80b4..7874c83d 100644 --- a/docs/content/guide/e2e-testing.ngdoc +++ b/docs/content/guide/e2e-testing.ngdoc @@ -3,319 +3,83 @@ @name E2E Testing @description +# E2E Testing +
-**Note:** Angular Scenario Runner is depricated. If you're starting a new Angular project, -consider using [Protractor](https://github.com/angular/protractor). +**Note:** In the past, end to end testing could be done with a deprecated tool called +[Angular Scenario Runner](http://code.angularjs.org/1.2.16/docs/guide/e2e-testing). That tool +is now in maintenance mode.
-# E2E Testing with the Angular Scenario Runner - As applications grow in size and complexity, it becomes unrealistic to rely on manual testing to -verify the correctness of new features, catch bugs and notice regressions. +verify the correctness of new features, catch bugs and notice regressions. End to end tests +are the first line of defense for catching bugs, but sometimes issues come up with integration +between components which can't be captured in a unit test. End to end tests are made to find +these problems. -To solve this problem, we have built an Angular Scenario Runner which simulates user interactions -that will help you verify the health of your Angular application. +We have built [Protractor](https://github.com/angular/protractor), an end +to end test runner which simulates user interactions that will help you verify the health of your +Angular application. -## Overview +## Using Protractor -You write scenario tests in JavaScript. These tests describe how your application should behave -given a certain interaction in a specific state. +Protractor is a [Node.js](http://nodejs.org) program, and runs end to end tests that are also +written in JavaScript and run with node. Protractor uses [WebDriver](https://code.google.com/p/selenium/wiki/GettingStarted) +to control browsers and simulate user actions. -A scenario is comprised of one or more `it` blocks that describe the requirements of your -application. `it` blocks are made of **commands** and **expectations**. Commands tell the Runner -to do something with the application such as navigate to a page or click on a button. Expectations -tell the Runner to assert something about the application's state, such as the value of a field or -the current URL. +For more information on Protractor, view [getting started](https://github.com/angular/protractor/blob/master/docs/getting-started.md) +or the [api docs](https://github.com/angular/protractor/blob/master/docs/api.md). + +Protractor uses [Jasmine](http://jasmine.github.io/1.3/introduction.html) for its test syntax. +As in unit testing, a test file is comprised of one or +more `it` blocks that describe the requirements of your application. `it` blocks are made of +**commands** and **expectations**. Commands tell Protractor to do something with the application +such as navigate to a page or click on a button. Expectations tell Protractor to assert something +about the application's state, such as the value of a field or the current URL. If any expectation within an `it` block fails, the runner marks the `it` as "failed" and continues on to the next block. -Scenarios may also have `beforeEach` and `afterEach` blocks, which will be run before or after +Test files may also have `beforeEach` and `afterEach` blocks, which will be run before or after each `it` block regardless of whether the block passes or fails. -In addition to the above elements, scenarios may also contain helper functions to avoid duplicating +In addition to the above elements, tests may also contain helper functions to avoid duplicating code in the `it` blocks. -Here is an example of a simple scenario: +Here is an example of a simple test: ```js -describe('Buzz Client', function() { -it('should filter results', function() { - input('user').enter('jacksparrow'); - element(':button').click(); - expect(repeater('ul li').count()).toEqual(10); - input('filterText').enter('Bees'); - expect(repeater('ul li').count()).toEqual(1); -}); +describe('TODO list', function() { + it('should filter results', function() { + + // Find the element with ng-model="user" and type "jacksparrow" into it + element(by.model('user')).sendKeys('jacksparrow'); + + // Find the first (and only) button on the page and click it + element(by.css(':button')).click(); + + // Verify that there are 10 tasks + expect(element.all(by.repeater('task in tasks')).count()).toEqual(10); + + // Enter 'groceries' into the element with ng-model="filterText" + element(by.model('filterText')).sendKeys('groceries'); + + // Verify that now there is only one item in the task list + expect(element.all(by.repeater('task in tasks')).count()).toEqual(1); + }); }); ``` -Note that -[`input('user')`](https://github.com/angular/angular.js/blob/master/docs/content/guide/dev_guide.e2e-testing.ngdoc#L119) -finds the `` element with `ng-model="user"` not `name="user"`. - -This scenario describes the requirements of a Buzz Client, specifically, that it should be able to -filter the stream of the user. It starts by entering a value in the input field with ng-model="user", clicking -the only button on the page, and then it verifies that there are 10 items listed. It then enters -'Bees' in the input field with ng-model='filterText' and verifies that the list is reduced to a single item. - -The API section below lists the available commands and expectations for the Runner. - -## API -Source: https://github.com/angular/angular.js/blob/master/src/ngScenario/dsl.js - -### `pause()` -Pauses the execution of the tests until you call `resume()` in the console (or click the resume -link in the Runner UI). - -### `sleep(seconds)` -Pauses the execution of the tests for the specified number of `seconds`. - -### `browser().navigateTo(url)` -Loads the `url` into the test frame. - -### `browser().navigateTo(url, fn)` -Loads the URL returned by `fn` into the testing frame. The given `url` is only used for the test -output. Use this when the destination URL is dynamic (that is, the destination is unknown when you -write the test). - -### `browser().reload()` -Refreshes the currently loaded page in the test frame. - -### `browser().window().href()` -Returns the window.location.href of the currently loaded page in the test frame. - -### `browser().window().path()` -Returns the window.location.pathname of the currently loaded page in the test frame. - -### `browser().window().search()` -Returns the window.location.search of the currently loaded page in the test frame. - -### `browser().window().hash()` -Returns the window.location.hash (without `#`) of the currently loaded page in the test frame. - -### `browser().location().url()` -Returns the {@link ng.$location $location.url()} of the currently loaded page in -the test frame. - -### `browser().location().path()` -Returns the {@link ng.$location $location.path()} of the currently loaded page in -the test frame. - -### `browser().location().search()` -Returns the {@link ng.$location $location.search()} of the currently loaded page -in the test frame. - -### `browser().location().hash()` -Returns the {@link ng.$location $location.hash()} of the currently loaded page in -the test frame. - -### `expect(future).{matcher}` -Asserts the value of the given `future` satisfies the `matcher`. All API statements return a -`future` object, which get a `value` assigned after they are executed. Matchers are defined using -`angular.scenario.matcher`, and they use the value of futures to run the expectation. For example: -`expect(browser().location().href()).toEqual('http://www.google.com')`. Available matchers -are presented further down this document. - -### `expect(future).not().{matcher}` -Asserts the value of the given `future` satisfies the negation of the `matcher`. - -### `using(selector, label)` -Scopes the next DSL element selection. - -### `binding(name)` -Returns the value of the first binding matching the given `name`. - -### `input(name).enter(value)` -Enters the given `value` in the text field with the corresponding ng-model `name`. - -### `input(name).check()` -Checks/unchecks the checkbox with the corresponding ng-model `name`. - -### `input(name).select(value)` -Selects the given `value` in the radio button with the corresponding ng-model `name`. - -### `input(name).val()` -Returns the current value of an input field with the corresponding ng-model `name`. - -### `repeater(selector, label).count()` -Returns the number of rows in the repeater matching the given jQuery `selector`. The `label` is -used for test output. - -### `repeater(selector, label).row(index)` -Returns an array with the bindings in the row at the given `index` in the repeater matching the -given jQuery `selector`. The `label` is used for test output. - -### `repeater(selector, label).column(binding)` -Returns an array with the values in the column with the given `binding` in the repeater matching -the given jQuery `selector`. The `label` is used for test output. - -### `select(name).option(value)` -Picks the option with the given `value` on the select with the given ng-model `name`. - -### `select(name).options(value1, value2...)` -Picks the options with the given `values` on the multi select with the given ng-model `name`. - -### `element(selector, label).count()` -Returns the number of elements that match the given jQuery `selector`. The `label` is used for test -output. - -### `element(selector, label).click()` -Clicks on the element matching the given jQuery `selector`. The `label` is used for test output. - -### `element(selector, label).query(fn)` -Executes the function `fn(selectedElements, done)`, where selectedElements are the elements that -match the given jQuery `selector` and `done` is a function that is called at the end of the `fn` -function. The `label` is used for test output. - -### `element(selector, label).{method}()` -Returns the result of calling `method` on the element matching the given jQuery `selector`, where -`method` can be any of the following jQuery methods: `val`, `text`, `html`, `height`, -`innerHeight`, `outerHeight`, `width`, `innerWidth`, `outerWidth`, `position`, `scrollLeft`, -`scrollTop`, `offset`. The `label` is used for test output. - -### `element(selector, label).{method}(value)` -Executes the `method` passing in `value` on the element matching the given jQuery `selector`, where -`method` can be any of the following jQuery methods: `val`, `text`, `html`, `height`, -`innerHeight`, `outerHeight`, `width`, `innerWidth`, `outerWidth`, `position`, `scrollLeft`, -`scrollTop`, `offset`. The `label` is used for test output. - -### `element(selector, label).{method}(key)` -Returns the result of calling `method` passing in `key` on the element matching the given jQuery -`selector`, where `method` can be any of the following jQuery methods: `attr`, `prop`, `css`. The -`label` is used for test output. - -### `element(selector, label).{method}(key, value)` -Executes the `method` passing in `key` and `value` on the element matching the given jQuery -`selector`, where `method` can be any of the following jQuery methods: `attr`, `prop`, `css`. The -`label` is used for test output. - -## Matchers - -Matchers are used in combination with the `expect(...)` function as described above and can -be negated with `not()`. For instance: `expect(element('h1').text()).not().toEqual('Error')`. - -Source: https://github.com/angular/angular.js/blob/master/src/ngScenario/matchers.js - -```js -// value and Object comparison following the rules of angular.equals(). -expect(value).toEqual(value) - -// a simpler value comparison using === -expect(value).toBe(value) - -// checks that the value is defined by checking its type. -expect(value).toBeDefined() - -// the following two matchers are using JavaScript's standard truthiness rules -expect(value).toBeTruthy() -expect(value).toBeFalsy() - -// verify that the value matches the given regular expression. The regular -// expression may be passed in form of a string or a regular expression -// object. -expect(value).toMatch(expectedRegExp) - -// a check for null using === -expect(value).toBeNull() - -// Array.indexOf(...) is used internally to check whether the element is -// contained within the array. -expect(value).toContain(expected) - -// number comparison using < and > -expect(value).toBeLessThan(expected) -expect(value).toBeGreaterThan(expected) -``` +This test describes the requirements of a ToDo list, specifically, that it should be able to +filter the list of items. ## Example -See the [angular-seed](https://github.com/angular/angular-seed) project for more examples. - -### Conditional actions with element(...).query(fn) - -E2E testing with angular scenario is highly asynchronous and hides a lot of complexity by -queueing actions and expectations that can handle futures. From time to time, you might need -conditional assertions or element selection. Even though you should generally try to avoid this -(as it is can be sign for unstable tests), you can add conditional behavior with -`element(...).query(fn)`. The following code listing shows how this function can be used to delete -added entries (where an entry is some domain object) using the application's web interface. - -Imagine the application to be structured into two views: - - 1. *Overview view* which lists all the added entries in a table and - 2. a *detail view* which shows the entries' details and contains a delete button. When clicking the - delete button, the user is redirected back to the *overview page*. - -```js -beforeEach(function () { - var deleteEntry = function () { - browser().navigateTo('/entries'); - - // we need to select the element as it might be the case that there - // are no entries (and therefore no rows). When the selector does not - // result in a match, the test would be marked as a failure. - element('table tbody').query(function (tbody, done) { - // ngScenario gives us a jQuery lite wrapped element. We call the - // `children()` function to retrieve the table body's rows - var children = tbody.children(); - - if (children.length > 0) { - // if there is at least one entry in the table, click on the link to - // the entry's detail view - element('table tbody a').click(); - // and, after a route change, click the delete button - element('.btn-danger').click(); - } - - // if there is more than one entry shown in the table, queue another - // delete action. - if (children.length > 1) { - deleteEntry(); - } - - // remember to call `done()` so that ngScenario can continue - // test execution. - done(); - }); - - }; - - // start deleting entries - deleteEntry(); -}); -``` - -In order to understand what is happening, we should emphasize that ngScenario calls are not -immediately executed, but queued (in ngScenario terms, we would be talking about adding -future actions). If we had only one entry in our table, then the following future actions -would be queued: - -```js -// delete entry 1 -browser().navigateTo('/entries'); -element('table tbody').query(function (tbody, done) { ... }); -element('table tbody a'); -element('.btn-danger').click(); -``` - -For two entries, ngScenario would have to work on the following queue: - -```js -// delete entry 1 -browser().navigateTo('/entries'); -element('table tbody').query(function (tbody, done) { ... }); -element('table tbody a'); -element('.btn-danger').click(); - - // delete entry 2 - // indented to represent "recursion depth" - browser().navigateTo('/entries'); - element('table tbody').query(function (tbody, done) { ... }); - element('table tbody a'); - element('.btn-danger').click(); -``` +See the [angular-seed](https://github.com/angular/angular-seed) project for more examples, or look +at the embedded examples in the Angular documentation (For example, [$http](http://docs.angularjs.org/api/ng/service/$http) +has an end to end test in the example under the `protractor.js` tag). ## Caveats -`ngScenario` does not work with apps that manually bootstrap using `angular.bootstrap`. You must use the `ng-app` directive. +Protractor does not work out-of-the-box with apps that manually bootstrap manually using +`angular.bootstrap`. You must use the `ng-app` directive.