Question

I've a got a strange issue. When I try to select a date the select() function selects the wrong option.

Ng-Model:

days = ['01', '02', '03', '04', ..., '31'];

Markup:

<select ng-model="day" id="day" name="day" ng-options="day for day in days">
    <option value="" disabled="disabled"></option>
</select>

e2e test:

it('should select correct date', function () {
   select('day').option('30');
   expect(element('#day option:selected').text()).toEqual('30');
});

So my question is: Why does select('day').option('30') select the 31th day when select('day').option('02') works as expected?


So I was wondering what value it's targeting as it's working fine in other places. The documentation is very sparse so either it's a feature or a bug :)

What I think is going on is the select() tries to select the value in some sort of order. I.E. it first tries to select the option by value then it tries to select it by model value, or something along those lines.

Was it helpful?

Solution

So I looked up the implementation of the select function.

From github:

/**
 * Usage:
 *    select(name).option('value') select one option
 *    select(name).options('value1', 'value2', ...) select options from a multi select
 */
angular.scenario.dsl('select', function() {
  var chain = {};

  chain.option = function(value) {
    return this.addFutureAction("select '" + this.name + "' option '" + value + "'",
      function($window, $document, done) {
        var select = $document.elements('select[ng\\:model="$1"]', this.name);
        var option = select.find('option[value="' + value + '"]');
        if (option.length) {
          select.val(value);
        } else {
          option = select.find('option').filter(function(){
            return _jQuery(this).text() === value;
          });
          if (!option.length) {
            option = select.find('option:contains("' + value + '")');
          }
          if (option.length) {
            select.val(option.val());
          } else {
              return done("option '" + value + "' not found");
          }
        }
        select.trigger('change');
        done();
    });
  };

  return function(name) {
    this.name = name;
    return chain;
  };
});

So the "problem" is that select tries to select from the values in the DOM element, that is the <option value="THIS VALUE"> then it tries to find the value by what's being displayed <option>THIS VALUE</option> and then it tries to do a contains on the value. It doesn't actually use the model value at any point.

So select('day').option('02') worked because it was matching the displayed text, where as select('day').option('30') was matching the option value which has an offset.

Keep in mind the generated HTML is:

<select ng-model="day" id="day" name="day" ng-options="day for day in days">
  <option value="" disabled="disabled"></option>
  <option value="0">01</option> <-- note it starts at 0 not 1
  <option value="1">02</option> <-- select('day').option('02') matches display text '02' as no value 02 exists.
  <option value="2">03</option>
  <option value="29">30</option>
  <option value="30">31</option> <-- select('day').option('30') matches value 30 before display text 30 with value 29.
</select>

To "solve" this issue a new function needs to be created (or alter the existing).

angular.scenario.dsl('selectModel', function() {
    var chain = {};

    chain.option = function(value) {
        return this.addFutureAction("select '" + this.name + "' option '" + value + "'",
            function($window, $document, done) {
                var $ = $window.$; // jQuery inside the iframe
                var select = $document.elements('select[ng\\:model="$1"]', this.name);
                var option = select.find('option').filter(function(){
                    return $(this).text() === value;
                });
                if (!option.length) {
                    option = select.find('option:contains("' + value + '")');
                }
                if (option.length) {
                    select.val(option.val());
                } else {
                    return done("option '" + value + "' not found");
                }

                select.trigger('change');
                done();
            });
    };

    return function(name) {
        this.name = name;
        return chain;
    };
});

This does the trick.

OTHER TIPS

The Angular created option tags have indices as value, when the source model is a list:

$scope.listItems = [
    "day 1",
    "day 2",
    "day 3",
    "day 4",
    "day 5"
];

and

<select ng-model="listItem" ng-options="item for item in listItems"></select>

create the following HTML:

<select ng-options="item for item in listItems" ng-model="listItem" 
class="ng-pristine ng-valid">
    <option value="?" selected="selected"></option>
    <option value="0">day 1</option>
    <option value="1">day 2</option>
    <option value="2">day 3</option>
    <option value="3">day 4</option>
    <option value="4">day 5</option>
</select>

While the option tags have keys as value, when the source model is a map:

$scope.objItems = {
    "day 1":"1",
    "day 2":"2",
    "day 3":"3",
    "day 4":"4",
    "day 5":"5"
};

and

<select ng-model="objItem2" ng-options="value as key for (key, value) 
in objItems"></select>

create:

<select ng-options="value as key for (key, value) in objItems" 
ng-model="objItem2" class="ng-valid ng-dirty">
    <option value="day 1" selected="selected">day 1</option>
    <option value="day 2">day 2</option>
    <option value="day 3">day 3</option>
    <option value="day 4">day 4</option>
    <option value="day 5">day 5</option>
</select>

However this never poses a problem because when the user selects an option, Angular looks up at the indexed position inside the source and assigns that value to the model.

I am not quite familiar with e2e testing but I think the reason you are getting a different value everytime you select an option is that you are trying to access the value stored in the option tag and not what Angular has stored in the model for you.

Fiddle

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top