web-dev-qa-db-fra.com

Utiliser un rapporteur avec des boucles

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"
      }
    })
  }
})
27
hankduan

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 thens 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.

37
hankduan

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.

3
willko747

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 awaits 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.

0
Jeremy Kahan