| <a href='http://github.com/angular/angular.js/edit/master/docs/content/guide/e2e-testing.ngdoc' class='improve-docs btn btn-primary'><i class="glyphicon glyphicon-edit"> </i>Improve this doc</a> |
| |
| |
| <div class="alert alert-danger"> |
| <strong>Note:</strong> Angular Scenario Runner is depricated. If you're starting a new Angular project, |
| consider using <a href="https://github.com/angular/protractor">Protractor</a>. |
| </div> |
| |
| <h1 id="e2e-testing-with-the-angular-scenario-runner">E2E Testing with the Angular Scenario Runner</h1> |
| <p>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.</p> |
| <p>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.</p> |
| <h2 id="overview">Overview</h2> |
| <p>You write scenario tests in JavaScript. These tests describe how your application should behave |
| given a certain interaction in a specific state.</p> |
| <p>A scenario is comprised of one or more <code>it</code> blocks that describe the requirements of your |
| application. <code>it</code> blocks are made of <strong>commands</strong> and <strong>expectations</strong>. 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.</p> |
| <p>If any expectation within an <code>it</code> block fails, the runner marks the <code>it</code> as "failed" and continues |
| on to the next block.</p> |
| <p>Scenarios may also have <code>beforeEach</code> and <code>afterEach</code> blocks, which will be run before or after |
| each <code>it</code> block regardless of whether the block passes or fails.</p> |
| <p><img src="img/guide/scenario_runner.png"></p> |
| <p>In addition to the above elements, scenarios may also contain helper functions to avoid duplicating |
| code in the <code>it</code> blocks.</p> |
| <p>Here is an example of a simple scenario:</p> |
| <pre><code class="lang-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); |
| }); |
| });</code></pre> |
| <p>Note that |
| <a href="https://github.com/angular/angular.js/blob/master/docs/content/guide/dev_guide.e2e-testing.ngdoc#L119"><code>input('user')</code></a> |
| finds the <code><input></code> element with <code>ng-model="user"</code> not <code>name="user"</code>.</p> |
| <p>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.</p> |
| <p>The API section below lists the available commands and expectations for the Runner.</p> |
| <h2 id="api">API</h2> |
| <p>Source: <a href="https://github.com/angular/angular.js/blob/master/src/ngScenario/dsl.js">https://github.com/angular/angular.js/blob/master/src/ngScenario/dsl.js</a></p> |
| <h3 id="-pause-"><code>pause()</code></h3> |
| <p>Pauses the execution of the tests until you call <code>resume()</code> in the console (or click the resume |
| link in the Runner UI).</p> |
| <h3 id="-sleep-seconds-"><code>sleep(seconds)</code></h3> |
| <p>Pauses the execution of the tests for the specified number of <code>seconds</code>.</p> |
| <h3 id="-browser-navigateto-url-"><code>browser().navigateTo(url)</code></h3> |
| <p>Loads the <code>url</code> into the test frame.</p> |
| <h3 id="-browser-navigateto-url-fn-"><code>browser().navigateTo(url, fn)</code></h3> |
| <p>Loads the URL returned by <code>fn</code> into the testing frame. The given <code>url</code> 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).</p> |
| <h3 id="-browser-reload-"><code>browser().reload()</code></h3> |
| <p>Refreshes the currently loaded page in the test frame.</p> |
| <h3 id="-browser-window-href-"><code>browser().window().href()</code></h3> |
| <p>Returns the window.location.href of the currently loaded page in the test frame.</p> |
| <h3 id="-browser-window-path-"><code>browser().window().path()</code></h3> |
| <p>Returns the window.location.pathname of the currently loaded page in the test frame.</p> |
| <h3 id="-browser-window-search-"><code>browser().window().search()</code></h3> |
| <p>Returns the window.location.search of the currently loaded page in the test frame.</p> |
| <h3 id="-browser-window-hash-"><code>browser().window().hash()</code></h3> |
| <p>Returns the window.location.hash (without <code>#</code>) of the currently loaded page in the test frame.</p> |
| <h3 id="-browser-location-url-"><code>browser().location().url()</code></h3> |
| <p>Returns the <a href="api/ng/service/$location">$location.url()</a> of the currently loaded page in |
| the test frame.</p> |
| <h3 id="-browser-location-path-"><code>browser().location().path()</code></h3> |
| <p>Returns the <a href="api/ng/service/$location">$location.path()</a> of the currently loaded page in |
| the test frame.</p> |
| <h3 id="-browser-location-search-"><code>browser().location().search()</code></h3> |
| <p>Returns the <a href="api/ng/service/$location">$location.search()</a> of the currently loaded page |
| in the test frame.</p> |
| <h3 id="-browser-location-hash-"><code>browser().location().hash()</code></h3> |
| <p>Returns the <a href="api/ng/service/$location">$location.hash()</a> of the currently loaded page in |
| the test frame.</p> |
| <h3 id="-expect-future-matcher-"><code>expect(future).{matcher}</code></h3> |
| <p>Asserts the value of the given <code>future</code> satisfies the <code>matcher</code>. All API statements return a |
| <code>future</code> object, which get a <code>value</code> assigned after they are executed. Matchers are defined using |
| <code>angular.scenario.matcher</code>, and they use the value of futures to run the expectation. For example: |
| <code>expect(browser().location().href()).toEqual('http://www.google.com')</code>. Available matchers |
| are presented further down this document.</p> |
| <h3 id="-expect-future-not-matcher-"><code>expect(future).not().{matcher}</code></h3> |
| <p>Asserts the value of the given <code>future</code> satisfies the negation of the <code>matcher</code>.</p> |
| <h3 id="-using-selector-label-"><code>using(selector, label)</code></h3> |
| <p>Scopes the next DSL element selection.</p> |
| <h3 id="-binding-name-"><code>binding(name)</code></h3> |
| <p>Returns the value of the first binding matching the given <code>name</code>.</p> |
| <h3 id="-input-name-enter-value-"><code>input(name).enter(value)</code></h3> |
| <p>Enters the given <code>value</code> in the text field with the corresponding ng-model <code>name</code>.</p> |
| <h3 id="-input-name-check-"><code>input(name).check()</code></h3> |
| <p>Checks/unchecks the checkbox with the corresponding ng-model <code>name</code>.</p> |
| <h3 id="-input-name-select-value-"><code>input(name).select(value)</code></h3> |
| <p>Selects the given <code>value</code> in the radio button with the corresponding ng-model <code>name</code>.</p> |
| <h3 id="-input-name-val-"><code>input(name).val()</code></h3> |
| <p>Returns the current value of an input field with the corresponding ng-model <code>name</code>.</p> |
| <h3 id="-repeater-selector-label-count-"><code>repeater(selector, label).count()</code></h3> |
| <p>Returns the number of rows in the repeater matching the given jQuery <code>selector</code>. The <code>label</code> is |
| used for test output.</p> |
| <h3 id="-repeater-selector-label-row-index-"><code>repeater(selector, label).row(index)</code></h3> |
| <p>Returns an array with the bindings in the row at the given <code>index</code> in the repeater matching the |
| given jQuery <code>selector</code>. The <code>label</code> is used for test output.</p> |
| <h3 id="-repeater-selector-label-column-binding-"><code>repeater(selector, label).column(binding)</code></h3> |
| <p>Returns an array with the values in the column with the given <code>binding</code> in the repeater matching |
| the given jQuery <code>selector</code>. The <code>label</code> is used for test output.</p> |
| <h3 id="-select-name-option-value-"><code>select(name).option(value)</code></h3> |
| <p>Picks the option with the given <code>value</code> on the select with the given ng-model <code>name</code>.</p> |
| <h3 id="-select-name-options-value1-value2-"><code>select(name).options(value1, value2...)</code></h3> |
| <p>Picks the options with the given <code>values</code> on the multi select with the given ng-model <code>name</code>.</p> |
| <h3 id="-element-selector-label-count-"><code>element(selector, label).count()</code></h3> |
| <p>Returns the number of elements that match the given jQuery <code>selector</code>. The <code>label</code> is used for test |
| output.</p> |
| <h3 id="-element-selector-label-click-"><code>element(selector, label).click()</code></h3> |
| <p>Clicks on the element matching the given jQuery <code>selector</code>. The <code>label</code> is used for test output.</p> |
| <h3 id="-element-selector-label-query-fn-"><code>element(selector, label).query(fn)</code></h3> |
| <p>Executes the function <code>fn(selectedElements, done)</code>, where selectedElements are the elements that |
| match the given jQuery <code>selector</code> and <code>done</code> is a function that is called at the end of the <code>fn</code> |
| function. The <code>label</code> is used for test output.</p> |
| <h3 id="-element-selector-label-method-"><code>element(selector, label).{method}()</code></h3> |
| <p>Returns the result of calling <code>method</code> on the element matching the given jQuery <code>selector</code>, where |
| <code>method</code> can be any of the following jQuery methods: <code>val</code>, <code>text</code>, <code>html</code>, <code>height</code>, |
| <code>innerHeight</code>, <code>outerHeight</code>, <code>width</code>, <code>innerWidth</code>, <code>outerWidth</code>, <code>position</code>, <code>scrollLeft</code>, |
| <code>scrollTop</code>, <code>offset</code>. The <code>label</code> is used for test output.</p> |
| <h3 id="-element-selector-label-method-value-"><code>element(selector, label).{method}(value)</code></h3> |
| <p>Executes the <code>method</code> passing in <code>value</code> on the element matching the given jQuery <code>selector</code>, where |
| <code>method</code> can be any of the following jQuery methods: <code>val</code>, <code>text</code>, <code>html</code>, <code>height</code>, |
| <code>innerHeight</code>, <code>outerHeight</code>, <code>width</code>, <code>innerWidth</code>, <code>outerWidth</code>, <code>position</code>, <code>scrollLeft</code>, |
| <code>scrollTop</code>, <code>offset</code>. The <code>label</code> is used for test output.</p> |
| <h3 id="-element-selector-label-method-key-"><code>element(selector, label).{method}(key)</code></h3> |
| <p>Returns the result of calling <code>method</code> passing in <code>key</code> on the element matching the given jQuery |
| <code>selector</code>, where <code>method</code> can be any of the following jQuery methods: <code>attr</code>, <code>prop</code>, <code>css</code>. The |
| <code>label</code> is used for test output.</p> |
| <h3 id="-element-selector-label-method-key-value-"><code>element(selector, label).{method}(key, value)</code></h3> |
| <p>Executes the <code>method</code> passing in <code>key</code> and <code>value</code> on the element matching the given jQuery |
| <code>selector</code>, where <code>method</code> can be any of the following jQuery methods: <code>attr</code>, <code>prop</code>, <code>css</code>. The |
| <code>label</code> is used for test output.</p> |
| <h2 id="matchers">Matchers</h2> |
| <p>Matchers are used in combination with the <code>expect(...)</code> function as described above and can |
| be negated with <code>not()</code>. For instance: <code>expect(element('h1').text()).not().toEqual('Error')</code>.</p> |
| <p>Source: <a href="https://github.com/angular/angular.js/blob/master/src/ngScenario/matchers.js">https://github.com/angular/angular.js/blob/master/src/ngScenario/matchers.js</a></p> |
| <pre><code class="lang-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)</code></pre> |
| <h2 id="example">Example</h2> |
| <p>See the <a href="https://github.com/angular/angular-seed">angular-seed</a> project for more examples.</p> |
| <h3 id="conditional-actions-with-element-query-fn-">Conditional actions with element(...).query(fn)</h3> |
| <p>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 |
| <code>element(...).query(fn)</code>. 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.</p> |
| <p>Imagine the application to be structured into two views:</p> |
| <ol> |
| <li><em>Overview view</em> which lists all the added entries in a table and</li> |
| <li>a <em>detail view</em> which shows the entries' details and contains a delete button. When clicking the |
| delete button, the user is redirected back to the <em>overview page</em>.</li> |
| </ol> |
| <pre><code class="lang-js">beforeEach(function () { |
| var deleteEntry = function () { |
| browser().navigateTo('/entries'); |
| |
| // we need to select the <tbody> 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(); |
| });</code></pre> |
| <p>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:</p> |
| <pre><code class="lang-js">// delete entry 1 |
| browser().navigateTo('/entries'); |
| element('table tbody').query(function (tbody, done) { ... }); |
| element('table tbody a'); |
| element('.btn-danger').click();</code></pre> |
| <p>For two entries, ngScenario would have to work on the following queue:</p> |
| <pre><code class="lang-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();</code></pre> |
| <h2 id="caveats">Caveats</h2> |
| <p><code>ngScenario</code> does not work with apps that manually bootstrap using <code>angular.bootstrap</code>. You must use the <code>ng-app</code> directive.</p> |
| |
| |