17

The Problem:

I have an array of promises which is resolved to an array of strings. Now the test should pass if at least one of the strings matches a regular expression.

Currently, I solve it using simple string concatenation:

protractor.promise.all([text1, text2, text3]).then(function (values) {
    expect(values[0] + values[1] + values[2]).toMatch(/expression/);
});

Obviously, this does not scale well and is not particularly readable.

The Question:

Is is possible to solve it using a custom jasmine matcher, or jasmine.any() or custom asymmetric equality tester?

alecxe
  • 414,977
  • 106
  • 935
  • 1,083
  • Why wouldn't you just use a variable (`x = false;`) and `values.map(function(v) { x = v.match(/expression/) || x; });` and then just have `expect(x).toBe(true);`? – Elias Van Ootegem Jun 20 '16 at 15:46
  • 4
    What's wrong with `expect(values.some(function(i) { return /expression/.match(i); }).toBe(true);`? – haim770 Jun 20 '16 at 15:47
  • Seems like a custom matcher might be the way to go: http://jasmine.github.io/2.0/custom_matcher.html – Jack A. Jun 20 '16 at 15:47
  • Use Array.some to check for the false evaluation of a expression. values.some(expression) .. – Jose Hermosilla Rodrigo Jun 20 '16 at 15:49
  • @EliasVanOotegem ah, yeah, sort of a functional "reduce" way to solve it - interesting option. Here, I'm asking more about a "jasmine"-way to approach the problem focusing on reusability. Though, of course, I can put your solution into a jasmine matcher and reuse. And, `some()` would probably be the easiest option, missed that. Thank you! – alecxe Jun 20 '16 at 15:50
  • 1
    @alecxe: yeah, I figured you were looking for a more jasmine kind of approach, but yeah, in fact, I'd consider `expect(values.reduce(function(p, v) { return v.match(/expression/) || p; }, false).toBe(true);` to be an acceptable thing to do, mind if I post that as an answer? – Elias Van Ootegem Jun 20 '16 at 15:56
  • @EliasVanOotegem of course, thank you for participation. – alecxe Jun 20 '16 at 15:59

5 Answers5

8

As said in the comments, although I initially use map, reduce would allow you to do what you need, and in this caste at least makes a lot more sense:

protractor.promise.all([text1, text2, text3]).then(function (values) {
    expect(
        values.reduce(function(p, v) {
            return v.match(/expression/) || p;
        }, false)
    ).toBe(true);
});

Or writing the same thing, but using ES6 arrow functions:

protractor.promise.all([text1, text2, text3]).then(function(values) {
    exptect(
        values.reduce( (p, v) => v.match(/expression/) || p, false )
    ).toBe(true);
});

Both do the same thing, the reduce callback will default to false, until the v.match expression evaluates to true.
I'm assuming this is obvious to most people, but I thought I'd provide both syntaxes and some explanation for future reference


Perhaps this solution could be optimized a bit more, to stop matching the pattern once a single match has been found:

protractor.promise.all([text1, text2, text3]).then(function (values) {
    expect(
        values.reduce(function(p, v) {
            return p || !!v.match(/expression/);
        }, false)
    ).toBe(true);
});

All I did was to use the current reduce value as default (once that has been set to true, there's no point in testing any other string value). To ensure v.match evaluates to a boolean instead of an array, I just used !!v.match(). That part is optional though. In ES6, the same thing looks like this:

protractor.promise.all([text1, text2, text3]).then(function(values) {
    exptect(
        values.reduce( (p, v) => p || !!v.match(/expression/), false )
    ).toBe(true);
});

This might perform better with big data sets (considering the match calls stop once the first match was found, as opposed to v.match being called every time).

Elias Van Ootegem
  • 67,812
  • 9
  • 101
  • 138
7

If this works,

protractor.promise.all([text1, text2, text3]).then(function (values) {
    expect(values[0] + values[1] + values[2]).toMatch(/expression/);
});

I think you can write this as follows;

protractor.promise.all([text1, text2, text3]).then(function (values) {
    expect(values.join('')).toMatch(/expression/);
});

And it's scalable. :)

Ali BARIN
  • 1,668
  • 16
  • 20
  • There could be some problems with this approach. If `values` looks like this, for example: `['foo', 'bar', 123, 'car']` and the expression is `/foobar/`, joining all of the values into one big string, the resulting string will match/contain `/foobar/`, but none of the actual individual values would. That wouldn't be desirable IMO – Elias Van Ootegem Jun 30 '16 at 13:24
7

You could simply use map to get a list of boolean and then assert the result with toContain(true):

var all = protractor.promise.all;
var map = protractor.promise.map;

expect(map(all([text1, text2, text3]), RegExp.prototype.test, /expression/)).toContain(true);

You could also use a custom matcher:

expect(all([text1, text2, text3])).toContainPattern(/expression/);

And the custom matcher declared in beforeEach:

beforeEach(function() {
  jasmine.addMatchers({
    toContainPattern: function() {
      return {
        compare: function(actual, regex) {
          return {
            pass: actual.some(RegExp.prototype.test, regex), 
            message: "Expected [" + actual + "] to have one or more match with " + regex
          };
        }
      };
    }
  });
});
Florent B.
  • 37,063
  • 6
  • 68
  • 92
  • Just out of curiosity: what are the advantages of `all`, which looks to me like it is the equivalent of `map` over `values.reduce()`? I know it doesn't really matter, but intuitively, I'd say `reduce` is more likely to perform better on large data sets, especially if you stop matching after a the first valid match was found (cf my updated answer). Anyway, +1 because your answer is more in line with the OP wanting a more jasmine-like approach – Elias Van Ootegem Jun 30 '16 at 13:26
  • @Elias Van Ootegem, `all` is not the same as `map`. `all` resolves a collection of promises, while `map` resolves a single promise. Using `map` over `reduce` won't have any performance impact since all the cost is in retrieving the text for each element, which is one call to the browser for each element. And if it were of any concern, then `Array.some()` with `/expression/.test(item)` would be more appropriate and less expensive than `Array.reduce` with `item.match(/expression/)`. – Florent B. Jun 30 '16 at 14:45
  • Yes, but `[].reduce` (as per second approach in my answer) allows you to skip the `match` call once a positive match was found, so there will be between 1 and n match calls, whereas `map` and `all` seem to process all array values, or am I missing something? – Elias Van Ootegem Jun 30 '16 at 16:56
  • @Elias Van Ootegem, you are only skipping the cost of the regex evaluation which is insignificant. The expensive part is resolving the `all` to the get the values. Why would you use `values.reduce((p, v) => p || !!v.match(/expression/), false)` when `values.some(v => /expression/.test(v))` does the same thing but better? – Florent B. Jun 30 '16 at 17:46
5

If those [text1, text2, text3] is texts from ElementFinder .getText() then you can also to try with Expected Conditions (You know that i am huge fan of EC right? :) ).

describe('test', function () {

    it('test', function () {
        var EC = protractor.ExpectedConditions;
        browser.get('http://www.protractortest.org/testapp/ng1/#/form');

        var textToContain = 'Check'; //Notice, that this is not 'equals', this is 'contains'
        var elementTextToCheck1 = EC.textToBePresentInElement($('#checkboxes h4'), textToContain); // Here it will be true : Checkboxes
        var elementTextToCheck2 = EC.textToBePresentInElement($('#animals h4'), textToContain); //false
        var elementTextToCheck3 = EC.textToBePresentInElement($('#transformedtext h4'), textToContain); //false

        var oneElementShouldContainText = EC.or(elementTextToCheck1, elementTextToCheck2, elementTextToCheck3);

        expect(oneElementShouldContainText()).toBeTruthy(`At least one element should contain "${textToContain}"`);
    })
});

For simple elements: http://www.protractortest.org/#/api?view=ExpectedConditions.prototype.textToBePresentInElement

For textArea, inputs: http://www.protractortest.org/#/api?view=ExpectedConditions.prototype.textToBePresentInElementValue

Notice, that unfortunatelly .textToBePresentInElement works only with single ElementFinder, ArrayElementFinder is not supported. But in that case you can make something with .filter() and assert that returned list is empty.

Xotabu4
  • 2,927
  • 12
  • 27
2

Array.prototype.some() looks like what you actually looking for.

protractor.promise.all([text1, text2, text3]).then(function (values) {
    expect(values.some(v => v.match(/expression/)).toBe(true);
});

For more information https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/some