Использование транспортира с петлями

Индекс цикла (i) - это не то, что я ожидаю, когда я использую Protractor в цикле.

Симптомы:

Не удалось: указатель не связан. Пытается получить доступ к элементу по индексу: "x", но есть только "x" элементы

или

Индекс статичен и всегда равен последнему значению

Мой код

for (var i = 0; i < MAX; ++i) {
  getPromise().then(function() {
    someArray[i] // 'i' always takes the value of 'MAX'
  })
}

Например:

var expected = ['expect1', 'expect2', 'expect3'];
var els = element.all(by.css('selector'));
for (var i = 0; i < expected.length; ++i) {
  els.get(i).getText().then(function(text) {
    expect(text).toEqual(expected[i]); // Error: `i` is always 3. 
  })
}

или

var els = element.all(by.css('selector'));
for (var i = 0; i < 3; ++i) {
  els.get(i).getText().then(function(text) {
    if (text === 'should click') {
      els.get(i).click(); // fails with "Failed: Index out of bound. Trying to access element at index:3, but there are only 3 elements"
    }
  })
}

или

var els = element.all(by.css('selector'));
els.then(function(rawelements) {
  for (var i = 0; i < rawelements.length; ++i) {
    rawelements[i].getText().then(function(text) {
      if (text === 'should click') {
        rawelements[i].click(); // fails with "Failed: Index out of bound. Trying to access element at index:'rawelements.length', but there are only 'rawelements.length' elements"
      }
    })
  }
})

Ответ 1

Причина, по которой это происходит, заключается в том, что в роли поставщика используется promises.

Прочитайте https://github.com/angular/protractor/blob/master/docs/control-flow.md

Promises (т.е. element(by...), element.all(by...)) выполняют свои функции then, когда базовое значение становится готовым. Это означает, что все promises сначала запланированы, а затем функции then запускаются по мере того, как результаты становятся готовыми.

Когда вы запускаете что-то вроде этого:

for (var i = 0; i < 3; ++i) {
  console.log('1) i is: ', i);
  getPromise().then(function() {
    console.log('2) i is: ', i);
    someArray[i] // 'i' always takes the value of 3
  })
}
console.log('*  finished looping. i is: ', i);

Что происходит, так это то, что getPromise().then(function() {...}) возвращается немедленно, пока обещание не будет готово и не будет выполнять функцию внутри then. Итак, сначала цикл проходит три раза, планируя все вызовы getPromise(). Затем, как разрешение promises, выполняются соответствующие then.

Консоль будет выглядеть примерно так:

1) i is: 0 // schedules first `getPromise()`
1) i is: 1 // schedules second `getPromise()`
1) i is: 2 // schedules third `getPromise()`
*  finished looping. i is: 3
2) i is: 3 // first `then` function runs, but i is already 3 now.
2) i is: 3 // second `then` function runs, but i is already 3 now.
2) i is: 3 // third `then` function runs, but i is already 3 now.

Итак, как вы запускаете транспортир в цикле? Общее решение - замыкание. См. Закрытие JavaScript внутри цикла - простой практический пример

for (var i = 0; i < 3; ++i) {
  console.log('1) i is: ', i);
  var func = (function() {
    var j = i; 
    return function() {
      console.log('2) j is: ', j);
      someArray[j] // 'j' takes the values of 0..2
    }
  })();
  getPromise().then(func);
}
console.log('*  finished looping. i is: ', i);

Но это не так приятно читать. К счастью, вы также можете использовать функции транспортиратора filter(fn), get(i), first(), last(), а тот факт, что expect исправлен, чтобы принять promises, чтобы справиться с этим.

Возвращаясь к приведенным ранее примерам. Первый пример можно переписать как:

var expected = ['expect1', 'expect2', 'expect3'];
var els = element.all(by.css('selector'));
for (var i = 0; i < expected.length; ++i) {
  expect(els.get(i).getText()).toEqual(expected[i]); // note, the i is no longer in a `then` function and take the correct values.
}

Второй и третий пример можно переписать как:

var els = element.all(by.css('selector'));
els.filter(function(elem) {
  return elem.getText().then(function(text) {
    return text === 'should click';
  });
}).click(); 
// note here we first used a 'filter' to select the appropriate elements, and used the fact that actions like `click` can act on an array to click all matching elements. The result is that we can stop using a for loop altogether. 

Другими словами, у protractor есть много способов перебора или доступа к элементу i, поэтому вам не нужно использовать для циклов и i. Но если вы должны использовать для циклов и i, вы можете использовать решение для закрытия.

Ответ 2

Хэнк отлично справился с этим ответом. Я хотел также отметить еще один быстрый и грязный способ справиться с этим. Просто переместите обещание на какую-нибудь внешнюю функцию и передайте ему индекс.

Например, если вы хотите регистрировать все элементы списка на странице по их соответствующему индексу (из ElementArrayFinder), вы можете сделать что-то вроде этого:

  var log_at_index = function (matcher, index) {
    return $$(matcher).get(index).getText().then(function (item_txt) {
      return console.log('item[' + index + '] = ' + item_txt);
    });
  };

  var css_match = 'li';
  it('should log all items found with their index and displayed text', function () {
    $$(css_match).count().then(function (total) {
      for(var i = 0; i < total; i++)
        log_at_index(css_match, i); // move promises to external function
    });
  });

Это пригодится, когда вам нужно выполнить быструю отладку и легко настроить для собственного использования.

Ответ 3

Я НЕ спорю с логикой или мудростью гораздо более образованных людей, обсуждающих выше. Я пишу, чтобы указать, что в текущей версии Protractor внутри функции, объявленной как async, цикл for похож на приведенный ниже (который я писал в typeScript, включающий flowLog из @hetznercloud/protractor-test-helper, хотя я считаю, что console. журнал тоже бы тут работал) ведет себя так, как можно наивно ожидать.

let inputFields = await element.all(by.tagName('input'));
let i: number;
flowLog('count = '+ inputFields.length);
for (i=0; i < inputFields.length; i++){
  flowLog(i+' '+await inputFields[i].getAttribute('id')+' '+await inputFields[i].getAttribute('value'));
}

производя продукцию как

    count = 44
0 7f7ac149-749f-47fd-a871-e989a5bd378e 1
1 7f7ac149-749f-47fd-a871-e989a5bd3781 2
2 7f7ac149-749f-47fd-a871-e989a5bd3782 3
3 7f7ac149-749f-47fd-a871-e989a5bd3783 4
4 7f7ac149-749f-47fd-a871-e989a5bd3784 5
5 7f7ac149-749f-47fd-a871-e989a5bd3785 6

...

42 7f7ac149-749f-47fd-a871-e989a5bd376a 1
43 7f7ac149-749f-47fd-a871-e989a5bd376b 2

Как я понимаю, await является ключевым здесь, заставляя массив быть решен до фронта (так рассчитывать правильно) и await в деле циклы каждого обещает быть решена прежде, чем я разрешено увеличиваться.

Мое намерение здесь состоит в том, чтобы предоставить читателям варианты, а не подвергать сомнению вышеупомянутое.