Использование транспортира с петлями
Индекс цикла (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
в деле циклы каждого обещает быть решена прежде, чем я разрешено увеличиваться.
Мое намерение здесь состоит в том, чтобы предоставить читателям варианты, а не подвергать сомнению вышеупомянутое.