|
3 | 3 | @name E2E Testing
|
4 | 4 | @description
|
5 | 5 |
|
| 6 | +# E2E Testing |
| 7 | + |
6 | 8 | <div class="alert alert-danger">
|
7 |
| -**Note:** Angular Scenario Runner is depricated. If you're starting a new Angular project, |
8 |
| -consider using [Protractor](https://github.com/angular/protractor). |
| 9 | +**Note:** In the past, end to end testing could be done with a deprecated tool called |
| 10 | +[Angular Scenario Runner](http://code.angularjs.org/1.2.16/docs/guide/e2e-testing). That tool |
| 11 | +is now in maintenance mode. |
9 | 12 | </div>
|
10 | 13 |
|
11 |
| -# E2E Testing with the Angular Scenario Runner |
12 |
| - |
13 | 14 | As applications grow in size and complexity, it becomes unrealistic to rely on manual testing to
|
14 |
| -verify the correctness of new features, catch bugs and notice regressions. |
| 15 | +verify the correctness of new features, catch bugs and notice regressions. End to end tests |
| 16 | +are the first line of defense for catching bugs, but sometimes issues come up with integration |
| 17 | +between components which can't be captured in a unit test. End to end tests are made to find |
| 18 | +these problems. |
| 19 | + |
| 20 | +We have built [Protractor](https://github.com/angular/protractor), an end |
| 21 | +to end test runner which simulates user interactions that will help you verify the health of your |
| 22 | +Angular application. |
15 | 23 |
|
16 |
| -To solve this problem, we have built an Angular Scenario Runner which simulates user interactions |
17 |
| -that will help you verify the health of your Angular application. |
| 24 | +## Using Protractor |
18 | 25 |
|
19 |
| -## Overview |
| 26 | +Protractor is a [Node.js](http://nodejs.org) program, and runs end to end tests that are also |
| 27 | +written in JavaScript and run with node. Protractor uses [WebDriver](https://code.google.com/p/selenium/wiki/GettingStarted) |
| 28 | +to control browsers and simulate user actions. |
20 | 29 |
|
21 |
| -You write scenario tests in JavaScript. These tests describe how your application should behave |
22 |
| -given a certain interaction in a specific state. |
| 30 | +For more information on Protractor, view [getting started](https://github.com/angular/protractor/blob/master/docs/getting-started.md) |
| 31 | +or the [api docs](https://github.com/angular/protractor/blob/master/docs/api.md). |
23 | 32 |
|
24 |
| -A scenario is comprised of one or more `it` blocks that describe the requirements of your |
25 |
| -application. `it` blocks are made of **commands** and **expectations**. Commands tell the Runner |
26 |
| -to do something with the application such as navigate to a page or click on a button. Expectations |
27 |
| -tell the Runner to assert something about the application's state, such as the value of a field or |
28 |
| -the current URL. |
| 33 | +Protractor uses [Jasmine](http://jasmine.github.io/1.3/introduction.html) for its test syntax. |
| 34 | +As in unit testing, a test file is comprised of one or |
| 35 | +more `it` blocks that describe the requirements of your application. `it` blocks are made of |
| 36 | +**commands** and **expectations**. Commands tell Protractor to do something with the application |
| 37 | +such as navigate to a page or click on a button. Expectations tell Protractor to assert something |
| 38 | +about the application's state, such as the value of a field or the current URL. |
29 | 39 |
|
30 | 40 | If any expectation within an `it` block fails, the runner marks the `it` as "failed" and continues
|
31 | 41 | on to the next block.
|
32 | 42 |
|
33 |
| -Scenarios may also have `beforeEach` and `afterEach` blocks, which will be run before or after |
| 43 | +Test files may also have `beforeEach` and `afterEach` blocks, which will be run before or after |
34 | 44 | each `it` block regardless of whether the block passes or fails.
|
35 | 45 |
|
36 | 46 | <img src="img/guide/scenario_runner.png">
|
37 | 47 |
|
38 |
| -In addition to the above elements, scenarios may also contain helper functions to avoid duplicating |
| 48 | +In addition to the above elements, tests may also contain helper functions to avoid duplicating |
39 | 49 | code in the `it` blocks.
|
40 | 50 |
|
41 |
| -Here is an example of a simple scenario: |
| 51 | +Here is an example of a simple test: |
42 | 52 | ```js
|
43 |
| -describe('Buzz Client', function() { |
44 |
| -it('should filter results', function() { |
45 |
| - input('user').enter('jacksparrow'); |
46 |
| - element(':button').click(); |
47 |
| - expect(repeater('ul li').count()).toEqual(10); |
48 |
| - input('filterText').enter('Bees'); |
49 |
| - expect(repeater('ul li').count()).toEqual(1); |
50 |
| -}); |
51 |
| -}); |
52 |
| -``` |
53 |
| - |
54 |
| -Note that |
55 |
| -[`input('user')`](https://github.com/angular/angular.js/blob/master/docs/content/guide/dev_guide.e2e-testing.ngdoc#L119) |
56 |
| -finds the `<input>` element with `ng-model="user"` not `name="user"`. |
57 |
| - |
58 |
| -This scenario describes the requirements of a Buzz Client, specifically, that it should be able to |
59 |
| -filter the stream of the user. It starts by entering a value in the input field with ng-model="user", clicking |
60 |
| -the only button on the page, and then it verifies that there are 10 items listed. It then enters |
61 |
| -'Bees' in the input field with ng-model='filterText' and verifies that the list is reduced to a single item. |
62 |
| - |
63 |
| -The API section below lists the available commands and expectations for the Runner. |
64 |
| - |
65 |
| -## API |
66 |
| -Source: https://github.com/angular/angular.js/blob/master/src/ngScenario/dsl.js |
67 |
| - |
68 |
| -### `pause()` |
69 |
| -Pauses the execution of the tests until you call `resume()` in the console (or click the resume |
70 |
| -link in the Runner UI). |
71 |
| - |
72 |
| -### `sleep(seconds)` |
73 |
| -Pauses the execution of the tests for the specified number of `seconds`. |
74 |
| - |
75 |
| -### `browser().navigateTo(url)` |
76 |
| -Loads the `url` into the test frame. |
77 |
| - |
78 |
| -### `browser().navigateTo(url, fn)` |
79 |
| -Loads the URL returned by `fn` into the testing frame. The given `url` is only used for the test |
80 |
| -output. Use this when the destination URL is dynamic (that is, the destination is unknown when you |
81 |
| -write the test). |
82 |
| - |
83 |
| -### `browser().reload()` |
84 |
| -Refreshes the currently loaded page in the test frame. |
85 |
| - |
86 |
| -### `browser().window().href()` |
87 |
| -Returns the window.location.href of the currently loaded page in the test frame. |
88 |
| - |
89 |
| -### `browser().window().path()` |
90 |
| -Returns the window.location.pathname of the currently loaded page in the test frame. |
91 |
| - |
92 |
| -### `browser().window().search()` |
93 |
| -Returns the window.location.search of the currently loaded page in the test frame. |
94 |
| - |
95 |
| -### `browser().window().hash()` |
96 |
| -Returns the window.location.hash (without `#`) of the currently loaded page in the test frame. |
97 |
| - |
98 |
| -### `browser().location().url()` |
99 |
| -Returns the {@link ng.$location $location.url()} of the currently loaded page in |
100 |
| -the test frame. |
101 |
| - |
102 |
| -### `browser().location().path()` |
103 |
| -Returns the {@link ng.$location $location.path()} of the currently loaded page in |
104 |
| -the test frame. |
105 |
| - |
106 |
| -### `browser().location().search()` |
107 |
| -Returns the {@link ng.$location $location.search()} of the currently loaded page |
108 |
| -in the test frame. |
109 |
| - |
110 |
| -### `browser().location().hash()` |
111 |
| -Returns the {@link ng.$location $location.hash()} of the currently loaded page in |
112 |
| -the test frame. |
113 |
| - |
114 |
| -### `expect(future).{matcher}` |
115 |
| -Asserts the value of the given `future` satisfies the `matcher`. All API statements return a |
116 |
| -`future` object, which get a `value` assigned after they are executed. Matchers are defined using |
117 |
| -`angular.scenario.matcher`, and they use the value of futures to run the expectation. For example: |
118 |
| -`expect(browser().location().href()).toEqual('http://www.google.com')`. Available matchers |
119 |
| -are presented further down this document. |
120 |
| - |
121 |
| -### `expect(future).not().{matcher}` |
122 |
| -Asserts the value of the given `future` satisfies the negation of the `matcher`. |
123 |
| - |
124 |
| -### `using(selector, label)` |
125 |
| -Scopes the next DSL element selection. |
126 |
| - |
127 |
| -### `binding(name)` |
128 |
| -Returns the value of the first binding matching the given `name`. |
129 |
| - |
130 |
| -### `input(name).enter(value)` |
131 |
| -Enters the given `value` in the text field with the corresponding ng-model `name`. |
132 |
| - |
133 |
| -### `input(name).check()` |
134 |
| -Checks/unchecks the checkbox with the corresponding ng-model `name`. |
135 |
| - |
136 |
| -### `input(name).select(value)` |
137 |
| -Selects the given `value` in the radio button with the corresponding ng-model `name`. |
138 |
| - |
139 |
| -### `input(name).val()` |
140 |
| -Returns the current value of an input field with the corresponding ng-model `name`. |
141 |
| - |
142 |
| -### `repeater(selector, label).count()` |
143 |
| -Returns the number of rows in the repeater matching the given jQuery `selector`. The `label` is |
144 |
| -used for test output. |
145 |
| - |
146 |
| -### `repeater(selector, label).row(index)` |
147 |
| -Returns an array with the bindings in the row at the given `index` in the repeater matching the |
148 |
| -given jQuery `selector`. The `label` is used for test output. |
149 |
| - |
150 |
| -### `repeater(selector, label).column(binding)` |
151 |
| -Returns an array with the values in the column with the given `binding` in the repeater matching |
152 |
| -the given jQuery `selector`. The `label` is used for test output. |
153 |
| - |
154 |
| -### `select(name).option(value)` |
155 |
| -Picks the option with the given `value` on the select with the given ng-model `name`. |
156 |
| - |
157 |
| -### `select(name).options(value1, value2...)` |
158 |
| -Picks the options with the given `values` on the multi select with the given ng-model `name`. |
159 |
| - |
160 |
| -### `element(selector, label).count()` |
161 |
| -Returns the number of elements that match the given jQuery `selector`. The `label` is used for test |
162 |
| -output. |
| 53 | +describe('TODO list', function() { |
| 54 | + it('should filter results', function() { |
163 | 55 |
|
164 |
| -### `element(selector, label).click()` |
165 |
| -Clicks on the element matching the given jQuery `selector`. The `label` is used for test output. |
| 56 | + // Find the element with ng-model="user" and type "jacksparrow" into it |
| 57 | + element(by.model('user')).sendKeys('jacksparrow'); |
166 | 58 |
|
167 |
| -### `element(selector, label).query(fn)` |
168 |
| -Executes the function `fn(selectedElements, done)`, where selectedElements are the elements that |
169 |
| -match the given jQuery `selector` and `done` is a function that is called at the end of the `fn` |
170 |
| -function. The `label` is used for test output. |
| 59 | + // Find the first (and only) button on the page and click it |
| 60 | + element(by.css(':button')).click(); |
171 | 61 |
|
172 |
| -### `element(selector, label).{method}()` |
173 |
| -Returns the result of calling `method` on the element matching the given jQuery `selector`, where |
174 |
| -`method` can be any of the following jQuery methods: `val`, `text`, `html`, `height`, |
175 |
| -`innerHeight`, `outerHeight`, `width`, `innerWidth`, `outerWidth`, `position`, `scrollLeft`, |
176 |
| -`scrollTop`, `offset`. The `label` is used for test output. |
| 62 | + // Verify that there are 10 tasks |
| 63 | + expect(element.all(by.repeater('task in tasks')).count()).toEqual(10); |
177 | 64 |
|
178 |
| -### `element(selector, label).{method}(value)` |
179 |
| -Executes the `method` passing in `value` on the element matching the given jQuery `selector`, where |
180 |
| -`method` can be any of the following jQuery methods: `val`, `text`, `html`, `height`, |
181 |
| -`innerHeight`, `outerHeight`, `width`, `innerWidth`, `outerWidth`, `position`, `scrollLeft`, |
182 |
| -`scrollTop`, `offset`. The `label` is used for test output. |
| 65 | + // Enter 'groceries' into the element with ng-model="filterText" |
| 66 | + element(by.model('filterText')).sendKeys('groceries'); |
183 | 67 |
|
184 |
| -### `element(selector, label).{method}(key)` |
185 |
| -Returns the result of calling `method` passing in `key` on the element matching the given jQuery |
186 |
| -`selector`, where `method` can be any of the following jQuery methods: `attr`, `prop`, `css`. The |
187 |
| -`label` is used for test output. |
188 |
| - |
189 |
| -### `element(selector, label).{method}(key, value)` |
190 |
| -Executes the `method` passing in `key` and `value` on the element matching the given jQuery |
191 |
| -`selector`, where `method` can be any of the following jQuery methods: `attr`, `prop`, `css`. The |
192 |
| -`label` is used for test output. |
193 |
| - |
194 |
| -## Matchers |
195 |
| - |
196 |
| -Matchers are used in combination with the `expect(...)` function as described above and can |
197 |
| -be negated with `not()`. For instance: `expect(element('h1').text()).not().toEqual('Error')`. |
198 |
| - |
199 |
| -Source: https://github.com/angular/angular.js/blob/master/src/ngScenario/matchers.js |
200 |
| - |
201 |
| -```js |
202 |
| -// value and Object comparison following the rules of angular.equals(). |
203 |
| -expect(value).toEqual(value) |
204 |
| - |
205 |
| -// a simpler value comparison using === |
206 |
| -expect(value).toBe(value) |
207 |
| - |
208 |
| -// checks that the value is defined by checking its type. |
209 |
| -expect(value).toBeDefined() |
210 |
| - |
211 |
| -// the following two matchers are using JavaScript's standard truthiness rules |
212 |
| -expect(value).toBeTruthy() |
213 |
| -expect(value).toBeFalsy() |
214 |
| - |
215 |
| -// verify that the value matches the given regular expression. The regular |
216 |
| -// expression may be passed in form of a string or a regular expression |
217 |
| -// object. |
218 |
| -expect(value).toMatch(expectedRegExp) |
219 |
| - |
220 |
| -// a check for null using === |
221 |
| -expect(value).toBeNull() |
222 |
| - |
223 |
| -// Array.indexOf(...) is used internally to check whether the element is |
224 |
| -// contained within the array. |
225 |
| -expect(value).toContain(expected) |
226 |
| - |
227 |
| -// number comparison using < and > |
228 |
| -expect(value).toBeLessThan(expected) |
229 |
| -expect(value).toBeGreaterThan(expected) |
230 |
| -``` |
231 |
| - |
232 |
| -## Example |
233 |
| -See the [angular-seed](https://github.com/angular/angular-seed) project for more examples. |
234 |
| - |
235 |
| -### Conditional actions with element(...).query(fn) |
236 |
| - |
237 |
| -E2E testing with angular scenario is highly asynchronous and hides a lot of complexity by |
238 |
| -queueing actions and expectations that can handle futures. From time to time, you might need |
239 |
| -conditional assertions or element selection. Even though you should generally try to avoid this |
240 |
| -(as it is can be sign for unstable tests), you can add conditional behavior with |
241 |
| -`element(...).query(fn)`. The following code listing shows how this function can be used to delete |
242 |
| -added entries (where an entry is some domain object) using the application's web interface. |
243 |
| - |
244 |
| -Imagine the application to be structured into two views: |
245 |
| - |
246 |
| - 1. *Overview view* which lists all the added entries in a table and |
247 |
| - 2. a *detail view* which shows the entries' details and contains a delete button. When clicking the |
248 |
| - delete button, the user is redirected back to the *overview page*. |
249 |
| - |
250 |
| -```js |
251 |
| -beforeEach(function () { |
252 |
| - var deleteEntry = function () { |
253 |
| - browser().navigateTo('/entries'); |
254 |
| - |
255 |
| - // we need to select the <tbody> element as it might be the case that there |
256 |
| - // are no entries (and therefore no rows). When the selector does not |
257 |
| - // result in a match, the test would be marked as a failure. |
258 |
| - element('table tbody').query(function (tbody, done) { |
259 |
| - // ngScenario gives us a jQuery lite wrapped element. We call the |
260 |
| - // `children()` function to retrieve the table body's rows |
261 |
| - var children = tbody.children(); |
262 |
| - |
263 |
| - if (children.length > 0) { |
264 |
| - // if there is at least one entry in the table, click on the link to |
265 |
| - // the entry's detail view |
266 |
| - element('table tbody a').click(); |
267 |
| - // and, after a route change, click the delete button |
268 |
| - element('.btn-danger').click(); |
269 |
| - } |
270 |
| - |
271 |
| - // if there is more than one entry shown in the table, queue another |
272 |
| - // delete action. |
273 |
| - if (children.length > 1) { |
274 |
| - deleteEntry(); |
275 |
| - } |
276 |
| - |
277 |
| - // remember to call `done()` so that ngScenario can continue |
278 |
| - // test execution. |
279 |
| - done(); |
280 |
| - }); |
281 |
| - |
282 |
| - }; |
283 |
| - |
284 |
| - // start deleting entries |
285 |
| - deleteEntry(); |
| 68 | + // Verify that now there is only one item in the task list |
| 69 | + expect(element.all(by.repeater('task in tasks')).count()).toEqual(1); |
| 70 | + }); |
286 | 71 | });
|
287 | 72 | ```
|
288 | 73 |
|
289 |
| -In order to understand what is happening, we should emphasize that ngScenario calls are not |
290 |
| -immediately executed, but queued (in ngScenario terms, we would be talking about adding |
291 |
| -future actions). If we had only one entry in our table, then the following future actions |
292 |
| -would be queued: |
293 |
| - |
294 |
| -```js |
295 |
| -// delete entry 1 |
296 |
| -browser().navigateTo('/entries'); |
297 |
| -element('table tbody').query(function (tbody, done) { ... }); |
298 |
| -element('table tbody a'); |
299 |
| -element('.btn-danger').click(); |
300 |
| -``` |
301 |
| - |
302 |
| -For two entries, ngScenario would have to work on the following queue: |
| 74 | +This test describes the requirements of a ToDo list, specifically, that it should be able to |
| 75 | +filter the list of items. |
303 | 76 |
|
304 |
| -```js |
305 |
| -// delete entry 1 |
306 |
| -browser().navigateTo('/entries'); |
307 |
| -element('table tbody').query(function (tbody, done) { ... }); |
308 |
| -element('table tbody a'); |
309 |
| -element('.btn-danger').click(); |
310 |
| - |
311 |
| - // delete entry 2 |
312 |
| - // indented to represent "recursion depth" |
313 |
| - browser().navigateTo('/entries'); |
314 |
| - element('table tbody').query(function (tbody, done) { ... }); |
315 |
| - element('table tbody a'); |
316 |
| - element('.btn-danger').click(); |
317 |
| -``` |
| 77 | +## Example |
| 78 | +See the [angular-seed](https://github.com/angular/angular-seed) project for more examples, or look |
| 79 | +at the embedded examples in the Angular documentation (For example, [$http](http://docs.angularjs.org/api/ng/service/$http) |
| 80 | +has an end to end test in the example under the `protractor.js` tag). |
318 | 81 |
|
319 | 82 | ## Caveats
|
320 | 83 |
|
321 |
| -`ngScenario` does not work with apps that manually bootstrap using `angular.bootstrap`. You must use the `ng-app` directive. |
| 84 | +Protractor does not work out-of-the-box with apps that manually bootstrap manually using |
| 85 | +`angular.bootstrap`. You must use the `ng-app` directive. |
0 commit comments