diff --git a/src/__tests__/example-currency.ts b/src/__tests__/example-currency.ts index 200c45c..26b4dc0 100644 --- a/src/__tests__/example-currency.ts +++ b/src/__tests__/example-currency.ts @@ -22,18 +22,21 @@ test('example: extracting currency values', () => { endOfString, ]); - expect(currencyRegex).toMatchString('$10'); - expect(currencyRegex).toMatchString('$ 10'); + expect(currencyRegex).toMatchString('$10', { exactString: false, substring: '10' }); + expect(currencyRegex).toMatchString('$ 10', { exactString: false, substring: '10' }); expect(currencyRegex).not.toMatchString('$ 10.'); - expect(currencyRegex).toMatchString('$ 10'); + expect(currencyRegex).toMatchString('$ 10', { exactString: false, substring: '10' }); expect(currencyRegex).not.toMatchString('$10.5'); - expect(currencyRegex).toMatchString('$10.50'); + expect(currencyRegex).toMatchString('$10.50', { exactString: false, substring: '10.50' }); expect(currencyRegex).not.toMatchString('$10.501'); - expect(currencyRegex).toMatchString('€100'); - expect(currencyRegex).toMatchString('£1,000'); - expect(currencyRegex).toMatchString('$ 100000000000000000'); - expect(currencyRegex).toMatchString('€ 10000'); - expect(currencyRegex).toMatchString('₿ 100,000'); + expect(currencyRegex).toMatchString('€100', { exactString: false, substring: '100' }); + expect(currencyRegex).toMatchString('£1,000', { exactString: false, substring: '1,000' }); + expect(currencyRegex).toMatchString('$ 100000000000000000', { + exactString: false, + substring: '100000000000000000', + }); + expect(currencyRegex).toMatchString('€ 10000', { exactString: false, substring: '10' }); + expect(currencyRegex).toMatchString('₿ 100,000', { exactString: false, substring: '100,000' }); expect(currencyRegex).not.toMatchString('10$'); expect(currencyRegex).not.toMatchString('£A000'); diff --git a/src/__tests__/example-filename.ts b/src/__tests__/example-filename.ts index 5018b94..47823a6 100644 --- a/src/__tests__/example-filename.ts +++ b/src/__tests__/example-filename.ts @@ -9,13 +9,13 @@ test('example: filename validator', () => { endOfString, ]); - expect(filenameRegex).toMatchString('index.ts'); - expect(filenameRegex).toMatchString('index.tsx'); - expect(filenameRegex).toMatchString('ind/ex.ts'); - expect(filenameRegex).not.toMatchString('index.js'); - expect(filenameRegex).not.toMatchString('index.html'); - expect(filenameRegex).not.toMatchString('index.css'); - expect(filenameRegex).not.toMatchString('./index.js'); - expect(filenameRegex).not.toMatchString('./index.html'); - expect(filenameRegex).not.toMatchString('./index.css'); + expect(filenameRegex).toMatchString('index.ts', { exactString: false }); + expect(filenameRegex).toMatchString('index.tsx', { exactString: false }); + expect(filenameRegex).toMatchString('ind/ex.ts', { exactString: false }); + expect(filenameRegex).not.toMatchString('index.js', { exactString: false }); + expect(filenameRegex).not.toMatchString('index.html', { exactString: false }); + expect(filenameRegex).not.toMatchString('index.css', { exactString: false }); + expect(filenameRegex).not.toMatchString('./index.js', { exactString: false }); + expect(filenameRegex).not.toMatchString('./index.html', { exactString: false }); + expect(filenameRegex).not.toMatchString('./index.css', { exactString: false }); }); diff --git a/src/__tests__/example-find-suffixes.ts b/src/__tests__/example-find-suffixes.ts index aec6195..e8570a3 100644 --- a/src/__tests__/example-find-suffixes.ts +++ b/src/__tests__/example-find-suffixes.ts @@ -9,16 +9,16 @@ test('example: find words with suffix', () => { wordBoundary, ]); - expect(regex).toMatchString('democracy'); - expect(regex).toMatchString('Bureaucracy'); - expect(regex).toMatchString('abc privacy '); - expect(regex).toMatchString('abc dynamism'); - expect(regex).toMatchString('realism abc'); - expect(regex).toMatchString('abc modernism abc'); + expect(regex).toMatchString('democracy', { exactString: false }); + expect(regex).toMatchString('Bureaucracy', { exactString: false }); + expect(regex).toMatchString('abc privacy ', { exactString: false }); + expect(regex).toMatchString('abc dynamism', { exactString: false }); + expect(regex).toMatchString('realism abc', { exactString: false }); + expect(regex).toMatchString('abc modernism abc', { exactString: false }); - expect(regex).not.toMatchString('abc acy'); - expect(regex).not.toMatchString('ism abc'); - expect(regex).not.toMatchString('dynamisms'); + expect(regex).not.toMatchString('abc acy', { exactString: false }); + expect(regex).not.toMatchString('ism abc', { exactString: false }); + expect(regex).not.toMatchString('dynamisms', { exactString: false }); expect(regex).toEqualRegex(/\B(?:acy|ism)\b/); }); diff --git a/src/__tests__/example-find-words.ts b/src/__tests__/example-find-words.ts index d7f1254..3c4f784 100644 --- a/src/__tests__/example-find-words.ts +++ b/src/__tests__/example-find-words.ts @@ -9,15 +9,15 @@ test('example: find specific words', () => { wordBoundary, ]); - expect(regex).toMatchString('word'); - expect(regex).toMatchString('some date'); - expect(regex).toMatchString('date and word'); + expect(regex).toMatchString('word', { exactString: false }); + expect(regex).toMatchString('some date', { exactString: false }); + expect(regex).toMatchString('date and word', { exactString: false }); - expect(regex).not.toMatchString('sword'); - expect(regex).not.toMatchString('keywords'); - expect(regex).not.toMatchString('words'); - expect(regex).not.toMatchString('update'); - expect(regex).not.toMatchString('dates'); + expect(regex).not.toMatchString('sword', { exactString: false }); + expect(regex).not.toMatchString('keywords', { exactString: false }); + expect(regex).not.toMatchString('words', { exactString: false }); + expect(regex).not.toMatchString('update', { exactString: false }); + expect(regex).not.toMatchString('dates', { exactString: false }); expect(regex).toEqualRegex(/\b(?:word|date)\b/); }); diff --git a/src/constructs/__tests__/anchors.test.tsx b/src/constructs/__tests__/anchors.test.ts similarity index 100% rename from src/constructs/__tests__/anchors.test.tsx rename to src/constructs/__tests__/anchors.test.ts diff --git a/src/constructs/__tests__/lookahead.test.ts b/src/constructs/__tests__/lookahead.test.ts index 64d76dc..8d0dc1a 100644 --- a/src/constructs/__tests__/lookahead.test.ts +++ b/src/constructs/__tests__/lookahead.test.ts @@ -11,8 +11,10 @@ test('`lookahead` pattern', () => { }); test('`lookahead` matching', () => { - expect([oneOrMore(digit), lookahead('$')]).toMatchString('1 turkey costs 30$'); - expect(['q', lookahead('u')]).toMatchString('queen'); + expect([oneOrMore(digit), lookahead('$')]).toMatchString('1 turkey costs 30$', { + exactString: false, + }); + expect(['q', lookahead('u')]).toMatchString('queen', { exactString: false }); expect(['a', lookahead('b'), lookahead('c')]).not.toMatchString('abc'); expect(['a', lookahead(capture('bba'))]).toMatchGroups('abba', ['a', 'bba']); }); diff --git a/src/constructs/__tests__/lookbehind.test.ts b/src/constructs/__tests__/lookbehind.test.ts index 16053be..ba9b5bb 100644 --- a/src/constructs/__tests__/lookbehind.test.ts +++ b/src/constructs/__tests__/lookbehind.test.ts @@ -12,20 +12,22 @@ test('`lookbehind` pattern', () => { test('`lookbehind` matching', () => { expect([zeroOrMore(whitespace), word, lookbehind('s'), oneOrMore(whitespace)]).toMatchString( 'too many cats to feed.', + { exactString: false }, ); expect([lookbehind('USD'), zeroOrMore(whitespace), oneOrMore(digit)]).toMatchString( 'The price is USD 30', + { exactString: false }, ); expect([lookbehind('USD'), zeroOrMore(whitespace), oneOrMore(digit)]).not.toMatchString( 'The price is CDN 30', ); - expect([lookbehind('a'), 'b']).toMatchString('abba'); + expect([lookbehind('a'), 'b']).toMatchString('abba', { exactString: false }); const mjsImport = [lookbehind('.mjs')]; - expect(mjsImport).toMatchString("import {Person} from './person.mjs';"); + expect(mjsImport).toMatchString("import {Person} from './person.mjs';", { exactString: false }); expect(mjsImport).not.toMatchString("import {Person} from './person.js';"); expect([anyOf('+-'), oneOrMore(digit), lookbehind('-')]).not.toMatchString('+123'); }); diff --git a/src/constructs/__tests__/negative-lookahead.test.ts b/src/constructs/__tests__/negative-lookahead.test.ts index 3938117..430a0c6 100644 --- a/src/constructs/__tests__/negative-lookahead.test.ts +++ b/src/constructs/__tests__/negative-lookahead.test.ts @@ -11,8 +11,10 @@ test('`negativeLookahead` pattern', () => { }); test('`negativeLookahead` matching', () => { - expect([negativeLookahead('$'), oneOrMore(digit)]).toMatchString('1 turkey costs 30$'); - expect([negativeLookahead('a'), 'b']).toMatchString('abba'); + expect([negativeLookahead('$'), oneOrMore(digit)]).toMatchString('1 turkey costs 30$', { + exactString: false, + }); + expect([negativeLookahead('a'), 'b']).toMatchString('abba', { exactString: false }); expect(['a', negativeLookahead(capture('bba'))]).not.toMatchGroups('abba', ['a', 'bba']); expect([negativeLookahead('-'), anyOf('+-'), zeroOrMore(digit)]).not.toMatchString('-123'); expect([negativeLookahead('-'), anyOf('+-'), zeroOrMore(digit)]).toMatchString('+123'); diff --git a/src/encoder/__tests__/encoder.test.tsx b/src/encoder/__tests__/encoder.test.ts similarity index 100% rename from src/encoder/__tests__/encoder.test.tsx rename to src/encoder/__tests__/encoder.test.ts diff --git a/src/patterns/__tests__/atoms.tests.ts b/src/patterns/__tests__/atoms.tests.ts new file mode 100644 index 0000000..6c54ad1 --- /dev/null +++ b/src/patterns/__tests__/atoms.tests.ts @@ -0,0 +1,72 @@ +import { hexDigit, lowerCaseHexDigit, upperCaseHexDigit } from '../atoms'; + +test('lowerCaseHexDigit', () => { + expect(lowerCaseHexDigit).toMatchString('0'); + expect(lowerCaseHexDigit).toMatchString('1'); + expect(lowerCaseHexDigit).toMatchString('2'); + expect(lowerCaseHexDigit).toMatchString('3'); + expect(lowerCaseHexDigit).toMatchString('4'); + expect(lowerCaseHexDigit).toMatchString('5'); + expect(lowerCaseHexDigit).toMatchString('6'); + expect(lowerCaseHexDigit).toMatchString('7'); + expect(lowerCaseHexDigit).toMatchString('8'); + expect(lowerCaseHexDigit).toMatchString('9'); + expect(lowerCaseHexDigit).toMatchString('a'); + expect(lowerCaseHexDigit).toMatchString('b'); + expect(lowerCaseHexDigit).toMatchString('c'); + expect(lowerCaseHexDigit).toMatchString('d'); + expect(lowerCaseHexDigit).toMatchString('e'); + expect(lowerCaseHexDigit).toMatchString('f'); + expect(lowerCaseHexDigit).not.toMatchString('g'); + expect(lowerCaseHexDigit).not.toMatchString('h'); +}); + +test('upperCaseHexDigit', () => { + expect(upperCaseHexDigit).toMatchString('0'); + expect(upperCaseHexDigit).toMatchString('1'); + expect(upperCaseHexDigit).toMatchString('2'); + expect(upperCaseHexDigit).toMatchString('3'); + expect(upperCaseHexDigit).toMatchString('4'); + expect(upperCaseHexDigit).toMatchString('5'); + expect(upperCaseHexDigit).toMatchString('6'); + expect(upperCaseHexDigit).toMatchString('7'); + expect(upperCaseHexDigit).toMatchString('8'); + expect(upperCaseHexDigit).toMatchString('9'); + expect(upperCaseHexDigit).toMatchString('A'); + expect(upperCaseHexDigit).toMatchString('B'); + expect(upperCaseHexDigit).toMatchString('C'); + expect(upperCaseHexDigit).toMatchString('D'); + expect(upperCaseHexDigit).toMatchString('E'); + expect(upperCaseHexDigit).toMatchString('F'); + expect(upperCaseHexDigit).not.toMatchString('G'); + expect(upperCaseHexDigit).not.toMatchString('H'); +}); + +test('hexDigit', () => { + expect(hexDigit).toMatchString('0'); + expect(hexDigit).toMatchString('1'); + expect(hexDigit).toMatchString('2'); + expect(hexDigit).toMatchString('3'); + expect(hexDigit).toMatchString('4'); + expect(hexDigit).toMatchString('5'); + expect(hexDigit).toMatchString('6'); + expect(hexDigit).toMatchString('7'); + expect(hexDigit).toMatchString('8'); + expect(hexDigit).toMatchString('9'); + expect(hexDigit).toMatchString('a'); + expect(hexDigit).toMatchString('b'); + expect(hexDigit).toMatchString('c'); + expect(hexDigit).toMatchString('d'); + expect(hexDigit).toMatchString('e'); + expect(hexDigit).toMatchString('f'); + expect(hexDigit).not.toMatchString('g'); + expect(hexDigit).not.toMatchString('h'); + expect(hexDigit).toMatchString('A'); + expect(hexDigit).toMatchString('B'); + expect(hexDigit).toMatchString('C'); + expect(hexDigit).toMatchString('D'); + expect(hexDigit).toMatchString('E'); + expect(hexDigit).toMatchString('F'); + expect(hexDigit).not.toMatchString('G'); + expect(hexDigit).not.toMatchString('H'); +}); diff --git a/src/patterns/__tests__/ip-addr.tests.ts b/src/patterns/__tests__/ip-addr.tests.ts new file mode 100644 index 0000000..10ff9d6 --- /dev/null +++ b/src/patterns/__tests__/ip-addr.tests.ts @@ -0,0 +1,68 @@ +import { ipv4DigitValidator, ipv4Validator, ipv6GroupValidator } from '..'; + +test('ipDigit', () => { + expect(ipv4DigitValidator).toMatchString('255'); + expect(ipv4DigitValidator).not.toMatchString('256'); + expect(ipv4DigitValidator).toMatchString('25'); + expect(ipv4DigitValidator).not.toMatchString('25.5'); + expect(ipv4DigitValidator).toMatchString('249'); + expect(ipv4DigitValidator).toMatchString('100'); + expect(ipv4DigitValidator).toMatchString('199'); + expect(ipv4DigitValidator).not.toMatchString('1000'); + expect(ipv4DigitValidator).not.toMatchString('100.'); + expect(ipv4DigitValidator).toMatchString('000'); + expect(ipv4DigitValidator).toMatchString('00'); + expect(ipv4DigitValidator).toMatchString('0'); + expect(ipv4DigitValidator).not.toMatchString('000.0'); + expect(ipv4DigitValidator).not.toMatchString('00.0'); + expect(ipv4DigitValidator).not.toMatchString('0.0'); +}); + +test('ipv6GroupValidator', () => { + expect(ipv6GroupValidator).toMatchString('0'); + expect(ipv6GroupValidator).toMatchString('00'); + expect(ipv6GroupValidator).toMatchString('000'); + expect(ipv6GroupValidator).toMatchString('0000'); + expect(ipv6GroupValidator).not.toMatchString('00000'); + expect(ipv6GroupValidator).toMatchString('1'); + expect(ipv6GroupValidator).toMatchString('01'); + expect(ipv6GroupValidator).toMatchString('001'); + expect(ipv6GroupValidator).toMatchString('0001'); + expect(ipv6GroupValidator).not.toMatchString('00001'); + expect(ipv6GroupValidator).toMatchString('f'); + expect(ipv6GroupValidator).toMatchString('ff'); + expect(ipv6GroupValidator).toMatchString('fff'); + expect(ipv6GroupValidator).toMatchString('ffff'); + expect(ipv6GroupValidator).not.toMatchString('fffff'); + expect(ipv6GroupValidator).toMatchString('a'); + expect(ipv6GroupValidator).toMatchString('aa'); + expect(ipv6GroupValidator).toMatchString('aaa'); + expect(ipv6GroupValidator).toMatchString('aaaa'); + expect(ipv6GroupValidator).not.toMatchString('aaaaa'); + expect(ipv6GroupValidator).toMatchString('A'); + expect(ipv6GroupValidator).toMatchString('AA'); + expect(ipv6GroupValidator).toMatchString('AAA'); + expect(ipv6GroupValidator).toMatchString('AAAA'); + expect(ipv6GroupValidator).not.toMatchString('AAAAA'); + expect(ipv6GroupValidator).not.toMatchString('g'); + expect(ipv6GroupValidator).not.toMatchString('gg'); + expect(ipv6GroupValidator).not.toMatchString('ggg'); + expect(ipv6GroupValidator).not.toMatchString('gggg'); + expect(ipv6GroupValidator).toMatchString('2001'); + expect(ipv6GroupValidator).toMatchString('Db8'); + expect(ipv6GroupValidator).toMatchString('8'); + expect(ipv6GroupValidator).toMatchString('800'); + expect(ipv6GroupValidator).toMatchString('200C'); + expect(ipv6GroupValidator).toMatchString('417A'); +}); + +test('ipv4Validator', () => { + expect(ipv4Validator).toMatchString('255.255.255.255'); + expect(ipv4Validator).not.toMatchString('255:255:255:255'); + expect(ipv4Validator).not.toMatchString('256.256.256.256'); + expect(ipv4Validator).not.toMatchString('255.255.255.255.255'); + expect(ipv4Validator).not.toMatchString('255.255.255'); + expect(ipv4Validator).toMatchString('10.0.0.128'); + expect(ipv4Validator).not.toMatchString('10.0.0.128.'); + expect(ipv4Validator).not.toMatchString('.....'); +}); diff --git a/src/patterns/__tests__/url.tests.ts b/src/patterns/__tests__/url.tests.ts new file mode 100644 index 0000000..46d4f1f --- /dev/null +++ b/src/patterns/__tests__/url.tests.ts @@ -0,0 +1,225 @@ +import { + urlAuthorityFinder, + urlAuthorityValidator, + urlFragmentValidator, + urlHost, + urlHostValidator, + urlPathValidator, + urlQueryValidator, + urlScheme, + urlSchemeFinder, + urlSchemeValidator, + urlValidator, +} from '..'; + +test('urlScheme', () => { + expect(urlScheme).not.toMatchString('http'); + expect(urlScheme).not.toMatchString('http://'); + expect(urlScheme).not.toMatchString('http//'); + expect(urlScheme).toMatchString('ftp:', { exactString: false, substring: 'ftp' }); + expect(urlScheme).not.toMatchString('ftp:'); + expect(urlScheme).toMatchString('http:', { exactString: false, substring: 'http' }); + expect(urlScheme).not.toMatchString('http:'); + expect(urlScheme).toMatchString('http://', { exactString: false, substring: 'http' }); + expect(urlScheme).not.toMatchString('http://'); +}); + +test('urlSchemeValidator', () => { + expect(urlSchemeValidator).not.toMatchString('http'); + expect(urlSchemeValidator).not.toMatchString('http://'); + expect(urlSchemeValidator).not.toMatchString('http//', { exactString: false, substring: 'http' }); + expect(urlSchemeValidator).toMatchString('ftp:'); + expect(urlSchemeValidator).not.toMatchString('http://'); + expect(urlSchemeValidator).not.toMatchString('https'); + expect(urlSchemeValidator).not.toMatchString('http://', { + exactString: false, + substring: 'http', + }); + expect(urlSchemeValidator).not.toMatchString('http://'); + expect(urlSchemeValidator).not.toMatchString('ft'); + expect(urlSchemeValidator).not.toMatchString('httpsftpmailtoirc'); +}); + +test('urlSchemeFinder', () => { + expect(urlSchemeFinder).toMatchAllGroups( + 'The best way to get data for interactive apps is "http:" and not "ftp:."', + [['http:'], ['ftp:']], + ); +}); + +test('urlAuthorityValidator', () => { + expect(urlAuthorityValidator).toMatchString('abba@a'); + expect(urlAuthorityValidator).not.toMatchString('abba@'); + expect(urlAuthorityValidator).not.toMatchString('@'); + expect(urlAuthorityValidator).not.toMatchString('@aa.aa.aa.aa.aa.aa'); + expect(urlAuthorityValidator).toMatchString('aa'); + expect(urlAuthorityValidator).toMatchString('aaa'); + expect(urlAuthorityValidator).toMatchString('aa.aa'); + expect(urlAuthorityValidator).toMatchString('aa.aa.aa'); + expect(urlAuthorityValidator).toMatchString('aa.aa.aa.aa.aa.aa'); + expect(urlAuthorityValidator).toMatchString('abba@aa.aa.aa.aa.aa.aa'); + expect(urlAuthorityValidator).toMatchString('aaaa.aaaa'); + expect(urlAuthorityValidator).toMatchString('aaaaaaaaaa'); + expect(urlAuthorityValidator).toMatchString('aaaa.aaaaaaa'); + expect(urlAuthorityValidator).toMatchString('abba@aaaa.aaaa'); + expect(urlAuthorityValidator).toMatchString('abba@aaaaaaaaaa'); + expect(urlAuthorityValidator).toMatchString('abba@aaaa.aaaaaaa'); + expect(urlAuthorityValidator).toMatchString('www.example.com'); +}); + +test('urlHostValidator', () => { + expect(urlHostValidator).toMatchString('www.google.com'); + expect(urlHostValidator).toMatchString('www.google'); + expect(urlHostValidator).toMatchString('google.com'); + expect(urlHostValidator).not.toMatchString('ftp.data.com.'); + expect(urlHostValidator).toMatchString('a'); + expect(urlHostValidator).toMatchString('a.a'); + expect(urlHostValidator).toMatchString('a.a.a'); + expect(urlHostValidator).toMatchString('a.a.a.a.a.a'); + expect(urlHostValidator).not.toMatchString('a.'); + expect(urlHostValidator).not.toMatchString('a.a.a.a.a.a.'); + expect(urlHostValidator).toMatchString('localhost'); +}); + +test('urlHost', () => { + expect(urlHost).toMatchString('www'); +}); + +test('urlAuthorityFinder', () => { + expect(urlAuthorityFinder).toMatchString('abba@a'); +}); + +let longPath100 = '/a'; +for (let i = 0; i < 100; i++) { + longPath100 += '/a'; +} + +let longPath256 = '/a'; +for (let i = 0; i < 256; i++) { + longPath256 += '/a'; +} + +test('urlPathValidator', () => { + expect(urlPathValidator).toMatchString('/a'); + expect(urlPathValidator).toMatchString('/a/a'); + expect(urlPathValidator).toMatchString('/a/a/a'); + expect(urlPathValidator).toMatchString('/a/a/a/a/a/a'); + expect(urlPathValidator).toMatchString('/a/'); + expect(urlPathValidator).toMatchString('/a/a/a/a/a/a/'); + expect(urlPathValidator).toMatchString('/a/a/a/a/a/a/a'); + expect(urlPathValidator).toMatchString('/a/a/a/a/a/a/a/a'); + expect(urlPathValidator).toMatchString('/a/a/a/a/a/a/a/a/a'); + expect(urlPathValidator).toMatchString('/a/a/a/a/a/a/a/a/a/a'); + expect(urlPathValidator).not.toMatchString('a'); + expect(urlPathValidator).toMatchString('/'); + expect(urlPathValidator).toMatchString('//'); + expect(urlPathValidator).not.toMatchString('a/'); + expect(urlPathValidator).not.toMatchString( + '/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + ); + expect(urlPathValidator).toMatchString(longPath100); + expect(urlPathValidator).not.toMatchString(longPath256); +}); + +test('urlQueryValidator', () => { + expect(urlQueryValidator).not.toMatchString('?'); + expect(urlQueryValidator).not.toMatchString('?a'); + expect(urlQueryValidator).not.toMatchString('?a='); + expect(urlQueryValidator).toMatchString('?a=b'); + expect(urlQueryValidator).toMatchString('?a=b&c=d'); + expect(urlQueryValidator).toMatchString('?a=b&c=d;e=f'); + expect(urlQueryValidator).toMatchString('?a=b;c=d'); + expect(urlQueryValidator).not.toMatchString('?a=b+c=d'); + expect(urlQueryValidator).not.toMatchString('a=b'); + expect(urlQueryValidator).toMatchString('?bar=baz'); + expect(urlQueryValidator).toMatchString('?bar=baz&inga=42'); + expect(urlQueryValidator).not.toMatchString('?bar=baz&inga=42&quux'); + expect(urlQueryValidator).toMatchString('?q=TestURL-encodedstuff'); +}); + +test('urlFragmentValidator', () => { + expect(urlFragmentValidator).not.toMatchString('#'); + expect(urlFragmentValidator).toMatchString('#a'); + expect(urlFragmentValidator).toMatchString('#bar'); + expect(urlFragmentValidator).toMatchString('#xpointer(//Rube)'); +}); + +const urlShouldMatch = [ + 'http://foo.com/blah_blah', + 'http://foo.com/blah_blah/', + 'http://foo.com/blah_blah_(wikipedia)', + 'http://foo.com/blah_blah_(wikipedia)_(again)', + 'http://www.example.com/wpstyle/?p=364', + 'http://odf.ws/123', + 'http://userid@example.com', + 'http://userid@example.com/', + 'http://userid@example.com:8080', + 'http://userid@example.com:8080/', + 'http://142.42.1.1/', + 'http://142.42.1.1:8080/', + 'http://foo.com/blah_(wikipedia)#cite-1', + 'http://foo.com/blah_(wikipedia)_blah#cite-1', + 'http://foo.com/unicode_(o)_in_parens', + 'http://foo.com/(something)?after=parens', + 'http://o.damowmow.com/', + 'http://code.google.com/events/#&product=browser', + 'http://j.mp', + 'ftp://foo.bar/baz', + 'http://foo.bar/?q=TestURL-encodedstuff', + 'http://1337.net', + 'http://a.b-c.de', + 'http://223.255.255.254', + 'rdar://1234', + 'ftps://foo.bar/', + 'http://-a.b.co', + 'http://0.0.0.0', + 'http://10.1.1.0', + 'http://10.1.1.255', + 'http://224.1.1.1', + 'http://1.1.1.1.1', + 'http://123.123.123', + 'http://3628126748', + 'http://10.1.1.1', + 'http://10.1.1.254', +]; + +const urlShouldNotMatch = [ + 'www.google.com', + 'google.com', + 'http://', + 'http://.', + 'http://..', + 'http://../', + 'http://?', + 'http://??', + 'http://??/', + 'http://#', + 'http://##', + 'http://##/', + 'http://foo.bar?q=Spaces should be encoded', + 'http://⌘.ws', + 'http://⌘.ws/', + 'http:///a', + '//', + '//a', + '///a', + '///', + 'foo.com', + 'h://test', + 'http:// shouldfail.com', + ':// should fail', + 'http://foo.bar/foo(bar)baz quux', + 'http://.www.foo.bar/', + 'http://www.foo.bar./', + 'http://.www.foo.bar./', + 'https://www.example.com/foo/?bar=baz&inga=42&quux', +]; + +test('urlValidator', () => { + for (const url of urlShouldMatch) { + expect(url).toMatch(urlValidator); + } + for (const url of urlShouldNotMatch) { + expect(urlValidator).not.toMatchString(url); + } +}); diff --git a/src/patterns/atoms.ts b/src/patterns/atoms.ts new file mode 100644 index 0000000..3644bf6 --- /dev/null +++ b/src/patterns/atoms.ts @@ -0,0 +1,8 @@ +import { charClass, charRange, digit } from '../constructs/character-class'; + +export const lowercase = charRange('a', 'z'); +export const uppercase = charRange('A', 'Z'); +export const alphabetical = charClass(lowercase, uppercase); +export const lowerCaseHexDigit = charClass(digit, charRange('a', 'f')); +export const upperCaseHexDigit = charClass(digit, charRange('A', 'F')); +export const hexDigit = charClass(digit, charRange('a', 'f'), charRange('A', 'F')); diff --git a/src/patterns/hex-color.ts b/src/patterns/hex-color.ts index 1a1a50c..5a2701e 100644 --- a/src/patterns/hex-color.ts +++ b/src/patterns/hex-color.ts @@ -1,18 +1,16 @@ import { buildRegExp } from '../builders'; import { endOfString, startOfString, wordBoundary } from '../constructs/anchors'; -import { charClass, charRange, digit } from '../constructs/character-class'; import { choiceOf } from '../constructs/choice-of'; import { repeat } from '../constructs/repeat'; - -const hexDigit = charClass(digit, charRange('a', 'f')); +import { lowerCaseHexDigit } from './atoms'; /** Find hex color strings in a text. */ export const hexColorFinder = buildRegExp( [ '#', choiceOf( - repeat(hexDigit, 6), // #rrggbb - repeat(hexDigit, 3), // #rgb + repeat(lowerCaseHexDigit, 6), // #rrggbb + repeat(lowerCaseHexDigit, 3), // #rgb ), wordBoundary, ], @@ -29,8 +27,8 @@ export const hexColorValidator = buildRegExp( startOfString, // Match whole string '#', choiceOf( - repeat(hexDigit, 6), // #rrggbb - repeat(hexDigit, 3), // #rgb + repeat(lowerCaseHexDigit, 6), // #rrggbb + repeat(lowerCaseHexDigit, 3), // #rgb ), endOfString, ], diff --git a/src/patterns/index.ts b/src/patterns/index.ts index 3222ce7..fc33267 100644 --- a/src/patterns/index.ts +++ b/src/patterns/index.ts @@ -1 +1,10 @@ export { hexColorFinder, hexColorValidator } from './hex-color'; +export { url, urlFinder, urlValidator } from './url-pattern'; +export { urlHost, urlHostFinder, urlHostValidator } from './url-pattern'; +export { urlScheme, urlSchemeFinder, urlSchemeValidator } from './url-pattern'; +export { urlAuthority, urlAuthorityFinder, urlAuthorityValidator } from './url-pattern'; +export { urlPath, urlPathFinder, urlPathValidator } from './url-pattern'; +export { urlQuery, urlQueryFinder, urlQueryValidator } from './url-pattern'; +export { urlFragment, urlFragmentFinder, urlFragmentValidator } from './url-pattern'; +export { ipv4DigitValidator, ipv4Finder, ipv4Validator } from './ip-addr'; +export { ipv6Finder, ipv6Validator, ipv6GroupValidator } from './ip-addr'; diff --git a/src/patterns/ip-addr.ts b/src/patterns/ip-addr.ts new file mode 100644 index 0000000..b733344 --- /dev/null +++ b/src/patterns/ip-addr.ts @@ -0,0 +1,105 @@ +// +// A Regular Expression to validate ipv4 and ipv6 addresses. +// + +// +// ipv4 Address +// Format: xxx.xxx.xxx.xxx e.g.(172.16.254.1) +// Source (IP V4): https://datatracker.ietf.org/doc/html/rfc791 +// + +import { buildRegExp } from '../builders'; +import { endOfString, startOfString } from '../constructs/anchors'; +import { capture } from '../constructs/capture'; +import { charRange } from '../constructs/character-class'; +import { choiceOf } from '../constructs/choice-of'; +import { optional } from '../constructs/quantifiers'; +import { regex } from '../constructs/regex'; +import { repeat } from '../constructs/repeat'; + +import { hexDigit } from './atoms'; + +export const ipv6Group = repeat(hexDigit, { min: 1, max: 4, greedy: false }); +export const ipv6GroupValidator = buildRegExp([startOfString, ipv6Group, endOfString]); +const ipv4Seperator = '.'; +const ipv6Seperator = ':'; + +export const ipDigit = choiceOf( + regex(['2', '5', charRange('0', '5')]), + regex(['2', charRange('0', '4'), charRange('0', '9')]), + regex([charRange('0', '1'), charRange('0', '9'), charRange('0', '9')]), + regex([charRange('0', '9'), charRange('0', '9')]), + regex([charRange('0', '9')]), +); + +export const ipv4DigitValidator = buildRegExp([startOfString, regex(ipDigit), endOfString]); + +export const ipv4Address = regex([ + ipDigit, + ipv4Seperator, + ipDigit, + ipv4Seperator, + ipDigit, + ipv4Seperator, + ipDigit, +]); + +export const ipv4Finder = buildRegExp([capture(ipv4Address)], { + ignoreCase: true, + global: true, +}); + +export const ipv4Validator = buildRegExp([startOfString, capture(ipv4Address), endOfString], { + ignoreCase: true, +}); + +/*** + *** ipv6 Address + *** Source: https://www.rfc-editor.org/rfc/rfc4291.html + *** + *** Form 1: + *** ABCD:EF01:2345:6789:ABCD:EF01:2345:6789 + *** 2001:DB8:0:0:8:800:200C:417A (collapsed leading zeros) + *** + *** Form 2: + *** The following valid addresses: + *** 2001:DB8:0:0:8:800:200C:417A a unicast address + *** FF01:0:0:0:0:0:0:101 a multicast address + *** 0:0:0:0:0:0:0:1 the loopback address + *** 0:0:0:0:0:0:0:0 the unspecified address + *** + *** Can be represented as the following collapsed valid addresses: + *** 2001:DB8::8:800:200C:417A a unicast address + *** FF01::101 a multicast address + *** ::1 the loopback address + *** :: the unspecified address + ***/ + +export const ipv6Address = regex([ + optional(ipv6Group), + ipv6Seperator, + optional(ipv6Group), + ipv6Seperator, + optional(ipv6Group), + optional(ipv6Seperator), + optional(ipv6Group), + optional(ipv6Seperator), + optional(ipv6Group), + optional(ipv6Seperator), + optional(ipv6Group), + optional(ipv6Seperator), + optional(ipv6Group), + optional(ipv6Seperator), + optional(ipv6Group), + optional(ipv6Seperator), + optional(ipv6Group), +]); + +export const ipv6Finder = buildRegExp([capture(ipv6Address)], { + ignoreCase: true, + global: true, +}); + +export const ipv6Validator = buildRegExp([startOfString, capture(ipv6Address), endOfString], { + ignoreCase: true, +}); diff --git a/src/patterns/url-pattern.ts b/src/patterns/url-pattern.ts new file mode 100644 index 0000000..a2730a5 --- /dev/null +++ b/src/patterns/url-pattern.ts @@ -0,0 +1,221 @@ +// +// A Regular Expression to validate URLs and to match URL components. +// This regular expression is based on the RFC 3986 standard which defines the URL format. +// +// URL = Scheme ":"["//" Authority]Path["?" Query]["#" Fragment] +// +// This pattern is designed to match WEB URLs, and does not support all possible URL schemes. +// Source: https://en.wikipedia.org/wiki/URL#External_links +// Source: https://datatracker.ietf.org/doc/html/rfc1738 +// Source: https://datatracker.ietf.org/doc/html/rfc3986 +// +// Current limitations: +// - Does not support URL Authorities with passwords. This pattern is generally considered deprecated. +// - URLs of the form 'http://' are considered valid. This is not a valid URL, but it is a valid URI. +// - Only URL Queries of the form 'x=y' are considered valid. Single values without an equals sign are not supported. +// + +import { buildRegExp } from '../builders'; +import { endOfString, startOfString } from '../constructs/anchors'; +//import { capture } from '../constructs/capture'; +import { anyOf, charClass, digit } from '../constructs/character-class'; +import { choiceOf } from '../constructs/choice-of'; +import { lookahead } from '../constructs/lookahead'; +import { negativeLookahead } from '../constructs/negative-lookahead'; +import { oneOrMore, optional, zeroOrMore } from '../constructs/quantifiers'; +import { regex } from '../constructs/regex'; +import { repeat } from '../constructs/repeat'; + +import { alphabetical, lowercase, uppercase } from './atoms'; +import { ipv4Address, ipv6Address } from './ip-addr'; + +// Scheme: +// The scheme is the first part of the URL and defines the protocol to be used. +// Examples of popular schemes include http, https, ftp, mailto, file, data and irc. +// A URL string must be a scheme, followed by a colon, followed by a scheme-specific part. +// +const schemeSpecialChars = anyOf('+-.'); +const schemeChar = charClass(alphabetical, digit, schemeSpecialChars); +const schemeSeperator = ':'; + +const schemePlural = regex([ + alphabetical, + repeat(schemeChar, { min: 2, max: 4, greedy: false }), + 's', +]); + +const schemeSingular = regex([alphabetical, repeat(schemeChar, { min: 2, max: 5, greedy: false })]); + +export const urlScheme = buildRegExp([ + choiceOf(schemePlural, schemeSingular), + lookahead(schemeSeperator), +]); + +export const urlSchemeFinder = buildRegExp([urlScheme, schemeSeperator], { + ignoreCase: false, + global: true, +}); + +export const urlSchemeValidator = buildRegExp( + [startOfString, urlScheme, schemeSeperator, endOfString], + { + ignoreCase: false, + global: false, + }, +); + +// Authority: +// The authority part of a URL consists of three sub-parts: +// 1. An optional username, followed by an at symbol (@) +// 2. A hostname (e.g. www.google.com) +// 3. An optional port number, preceded by a colon (:) +// Authority = [userinfo "@"] host [":" port] +// +const userinfoSpecialChars = anyOf('._%+-'); +const userinfoChars = charClass(lowercase, digit, userinfoSpecialChars); +const userinfo = oneOrMore(userinfoChars); +const userinfoSeperator = '@'; + +const hostnameSpecialChars = anyOf('-'); +const hostnameChars = charClass(lowercase, digit, hostnameSpecialChars); +const hostSeperator = '.'; +const port = repeat(digit, { min: 1, max: 5, greedy: false }); +const portSeperator = ':'; +const urlPort = regex([portSeperator, port]); +const host = repeat(hostnameChars, { min: 1, max: 63, greedy: true }); +const hostname = regex([host, repeat([hostSeperator, host], { min: 0, max: 255, greedy: false })]); +const authoritySeperator = '//'; + +export const urlAuthority = regex([ + optional([userinfo, userinfoSeperator]), + choiceOf(hostname, ipv4Address, ipv6Address), + optional(urlPort), +]); + +export const urlAuthorityFinder = buildRegExp(urlAuthority, { + ignoreCase: false, + global: true, +}); + +export const urlAuthorityValidator = buildRegExp([startOfString, urlAuthority, endOfString]); + +// +// Convenience Pattern - Host: +// A hostname (e.g. www.google.com) +// + +export const urlHost = choiceOf(hostname, ipv4Address, ipv6Address); +//export const urlHost = host; + +export const urlHostFinder = buildRegExp(hostname, { + ignoreCase: false, + global: true, +}); + +export const urlHostValidator = buildRegExp([startOfString, regex(hostname), endOfString], { + ignoreCase: false, + global: false, +}); + +// Path: +// The path is the part of the URL that comes after the authority and before the query. +// It consists of a sequence of path segments separated by a forward slash (/). +// A path string must begin with a forward slash (/). + +const pathSeparator = '/'; +const pathSpecialChars = anyOf('_-()+'); +const pathChar = charClass(lowercase, uppercase, digit, pathSpecialChars); +const pathSegment = regex([pathSeparator, repeat(pathChar, { min: 0, max: 63, greedy: false })]); + +export const urlPath = buildRegExp(repeat(pathSegment, { min: 0, max: 255, greedy: false })); + +export const urlPathFinder = buildRegExp(urlPath, { + ignoreCase: false, + global: true, +}); + +export const urlPathValidator = buildRegExp([startOfString, urlPath, endOfString], { + ignoreCase: false, + global: false, +}); + +// Query: +// The query part of a URL is optional and comes after the path. +// It is separated from the path by a question mark (?). +// The query string consists of a sequence of field-value pairs separated by an ampersand (&). +// Each field-value pair is separated by an equals sign (=). +// +const equals = '='; +const querySeparator = '?'; +const queryDelimiter = anyOf('&;'); +const queryChars = charClass(lowercase, uppercase, digit, anyOf('_-')); +const queryKey = oneOrMore(queryChars); +const queryValue = oneOrMore(queryChars); +const queryKeyValuePair = regex([queryKey, equals, queryValue]); + +export const urlQuery = regex([ + querySeparator, + queryKeyValuePair, + zeroOrMore([queryDelimiter, queryKeyValuePair]), +]); + +export const urlQueryFinder = buildRegExp(urlQuery, { + ignoreCase: false, + global: true, +}); + +export const urlQueryValidator = buildRegExp([startOfString, urlQuery, endOfString], { + ignoreCase: false, + global: false, +}); + +// Fragment: +// The fragment part of a URL is optional and comes after the query. +// It is separated from the query by a hash (#). +// The fragment string consists of a sequence of characters. + +const fragmentSeparator = '#'; +const fragmentSpecialChars = anyOf(":@%._+~=()/-&!$*;?,'"); +const fragmentChars = charClass(lowercase, uppercase, digit, fragmentSpecialChars); + +export const urlFragment = regex([fragmentSeparator, oneOrMore(fragmentChars)]); + +export const urlFragmentFinder = buildRegExp(urlFragment, { + ignoreCase: false, + global: true, +}); + +export const urlFragmentValidator = buildRegExp([startOfString, urlFragment, endOfString], { + ignoreCase: false, + global: false, +}); + +// +// These two patterns are needed to disambiguate between "http:/path" and "http://authority/path". +// "http://" is technically a valid URL: urlScheme = http, urlAuthority = null, urlPath = / +// By convention, an empty path is considered invalid, if it follows an empty authority. +// +const noAuthority = regex([pathSeparator, negativeLookahead(pathSeparator), urlPath]); +const hasAuthority = regex([authoritySeperator, urlAuthority, optional(urlPath)]); + +export const url = buildRegExp([ + urlScheme, + schemeSeperator, + choiceOf(noAuthority, hasAuthority), + optional(urlQuery), + optional(urlFragment), +]); + +/*** + *** Find URL strings in a text. + ***/ + +export const urlFinder = buildRegExp(url, { + global: true, +}); + +/*** + *** Check that given text is a valid URL. + ***/ + +export const urlValidator = buildRegExp([startOfString, url, endOfString]); diff --git a/test-utils/to-equal-regex.ts b/test-utils/to-equal-regex.ts index 786bbd4..4ada7c7 100644 --- a/test-utils/to-equal-regex.ts +++ b/test-utils/to-equal-regex.ts @@ -19,11 +19,11 @@ export function toEqualRegex( pass: expectedSource === received.source && (expectedFlags === undefined || expectedFlags === received.flags), - message: () => - this.utils.matcherHint('toEqualRegex', undefined, undefined, options) + - '\n\n' + - `Expected: ${this.isNot ? 'not ' : ''}${this.utils.printExpected(expected)}\n` + - `Received: ${this.utils.printReceived(received)}`, + message: () => ` + ${this.utils.matcherHint('toEqualRegex', undefined, undefined, options)}\n\n + Expected: ${this.isNot ? 'not ' : ''}${this.utils.printExpected(expected)}\n + Received: ${this.utils.printReceived(received)} + `, }; } diff --git a/test-utils/to-match-all-groups.ts b/test-utils/to-match-all-groups.ts index 9023183..7df4bfe 100644 --- a/test-utils/to-match-all-groups.ts +++ b/test-utils/to-match-all-groups.ts @@ -9,17 +9,14 @@ export function toMatchAllGroups( ) { const receivedRegex = wrapRegExp(received); const receivedGroups = toNestedArray(expectedString.matchAll(receivedRegex)); - const options = { - isNot: this.isNot, - }; return { pass: this.equals(receivedGroups, expectedGroups), - message: () => - this.utils.matcherHint('toMatchGroups', undefined, undefined, options) + - '\n\n' + - `Expected: ${this.isNot ? 'not ' : ''}${this.utils.printExpected(expectedGroups)}\n` + - `Received: ${this.utils.printReceived(receivedGroups)}`, + message: () => ` + this.utils.matcherHint('toMatchGroups', undefined, undefined, options)\n\n + Expected: ${this.isNot ? 'not ' : ''}${this.utils.printExpected(expectedGroups)}\n + Received: ${this.utils.printReceived(receivedGroups)} + `, }; } diff --git a/test-utils/to-match-all-named-groups.ts b/test-utils/to-match-all-named-groups.ts index d3c3d02..b3db121 100644 --- a/test-utils/to-match-all-named-groups.ts +++ b/test-utils/to-match-all-named-groups.ts @@ -16,11 +16,10 @@ export function toMatchAllNamedGroups( return { pass: this.equals(receivedGroups, expectedGroups), - message: () => - this.utils.matcherHint('toMatchGroups', undefined, undefined, options) + - '\n\n' + - `Expected: ${this.isNot ? 'not ' : ''}${this.utils.printExpected(expectedGroups)}\n` + - `Received: ${this.utils.printReceived(receivedGroups)}`, + message: () => ` + ${this.utils.matcherHint('toMatchGroups', undefined, undefined, options)}\n\n + Expected: ${this.isNot ? 'not ' : ''}${this.utils.printExpected(expectedGroups)}\n + Received: ${this.utils.printReceived(receivedGroups)}`, }; } diff --git a/test-utils/to-match-groups.ts b/test-utils/to-match-groups.ts index 6feb6f5..92a710d 100644 --- a/test-utils/to-match-groups.ts +++ b/test-utils/to-match-groups.ts @@ -10,17 +10,18 @@ export function toMatchGroups( const receivedRegex = wrapRegExp(received); const matchResult = inputText.match(receivedRegex); const receivedGroups = matchResult ? [...matchResult] : null; + const options = { isNot: this.isNot, }; return { pass: this.equals(receivedGroups, expectedGroups), - message: () => - this.utils.matcherHint('toMatchGroups', undefined, undefined, options) + - '\n\n' + - `Expected: ${this.isNot ? 'not ' : ''}${this.utils.printExpected(expectedGroups)}\n` + - `Received: ${this.utils.printReceived(receivedGroups)}`, + message: () => ` + ${this.utils.matcherHint('toMatchGroups', undefined, undefined, options)}\n\n + Expected: ${this.isNot ? 'not ' : ''}${this.utils.printExpected(expectedGroups)}\n + Received: ${this.utils.printReceived(receivedGroups)} + `, }; } diff --git a/test-utils/to-match-named-groups.ts b/test-utils/to-match-named-groups.ts index b844a58..c519547 100644 --- a/test-utils/to-match-named-groups.ts +++ b/test-utils/to-match-named-groups.ts @@ -16,11 +16,11 @@ export function toMatchNamedGroups( return { pass: this.equals(receivedGroups, expectedGroups), - message: () => - this.utils.matcherHint('toMatchGroups', undefined, undefined, options) + - '\n\n' + - `Expected: ${this.isNot ? 'not ' : ''}${this.utils.printExpected(expectedGroups)}\n` + - `Received: ${this.utils.printReceived(receivedGroups)}`, + message: () => ` + ${this.utils.matcherHint('toMatchGroups', undefined, undefined, options)}\n\n + Expected: ${this.isNot ? 'not ' : ''}${this.utils.printExpected(expectedGroups)}\n + Received: ${this.utils.printReceived(receivedGroups)} + `, }; } diff --git a/test-utils/to-match-string.ts b/test-utils/to-match-string.ts index 0bae22d..a316d6b 100644 --- a/test-utils/to-match-string.ts +++ b/test-utils/to-match-string.ts @@ -1,24 +1,40 @@ import type { RegexSequence } from '../src/types'; import { wrapRegExp } from './utils'; +interface MatchTypeOptions { + exactString: boolean; + substring?: string; +} + export function toMatchString( this: jest.MatcherContext, received: RegExp | RegexSequence, expected: string, + matchType?: MatchTypeOptions, ) { const receivedRegex = wrapRegExp(received); const matchResult = expected.match(receivedRegex); + + let pass: boolean = false; + if (matchType === undefined) pass = matchResult !== null && matchResult[0] === expected; + else if (matchType.exactString) pass = matchResult !== null && matchResult[0] === expected; + else if (typeof matchType.substring === 'string') { + pass = matchResult !== null && matchResult[0].includes(matchType.substring); + } else { + pass = matchResult !== null; + } + const options = { isNot: this.isNot, }; return { - pass: matchResult !== null, - message: () => - this.utils.matcherHint('toMatchString', undefined, undefined, options) + - '\n\n' + - `Expected: ${this.isNot ? 'not ' : ''} matching ${this.utils.printExpected(expected)}\n` + - `Received pattern: ${this.utils.printReceived(receivedRegex.source)}`, + pass: pass, + message: () => ` + ${this.utils.matcherHint('toMatchString', undefined, undefined, options)}\n\n + Expected: ${this.isNot ? 'not ' : ''} matching ${this.utils.printExpected(expected)}\n + Received pattern: ${this.utils.printReceived(receivedRegex.source)} + `, }; } @@ -28,7 +44,7 @@ declare global { namespace jest { // eslint-disable-next-line @typescript-eslint/no-unused-vars interface Matchers { - toMatchString(expected: string): R; + toMatchString(expected: string, matchType?: MatchTypeOptions): R; } } }