Search code examples
regextypescriptcypresschaiassertion

Chai expect() fails in forEach() on 2nd object


I am implementing a testcase in cypress where I want to match a list of dateTime values with a RegEx pattern. All of this gets done in a forEach loop. It works for the first Item and fails on the 2nd item, even though they are the same. Same Item fails test

Here is the code for reproduction:

const array = [
    "2022-05-23 14:39:43.145",
    "2022-05-23 14:39:43.145",
    "2022-05-23 14:39:43.120",
    "2022-05-23 14:39:43.120",
    "2022-05-23 14:39:43.096",
    "2022-05-23 14:39:43.096",
    "2022-05-23 14:39:43.074",
    "2022-05-23 14:39:43.074",
];
const dateTime = new RegExp(/\d\d\d\d-\d\d-\d\d\s\d\d:\d\d:\d\d\.\d\d\d/gm);

describe('tesst',() => {
    it('should work', function() {
        array.forEach((object) => {
            expect(object).to.match(dateTime);
        })
    });
})

Edit It seems like the bug was the global flag (/g) of the RegEx pattern. However I do not get why this is an issue here. I'd be thankful for an explanation :)


Solution

  • You can make the example simpler to help eliminate factors,

    it('tests with regex', function() {
      expect("2022-05-23 14:39:43.145").to.match(dateTime)  // passes
      expect("2022-05-23 14:39:43.120").to.match(dateTime)  // fails
    })
    

    If you look at the chaijs library, this is how to.match() is implemented

    function assertMatch(re, msg) {
      if (msg) flag(this, 'message', msg);
      var obj = flag(this, 'object');
      this.assert(
          re.exec(obj)
        , 'expected #{this} to match ' + re
        , 'expected #{this} not to match ' + re
      );
    }
    

    so the active ingredient is re.exec(obj), equivalent to dateTime.exec("2022-05-23 14:39:43.145") and if you console.log that expression, the first call succeeds and the second returns null - which chai interprets as a failure.

    it('tests with regex', function() {
      console.log(dateTime.exec("2022-05-23 14:39:43.145"))  // ['2022-05-23 14:39:43.145', index: 0...
      console.log(dateTime.exec("2022-05-23 14:39:43.120"))  // null
    })
    

    The reason can be found at MDN RegExp.prototype.exec() Finding successive matches

    If your regular expression uses the "g" flag, you can use the exec() method multiple times to find successive matches in the same string.

    When you do so, the search starts at the substring of str specified by the regular expression's lastIndex property (test() will also advance the lastIndex property).

    Note that the lastIndex property will not be reset when searching a different string, it will start its search at its existing lastIndex .

    If we check the lastIndex property after each step and repeat a few times, every 2nd date fails.

    But after a failure lastIndex is reset and the next test succeeds.

    it('tests with regex', function() {
      console.log(dateTime.exec("2022-05-23 14:39:43.145"))  // ['2022-05-23 14:39:43.145', index: 0...
      console.log(dateTime.lastIndex)                        // 23
      console.log(dateTime.exec("2022-05-23 14:39:43.120"))  // null
      console.log(dateTime.lastIndex)                        // 0
      console.log(dateTime.exec("2022-05-23 14:39:43.096"))  // ['2022-05-23 14:39:43.096', index: 0...
      console.log(dateTime.lastIndex)                        // 23
      console.log(dateTime.exec("2022-05-23 14:39:43.074"))  // null
      console.log(dateTime.lastIndex)                        // 0
    })
    

    So you can make your loop work by manually resetting the lastIndex

    it('should work', function() {
      array.forEach(object => {
        expect(object).to.match(dateTime);  // passes every date
        dateTime.lastIndex = 0;
      })
    })
    

    (or removing the /g flag)