forked from angular/code.angularjs.org
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdev_guide.e2e-testing.html
255 lines (241 loc) · 15.8 KB
/
dev_guide.e2e-testing.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
<a href="http://github.com/angular/angular.js/edit/master/docs/content/guide/dev_guide.e2e-testing.ngdoc" class="improve-docs btn btn-primary"><i class="icon-edit"> </i> Improve this doc</a><h1><code ng:non-bindable=""></code>
<div><span class="hint"></span>
</div>
</h1>
<div><div class="developer-guide-page developer-guide-e2e-testing-page"><p><strong>If you're starting a new Angular project, you may want to look into
using <a href="https://github.com/angular/protractor">Protractor</a>, as it is going to
replace the current method of E2E Testing in the near future.</strong></p>
<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 will write scenario tests in JavaScript, which describe how your application should behave,
given a certain interaction in a specific state. A scenario is comprised of one or more <code>it</code> blocks
(you can think of these as the requirements of your application), which in turn 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), and expectations tell the Runner to assert
something about the state (such as the value of a field or the current URL). If any expectation
fails, the runner marks the <code>it</code> as "failed" and continues on to the next one. Scenarios may also
have <strong>beforeEach</strong> and <strong>afterEach</strong> blocks, which will be run before (or after) each <code>it</code> block,
regardless of whether they pass or fail.</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:
<pre class="prettyprint linenums">
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);
});
});
</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"><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></a></p>
<h3 id="api_pause">pause()</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="api_sleep">sleep(seconds)</h3>
<p>Pauses the execution of the tests for the specified number of <code>seconds</code>.</p>
<h3 id="api_browser">browser().navigateTo(url)</h3>
<p>Loads the <code>url</code> into the test frame.</p>
<h3 id="api_browser">browser().navigateTo(url, fn)</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="api_browser">browser().reload()</h3>
<p>Refreshes the currently loaded page in the test frame.</p>
<h3 id="api_browser">browser().window().href()</h3>
<p>Returns the window.location.href of the currently loaded page in the test frame.</p>
<h3 id="api_browser">browser().window().path()</h3>
<p>Returns the window.location.pathname of the currently loaded page in the test frame.</p>
<h3 id="api_browser">browser().window().search()</h3>
<p>Returns the window.location.search of the currently loaded page in the test frame.</p>
<h3 id="api_browser">browser().window().hash()</h3>
<p>Returns the window.location.hash (without <code>#</code>) of the currently loaded page in the test frame.</p>
<h3 id="api_browser">browser().location().url()</h3>
<p>Returns the <a href="api/ng.$location"><code>$location.url()</code></a> of the currently loaded page in
the test frame.</p>
<h3 id="api_browser">browser().location().path()</h3>
<p>Returns the <a href="api/ng.$location"><code>$location.path()</code></a> of the currently loaded page in
the test frame.</p>
<h3 id="api_browser">browser().location().search()</h3>
<p>Returns the <a href="api/ng.$location"><code>$location.search()</code></a> of the currently loaded page
in the test frame.</p>
<h3 id="api_browser">browser().location().hash()</h3>
<p>Returns the <a href="api/ng.$location"><code>$location.hash()</code></a> of the currently loaded page in
the test frame.</p>
<h3 id="api_expect{matcher}">expect(future).{matcher}</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="api_expect{matcher}">expect(future).not().{matcher}</h3>
<p>Asserts the value of the given <code>future</code> satisfies the negation of the <code>matcher</code>.</p>
<h3 id="api_using">using(selector, label)</h3>
<p>Scopes the next DSL element selection.</p>
<h3 id="api_binding">binding(name)</h3>
<p>Returns the value of the first binding matching the given <code>name</code>.</p>
<h3 id="api_input">input(name).enter(value)</h3>
<p>Enters the given <code>value</code> in the text field with the corresponding ng-model <code>name</code>.</p>
<h3 id="api_input">input(name).check()</h3>
<p>Checks/unchecks the checkbox with the corresponding ng-model <code>name</code>.</p>
<h3 id="api_input">input(name).select(value)</h3>
<p>Selects the given <code>value</code> in the radio button with the corresponding ng-model <code>name</code>.</p>
<h3 id="api_input">input(name).val()</h3>
<p>Returns the current value of an input field with the corresponding ng-model <code>name</code>.</p>
<h3 id="api_repeater">repeater(selector, label).count()</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="api_repeater">repeater(selector, label).row(index)</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="api_repeater">repeater(selector, label).column(binding)</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="api_select">select(name).option(value)</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="api_select">select(name).options(value1, value2...)</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="api_element">element(selector, label).count()</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="api_element">element(selector, label).click()</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="api_element">element(selector, label).query(fn)</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="api_element">element(selector, label).{method}()</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="api_element">element(selector, label).{method}(value)</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="api_element">element(selector, label).{method}(key)</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="api_element">element(selector, label).{method}(key, value)</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"><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></a></p>
<pre class="prettyprint linenums">
// 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)
</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="example_conditional-actions-with-element">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 class="prettyprint linenums">
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();
});
</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 class="prettyprint linenums">
// delete entry 1
browser().navigateTo('/entries');
element('table tbody').query(function (tbody, done) { ... });
element('table tbody a');
element('.btn-danger').click();
</pre>
<p>For two entries, ngScenario would have to work on the following queue:</p>
<pre class="prettyprint linenums">
// 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();
</pre>
<h2 id="caveats">Caveats</h2>
<p>ngScenario does not work with apps that manually bootstrap using angular.bootstrap. You must use the ng-app directive.</p>
</div></div>