L'index de boucle (i
) n'est pas ce que j'attends lorsque j'utilise Protractor dans une boucle.
Symptômes:
Échec: index hors limites. Essayer d'accéder à l'élément à l'index: 'x', mais il n'y a que des éléments 'x'
ou
L'index est statique et toujours égal à la dernière valeur
Mon code
for (var i = 0; i < MAX; ++i) {
getPromise().then(function() {
someArray[i] // 'i' always takes the value of 'MAX'
})
}
Par exemple:
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.
})
}
ou
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"
}
})
}
ou
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"
}
})
}
})
La raison pour laquelle cela se produit est que le rapporteur utilise des promesses.
Lire https://github.com/angular/protractor/blob/master/docs/control-flow.md
Les promesses (c'est-à-dire element(by...)
, element.all(by...)
) exécutent leurs fonctions then
lorsque la valeur sous-jacente devient prête. Cela signifie que toutes les promesses sont d'abord planifiées, puis les fonctions then
sont exécutées lorsque les résultats sont prêts.
Lorsque vous exécutez quelque chose comme ça:
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);
Ce qui se passe, c'est que getPromise().then(function() {...})
retourne immédiatement, avant que la promesse ne soit prête et sans exécuter la fonction à l'intérieur de then
. Donc, tout d'abord, la boucle est exécutée 3 fois, en planifiant tous les appels getPromise()
. Ensuite, à mesure que les promesses se résolvent, les then
s correspondants sont exécutés.
La console ressemblerait à ceci:
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.
Alors, comment exécutez-vous le rapporteur en boucle? La solution générale est la fermeture. Voir fermeture JavaScript à l'intérieur des boucles - exemple pratique simple
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);
Mais ce n'est pas si agréable à lire. Heureusement, vous pouvez également utiliser les fonctions de rapporteur filter(fn)
, get(i)
, first()
, last()
et le fait que expect
est corrigé pour prendre des promesses, pour faire face à cela.
Revenons aux exemples fournis précédemment. Le premier exemple peut être réécrit comme suit:
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.
}
Les deuxième et troisième exemples peuvent être réécrits comme suit:
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.
En d'autres termes, le rapporteur a plusieurs façons d'itérer ou d'accéder à l'élément i
afin que vous n'ayez pas besoin d'utiliser pour les boucles et i
. Mais si vous devez utiliser pour les boucles et i
, vous pouvez utiliser la solution de fermeture.
Hank a fait un excellent travail pour répondre à cette question.
Je voulais également noter une autre façon rapide et sale de gérer cela. Déplacez simplement le contenu de la promesse vers une fonction externe et passez-lui l'index.
Par exemple, si vous souhaitez enregistrer tous les éléments de la liste sur la page à leur index respectif (depuis ElementArrayFinder), vous pouvez faire quelque chose comme ceci:
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
});
});
Cela est pratique lorsque vous devez effectuer un débogage rapide et facile à modifier pour votre propre usage.
Je ne discute PAS avec la logique ou la sagesse des personnes beaucoup plus savantes discutées ci-dessus. J'écris pour souligner que dans la version actuelle de Protractor dans une fonction déclarée asynchrone, une boucle for comme la ci-dessous (que j'écrivais en TypeScript, incorporant flowLog de @ hetznercloud/protractor-test-helper, bien que je pense console). log fonctionnerait également ici) agit comme ce à quoi on pourrait naïvement s’attendre.
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'));
}
produire une sortie comme
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
Si je comprends bien, le await
est la clé ici, forçant le tableau à être résolu à l'avance (donc le nombre est correct) et les await
s dans la boucle entraînent la résolution de chaque promesse avant que je ne le soit autorisé à être incrémenté.
Mon intention ici est de donner aux lecteurs des options, pas de remettre en question ce qui précède.