Improve this doc

We did a lot of work in laying a foundation for the app in the last step, so now we'll do something simple; we will add full text search (yes, it will be simple!). We will also write an end-to-end test, because a good end-to-end test is a good friend. It stays with your app, keeps an eye on it, and quickly detects regressions.

The app now has a search box. Notice that the phone list on the page changes depending on what a user types into the search box.

The most important differences between Steps 2 and 3 are listed below. You can see the full diff on GitHub:

Controller

We made no changes to the controller.

Template

app/index.html:

  1. <div class="container-fluid">
  2. <div class="row-fluid">
  3. <div class="span2">
  4. <!--Sidebar content-->
  5.  
  6. Search: <input ng-model="query">
  7.  
  8. </div>
  9. <div class="span10">
  10. <!--Body content-->
  11.  
  12. <ul class="phones">
  13. <li ng-repeat="phone in phones | filter:query">
  14. {{phone.name}}
  15. <p>{{phone.snippet}}</p>
  16. </li>
  17. </ul>
  18.  
  19. </div>
  20. </div>
  21. </div>

We added a standard HTML tag and used Angular's filter function to process the input for the ngRepeat directive.

This lets a user enter search criteria and immediately see the effects of their search on the phone list. This new code demonstrates the following:

  • Data-binding: This is one of the core features in Angular. When the page loads, Angular binds the name of the input box to a variable of the same name in the data model and keeps the two in sync.

In this code, the data that a user types into the input box (named query) is immediately available as a filter input in the list repeater (phone in phones | filter:query). When changes to the data model cause the repeater's input to change, the repeater efficiently updates the DOM to reflect the current state of the model.

Experiments - 图1

  • Use of the filter filter: The filter function uses the query value to create a new array that contains only those records that match the query.

ngRepeat automatically updates the view in response to the changing number of phones returned by the filter filter. The process is completely transparent to the developer.

Test

In Step 2, we learned how to write and run unit tests. Unit tests are perfect for testing controllers and other components of our application written in JavaScript, but they can't easily test DOM manipulation or the wiring of our application. For these, an end-to-end test is a much better choice.

The search feature was fully implemented via templates and data-binding, so we'll write our first end-to-end test, to verify that the feature works.

test/e2e/scenarios.js:

  1. describe('PhoneCat App', function() {
  2.  
  3. describe('Phone list view', function() {
  4.  
  5. beforeEach(function() {
  6. browser().navigateTo('../../app/index.html');
  7. });
  8.  
  9.  
  10. it('should filter the phone list as user types into the search box', function() {
  11. expect(repeater('.phones li').count()).toBe(3);
  12.  
  13. input('query').enter('nexus');
  14. expect(repeater('.phones li').count()).toBe(1);
  15.  
  16. input('query').enter('motorola');
  17. expect(repeater('.phones li').count()).toBe(2);
  18. });
  19. });
  20. });

Even though the syntax of this test looks very much like our controller unit test written with Jasmine, the end-to-end test uses APIs of Angular's end-to-end test runner.

To run the end-to-end test, open one of the following in a new browser tab:

Previously we've seen how Karma can be used to execute unit tests. Well, it can also run the end-to-end tests! Use ./scripts/e2e-test.sh script for that. End-to-end tests are slow, so unlike with unit tests, Karma will exit after the test run and will not automatically rerun the test suite on every file change. To rerun the test suite, execute the e2e-test.sh script again.

Note: You must ensure you've installed karma-ng-scenario prior to running the e2e-test.sh script. You can do this by issuing npm install karma-ng-scenario into your terminal.

This test verifies that the search box and the repeater are correctly wired together. Notice how easy it is to write end-to-end tests in Angular. Although this example is for a simple test, it really is that easy to set up any functional, readable, end-to-end test.

Experiments

  • Display the current value of the query model by adding a {{query}} binding into the index.html template, and see how it changes when you type in the input box.

  • Let's see how we can get the current value of the query model to appear in the HTML page title.

You might think you could just add the {{query}} to the title tag element as follows:

  1. <title>Google Phone Gallery: {{query}}</title>

However, when you reload the page, you won't see the expected result. This is because the "query" model lives in the scope, defined by the ng-controller="PhoneListCtrl" directive, on the body element:

  1. <body ng-controller="PhoneListCtrl">

If you want to bind to the query model from the element, you must move the ngController declaration to the HTML element because it is the common parent of both the body and title elements:

  1. <html ng-app ng-controller="PhoneListCtrl">

Be sure to remove the ng-controller declaration from the body element.

While using double curlies works fine within the title element, you might have noticed that for a split second they are actually displayed to the user while the page is loading. A better solution would be to use the ngBind or ngBindTemplate directives, which are invisible to the user while the page is loading:

  1. <title ng-bind-template="Google Phone Gallery: {{query}}">Google Phone Gallery</title>
  • Add the following end-to-end test into the describe block within test/e2e/scenarios.js:
  1. it('should display the current filter value within an element with id "status"',
  2. function() {
  3. expect(element('#status').text()).toMatch(/Current filter: \s*$/);
  4.  
  5. input('query').enter('nexus');
  6.  
  7. expect(element('#status').text()).toMatch(/Current filter: nexus\s*$/);
  8.  
  9. //alternative version of the last assertion that tests just the value of the binding
  10. using('#status').expect(binding('query')).toBe('nexus');
  11. });

Refresh the browser tab with the end-to-end test runner to see the test fail. To make the test pass, edit the index.html template to add a div or p element with id "status" and content with the query binding, prefixed by "Current filter:". For instance:

  1. <div id="status">Current filter: {{query}}</div>
  • Add a pause() statement inside of an end-to-end test and rerun it. You'll see the runner pause; this gives you the opportunity to explore the state of your application while it is displayed in the browser. The app is live! You can change the search query to prove it. Notice how useful this is for troubleshooting end-to-end tests.

Summary

We have now added full text search and included a test to verify that search works! Now let's go on to step 4 to learn how to add sorting capability to the phone app.