O índice de loop ( i
) não é o que estou esperando quando uso o transferidor em um loop.
Sintomas:
Falhou: indexar fora do limite. Tentando acessar o elemento no índice: ‘x’, mas há apenas elementos ‘x’
ou
O índice é estático e sempre igual ao último valor
Meu código
for (var i = 0; i < MAX; ++i) { getPromise().then(function() { someArray[i] // 'i' always takes the value of 'MAX' }) }
Por exemplo:
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" } }) } })
A razão pela qual isso está acontecendo é porque o transferidor usa promises.
Leia https://github.com/angular/protractor/blob/master/docs/control-flow.md
Promessas (isto é, element(by...)
, element.all(by...)
) executam suas funções quando o valor subjacente fica pronto. O que isto significa é que todas as promises são agendadas primeiro e, em seguida, as funções then
são executadas conforme os resultados se tornam prontos.
Quando você executa algo assim:
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);
O que acontece é que getPromise().then(function() {...})
retorna imediatamente, antes que a promise esteja pronta e sem executar a function dentro then
. Portanto, primeiro o loop é executado 3 vezes, agendando todas as chamadas getPromise()
. Então, como as promises resolvem, os correspondentes then
são executados.
O console seria algo como isto:
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.
Então, como você executa transferidor em loops? A solução geral é o fechamento. Veja o fechamento do JavaScript dentro de loops - exemplo prático simples
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);
Mas isso não é legal de ler. Felizmente, você também pode usar as funções transferidor filter(fn)
, get(i)
, first()
, last()
e o fato de que a expect
é corrigida para receber promises, para lidar com isso.
Voltando aos exemplos fornecidos anteriormente. O primeiro exemplo pode ser reescrito como:
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. }
O segundo e terceiro exemplo pode ser reescrito como:
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.
Em outras palavras, o transferidor tem muitas maneiras de iterar ou acessar o elemento i
para que você não precise usar loops e i
. Mas se você precisar usar loops i
, você pode usar a solução de fechamento.
Hank fez um ótimo trabalho ao responder isso.
Eu também queria anotar outro jeito rápido e sujo de lidar com isso. Basta mover o material de promise para alguma function externa e passar o índice.
Por exemplo, se você quiser registrar todos os itens da lista na página em seu respectivo índice (do ElementArrayFinder), você pode fazer algo assim:
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 }); });
Isso é útil quando você precisa fazer uma debugging rápida e fácil de ajustar para seu próprio uso.