From af3a2bdfdb688ced3dc5eb5f81c006a737dc1258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 21 Dec 2023 22:36:48 +0100 Subject: [PATCH 1/5] refactor: improve test structure using `expectPattern` --- package.json | 9 +++ src/components/__tests__/capture.test.tsx | 11 ++- .../__tests__/character-class.test.ts | 68 +++++++++---------- src/components/__tests__/choice-of.test.ts | 27 ++++---- src/components/__tests__/quantifiers.test.tsx | 31 +++++---- src/components/__tests__/repeat.test.tsx | 22 +++--- src/components/character-class.ts | 12 ++-- src/test-utils.ts | 8 ++- 8 files changed, 101 insertions(+), 87 deletions(-) diff --git a/package.json b/package.json index 7a9f5cc..a27e959 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,15 @@ "ignoreDeclarationSort": true } ], + "jest/expect-expect": [ + "error", + { + "assertFunctionNames": [ + "expect", + "expectPattern" + ] + } + ], "import/order": "error" } }, diff --git a/src/components/__tests__/capture.test.tsx b/src/components/__tests__/capture.test.tsx index 02b166d..b756213 100644 --- a/src/components/__tests__/capture.test.tsx +++ b/src/components/__tests__/capture.test.tsx @@ -1,13 +1,12 @@ -import { buildPattern } from '../../builders'; import { capture } from '../capture'; import { oneOrMore } from '../quantifiers'; -import { execRegex } from '../../test-utils'; +import { execRegex, expectPattern } from '../../test-utils'; test('"capture" base cases', () => { - expect(buildPattern(capture('a'))).toBe('(a)'); - expect(buildPattern(capture('abc'))).toBe('(abc)'); - expect(buildPattern(capture(oneOrMore('abc')))).toBe('((?:abc)+)'); - expect(buildPattern(oneOrMore(capture('abc')))).toBe('(abc)+'); + expectPattern(capture('a')).toBe('(a)'); + expectPattern(capture('abc')).toBe('(abc)'); + expectPattern(capture(oneOrMore('abc'))).toBe('((?:abc)+)'); + expectPattern(oneOrMore(capture('abc'))).toBe('(abc)+'); }); test('"capture" captures group', () => { diff --git a/src/components/__tests__/character-class.test.ts b/src/components/__tests__/character-class.test.ts index f73481b..0cab3be 100644 --- a/src/components/__tests__/character-class.test.ts +++ b/src/components/__tests__/character-class.test.ts @@ -1,5 +1,4 @@ -import { buildPattern } from '../../builders'; -import { one, oneOrMore } from '../quantifiers'; +import { oneOrMore, optionally, zeroOrMore } from '../quantifiers'; import { any, anyOf, @@ -9,57 +8,52 @@ import { whitespace, word, } from '../character-class'; -import { execRegex } from '../../test-utils'; +import { execRegex, expectPattern } from '../../test-utils'; -test('"whitespace" character class', () => { - expect(buildPattern(whitespace)).toEqual(`\\s`); - - expect(buildPattern(one('ab'), whitespace)).toEqual(`ab\\s`); - - expect(buildPattern(one('ab'), whitespace, one('c'))).toEqual(`ab\\sc`); +test('"any" character class', () => { + expectPattern(any).toBe('.'); + expectPattern('x', any).toBe('x.'); + expectPattern('x', any, 'x').toBe('x.x'); }); test('"digit" character class', () => { - expect(buildPattern(digit)).toEqual(`\\d`); - - expect(buildPattern(one('ab'), digit)).toEqual(`ab\\d`); - - expect(buildPattern(one('ab'), digit, one('c'))).toEqual(`ab\\dc`); + expectPattern(digit).toBe('\\d'); + expectPattern('x', digit).toBe('x\\d'); + expectPattern('x', digit, 'x').toBe('x\\dx'); }); test('"word" character class', () => { - expect(buildPattern(word)).toEqual(`\\w`); - - expect(buildPattern(one('ab'), word)).toEqual(`ab\\w`); - - expect(buildPattern(one('ab'), word, one('c'))).toEqual(`ab\\wc`); + expectPattern(word).toBe('\\w'); + expectPattern('x', word).toBe('x\\w'); + expectPattern('x', word, 'x').toBe('x\\wx'); }); -test('"any" character class', () => { - expect(buildPattern(any)).toEqual(`.`); - - expect(buildPattern(one('ab'), any)).toEqual(`ab.`); - - expect(buildPattern(one('ab'), any, one('c'))).toEqual(`ab.c`); +test('"whitespace" character class', () => { + expectPattern(whitespace).toBe('\\s'); + expectPattern('x', whitespace).toBe('x\\s'); + expectPattern('x', whitespace, 'x').toBe('x\\sx'); }); test('"anyOf" base cases', () => { - expect(buildPattern(anyOf('a'))).toBe('a'); - expect(buildPattern(anyOf('abc'))).toBe('[abc]'); + expectPattern(anyOf('a')).toBe('a'); + expectPattern('x', anyOf('a'), 'x').toBe('xax'); + expectPattern(anyOf('ab')).toBe('[ab]'); + expectPattern('x', anyOf('ab')).toBe('x[ab]'); + expectPattern('x', anyOf('ab'), 'x').toBe('x[ab]x'); }); -test('"anyOf" in context', () => { - expect(buildPattern('x', anyOf('a'), 'x')).toBe('xax'); - expect(buildPattern('x', anyOf('abc'), 'x')).toBe('x[abc]x'); - expect(buildPattern('x', oneOrMore(anyOf('abc')), 'x')).toBe('x[abc]+x'); +test('"anyOf" with quantifiers', () => { + expectPattern('x', oneOrMore(anyOf('abc')), 'x').toBe('x[abc]+x'); + expectPattern('x', optionally(anyOf('abc')), 'x').toBe('x[abc]?x'); + expectPattern('x', zeroOrMore(anyOf('abc')), 'x').toBe('x[abc]*x'); }); test('"anyOf" escapes special characters', () => { - expect(buildPattern(anyOf('abc-+.'))).toBe('[-abc\\+\\.]'); + expectPattern(anyOf('abc-+.')).toBe('[-abc\\+\\.]'); }); test('"anyOf" moves hyphen to the first position', () => { - expect(buildPattern(anyOf('a-bc'))).toBe('[-abc]'); + expectPattern(anyOf('a-bc')).toBe('[-abc]'); }); test('"anyOf" throws on empty text', () => { @@ -69,13 +63,13 @@ test('"anyOf" throws on empty text', () => { }); test('"inverted" character class', () => { - expect(buildPattern(inverted(anyOf('a')))).toBe('[^a]'); - expect(buildPattern(inverted(anyOf('abc')))).toBe('[^abc]'); + expectPattern(inverted(anyOf('a'))).toBe('[^a]'); + expectPattern(inverted(anyOf('abc'))).toBe('[^abc]'); }); test('"inverted" character class double inversion', () => { - expect(buildPattern(inverted(inverted(anyOf('a'))))).toBe('a'); - expect(buildPattern(inverted(inverted(anyOf('abc'))))).toBe('[abc]'); + expectPattern(inverted(inverted(anyOf('a')))).toBe('a'); + expectPattern(inverted(inverted(anyOf('abc')))).toBe('[abc]'); }); test('"inverted" character class execution', () => { diff --git a/src/components/__tests__/choice-of.test.ts b/src/components/__tests__/choice-of.test.ts index 3748dab..8474ef0 100644 --- a/src/components/__tests__/choice-of.test.ts +++ b/src/components/__tests__/choice-of.test.ts @@ -2,28 +2,29 @@ import { buildPattern } from '../../builders'; import { oneOrMore, zeroOrMore } from '../quantifiers'; import { repeat } from '../repeat'; import { choiceOf } from '../choice-of'; +import { expectPattern } from '../../test-utils'; test('"choiceOf" using basic strings', () => { - expect(buildPattern(choiceOf('a'))).toEqual('a'); - expect(buildPattern(choiceOf('a', 'b'))).toEqual('a|b'); - expect(buildPattern(choiceOf('a', 'b', 'c'))).toEqual('a|b|c'); - expect(buildPattern(choiceOf('aaa', 'bbb'))).toEqual('aaa|bbb'); + expectPattern(choiceOf('a')).toBe('a'); + expectPattern(choiceOf('a', 'b')).toBe('a|b'); + expectPattern(choiceOf('a', 'b', 'c')).toBe('a|b|c'); + expectPattern(choiceOf('aaa', 'bbb')).toBe('aaa|bbb'); }); test('"choiceOf" used in sequence', () => { - expect(buildPattern('x', choiceOf('a'), 'x')).toEqual('xax'); - expect(buildPattern(choiceOf('a', 'b'), 'x')).toEqual('(?:a|b)x'); - expect(buildPattern('x', choiceOf('a', 'b'))).toEqual('x(?:a|b)'); + expectPattern('x', choiceOf('a'), 'x').toBe('xax'); + expectPattern(choiceOf('a', 'b'), 'x').toBe('(?:a|b)x'); + expectPattern('x', choiceOf('a', 'b')).toBe('x(?:a|b)'); - expect(buildPattern(choiceOf('a', 'b', 'c'))).toEqual('a|b|c'); - expect(buildPattern('x', choiceOf('a', 'b', 'c'))).toEqual('x(?:a|b|c)'); - expect(buildPattern(choiceOf('a', 'b', 'c'), 'x')).toEqual('(?:a|b|c)x'); + expectPattern(choiceOf('a', 'b', 'c')).toBe('a|b|c'); + expectPattern('x', choiceOf('a', 'b', 'c')).toBe('x(?:a|b|c)'); + expectPattern(choiceOf('a', 'b', 'c'), 'x').toBe('(?:a|b|c)x'); - expect(buildPattern(choiceOf('aaa', 'bbb'))).toEqual('aaa|bbb'); + expectPattern(choiceOf('aaa', 'bbb')).toBe('aaa|bbb'); }); test('"choiceOf" using nested regex', () => { - expect(buildPattern(choiceOf(oneOrMore('a'), zeroOrMore('b')))).toBe('a+|b*'); + expectPattern(choiceOf(oneOrMore('a'), zeroOrMore('b'))).toBe('a+|b*'); expect( buildPattern( choiceOf(repeat({ min: 1, max: 3 }, 'a'), repeat({ count: 5 }, 'bx')) @@ -31,7 +32,7 @@ test('"choiceOf" using nested regex', () => { ).toBe('a{1,3}|(?:bx){5}'); }); -test('`anyOf` throws on empty options', () => { +test('`choiceOf` throws on empty options', () => { expect(() => choiceOf()).toThrowErrorMatchingInlineSnapshot( `"\`choiceOf\` should receive at least one option"` ); diff --git a/src/components/__tests__/quantifiers.test.tsx b/src/components/__tests__/quantifiers.test.tsx index f2fdddb..3563241 100644 --- a/src/components/__tests__/quantifiers.test.tsx +++ b/src/components/__tests__/quantifiers.test.tsx @@ -1,25 +1,26 @@ -import { buildPattern, buildRegex } from '../../builders'; +import { buildRegex } from '../../builders'; +import { expectPattern } from '../../test-utils'; import { digit } from '../character-class'; import { one, oneOrMore, optionally, zeroOrMore } from '../quantifiers'; test('"oneOrMore" quantifier', () => { - expect(buildPattern(oneOrMore('a'))).toEqual('a+'); - expect(buildPattern(oneOrMore('ab'))).toEqual('(?:ab)+'); + expectPattern(oneOrMore('a')).toBe('a+'); + expectPattern(oneOrMore('ab')).toBe('(?:ab)+'); }); test('"one" quantifier', () => { - expect(buildPattern(one('a'))).toEqual('a'); - expect(buildPattern(one('ab'))).toEqual('ab'); + expectPattern(one('a')).toBe('a'); + expectPattern(one('ab')).toBe('ab'); }); test('"optionally" quantifier', () => { - expect(buildPattern(optionally('a'))).toEqual('a?'); - expect(buildPattern(optionally('ab'))).toEqual('(?:ab)?'); + expectPattern(optionally('a')).toBe('a?'); + expectPattern(optionally('ab')).toBe('(?:ab)?'); }); test('"zeroOrMore" quantifier', () => { - expect(buildPattern(zeroOrMore('a'))).toEqual('a*'); - expect(buildPattern(zeroOrMore('ab'))).toEqual('(?:ab)*'); + expectPattern(zeroOrMore('a')).toBe('a*'); + expectPattern(zeroOrMore('ab')).toBe('(?:ab)*'); }); test('oneOrMore does not generate capture when grouping', () => { @@ -47,8 +48,12 @@ test('zeroOrMore does not generate capture when grouping', () => { }); test('base quantifiers optimize grouping for atoms', () => { - expect(buildPattern(one(digit))).toBe('\\d'); - expect(buildPattern(oneOrMore(digit))).toBe('\\d+'); - expect(buildPattern(optionally(digit))).toBe('\\d?'); - expect(buildPattern(zeroOrMore(digit))).toBe('\\d*'); + expectPattern(one(digit)).toBe('\\d'); + expectPattern(oneOrMore(digit)).toBe('\\d+'); + expectPattern(optionally(digit)).toBe('\\d?'); + expectPattern(zeroOrMore(digit)).toBe('\\d*'); + + expectPattern(oneOrMore('a')).toBe('a+'); + expectPattern(optionally('a')).toBe('a?'); + expectPattern(zeroOrMore('a')).toBe('a*'); }); diff --git a/src/components/__tests__/repeat.test.tsx b/src/components/__tests__/repeat.test.tsx index 0bb2225..24b187d 100644 --- a/src/components/__tests__/repeat.test.tsx +++ b/src/components/__tests__/repeat.test.tsx @@ -1,25 +1,25 @@ -import { buildPattern } from '../../builders'; +import { expectPattern } from '../../test-utils'; import { digit } from '../character-class'; import { oneOrMore, zeroOrMore } from '../quantifiers'; import { repeat } from '../repeat'; test('"repeat" quantifier', () => { - expect(buildPattern('a', repeat({ min: 1, max: 5 }, 'b'))).toEqual('ab{1,5}'); - expect(buildPattern('a', repeat({ min: 1 }, 'b'))).toEqual('ab{1,}'); - expect(buildPattern('a', repeat({ count: 1 }, 'b'))).toEqual('ab{1}'); + expectPattern('a', repeat({ min: 1, max: 5 }, 'b')).toBe('ab{1,5}'); + expectPattern('a', repeat({ min: 1 }, 'b')).toBe('ab{1,}'); + expectPattern('a', repeat({ count: 1 }, 'b')).toBe('ab{1}'); - expect(buildPattern('a', repeat({ count: 1 }, 'a', zeroOrMore('b')))).toEqual( + expectPattern('a', repeat({ count: 1 }, 'a', zeroOrMore('b'))).toBe( 'a(?:ab*){1}' ); - expect( - buildPattern(repeat({ count: 5 }, 'text', ' ', oneOrMore('d'))) - ).toEqual('(?:text d+){5}'); + expectPattern(repeat({ count: 5 }, 'text', ' ', oneOrMore('d'))).toBe( + '(?:text d+){5}' + ); }); test('"repeat"" optimizes grouping for atoms', () => { - expect(buildPattern(repeat({ count: 2 }, digit))).toBe('\\d{2}'); - expect(buildPattern(repeat({ min: 2 }, digit))).toBe('\\d{2,}'); - expect(buildPattern(repeat({ min: 1, max: 5 }, digit))).toBe('\\d{1,5}'); + expectPattern(repeat({ count: 2 }, digit)).toBe('\\d{2}'); + expectPattern(repeat({ min: 2 }, digit)).toBe('\\d{2,}'); + expectPattern(repeat({ min: 1, max: 5 }, digit)).toBe('\\d{1,5}'); }); test('`repeat` throws on no children', () => { diff --git a/src/components/character-class.ts b/src/components/character-class.ts index 1de3e6f..880030b 100644 --- a/src/components/character-class.ts +++ b/src/components/character-class.ts @@ -8,12 +8,6 @@ export const any: CharacterClass = { inverted: false, }; -export const whitespace: CharacterClass = { - type: 'characterClass', - characters: ['\\s'], - inverted: false, -}; - export const digit: CharacterClass = { type: 'characterClass', characters: ['\\d'], @@ -26,6 +20,12 @@ export const word: CharacterClass = { inverted: false, }; +export const whitespace: CharacterClass = { + type: 'characterClass', + characters: ['\\s'], + inverted: false, +}; + export function anyOf(characters: string): CharacterClass { const charactersArray = characters.split('').map(escapeText); if (charactersArray.length === 0) { diff --git a/src/test-utils.ts b/src/test-utils.ts index 520f235..31cabbb 100644 --- a/src/test-utils.ts +++ b/src/test-utils.ts @@ -1,4 +1,4 @@ -import { buildRegex } from './builders'; +import { buildPattern, buildRegex } from './builders'; import type { RegexElement } from './components/types'; export function execRegex( @@ -9,3 +9,9 @@ export function execRegex( const result = regex.exec(text); return result ? [...result] : null; } + +export function expectPattern(...elements: Array) { + const pattern = buildPattern(...elements); + // eslint-disable-next-line jest/valid-expect + return expect(pattern); +} From 1400fd4736693300d29f83c8d4295b61c92ae000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 21 Dec 2023 23:02:29 +0100 Subject: [PATCH 2/5] refactor: `toHavePattern` matcher --- src/components/__tests__/capture.test.tsx | 10 ++-- .../__tests__/character-class.test.ts | 54 +++++++++--------- src/components/__tests__/choice-of.test.ts | 33 +++++------ src/components/__tests__/quantifiers.test.tsx | 34 ++++++------ src/components/__tests__/repeat.test.tsx | 18 +++--- src/encoder/__tests__/encoder.test.tsx | 55 ++++++++++--------- src/test-utils.ts | 49 +++++++++++++++-- 7 files changed, 146 insertions(+), 107 deletions(-) diff --git a/src/components/__tests__/capture.test.tsx b/src/components/__tests__/capture.test.tsx index b756213..683b77a 100644 --- a/src/components/__tests__/capture.test.tsx +++ b/src/components/__tests__/capture.test.tsx @@ -1,12 +1,12 @@ import { capture } from '../capture'; import { oneOrMore } from '../quantifiers'; -import { execRegex, expectPattern } from '../../test-utils'; +import { execRegex } from '../../test-utils'; test('"capture" base cases', () => { - expectPattern(capture('a')).toBe('(a)'); - expectPattern(capture('abc')).toBe('(abc)'); - expectPattern(capture(oneOrMore('abc'))).toBe('((?:abc)+)'); - expectPattern(oneOrMore(capture('abc'))).toBe('(abc)+'); + expect(capture('a')).toHavePattern('(a)'); + expect(capture('abc')).toHavePattern('(abc)'); + expect(capture(oneOrMore('abc'))).toHavePattern('((?:abc)+)'); + expect(oneOrMore(capture('abc'))).toHavePattern('(abc)+'); }); test('"capture" captures group', () => { diff --git a/src/components/__tests__/character-class.test.ts b/src/components/__tests__/character-class.test.ts index 0cab3be..40d6946 100644 --- a/src/components/__tests__/character-class.test.ts +++ b/src/components/__tests__/character-class.test.ts @@ -8,52 +8,52 @@ import { whitespace, word, } from '../character-class'; -import { execRegex, expectPattern } from '../../test-utils'; +import { execRegex } from '../../test-utils'; test('"any" character class', () => { - expectPattern(any).toBe('.'); - expectPattern('x', any).toBe('x.'); - expectPattern('x', any, 'x').toBe('x.x'); + expect(any).toHavePattern('.'); + expect(['x', any]).toHavePattern('x.'); + expect(['x', any, 'x']).toHavePattern('x.x'); }); test('"digit" character class', () => { - expectPattern(digit).toBe('\\d'); - expectPattern('x', digit).toBe('x\\d'); - expectPattern('x', digit, 'x').toBe('x\\dx'); + expect(digit).toHavePattern('\\d'); + expect(['x', digit]).toHavePattern('x\\d'); + expect(['x', digit, 'x']).toHavePattern('x\\dx'); }); test('"word" character class', () => { - expectPattern(word).toBe('\\w'); - expectPattern('x', word).toBe('x\\w'); - expectPattern('x', word, 'x').toBe('x\\wx'); + expect(word).toHavePattern('\\w'); + expect(['x', word]).toHavePattern('x\\w'); + expect(['x', word, 'x']).toHavePattern('x\\wx'); }); test('"whitespace" character class', () => { - expectPattern(whitespace).toBe('\\s'); - expectPattern('x', whitespace).toBe('x\\s'); - expectPattern('x', whitespace, 'x').toBe('x\\sx'); + expect(whitespace).toHavePattern('\\s'); + expect(['x', whitespace]).toHavePattern('x\\s'); + expect(['x', whitespace, 'x']).toHavePattern('x\\sx'); }); test('"anyOf" base cases', () => { - expectPattern(anyOf('a')).toBe('a'); - expectPattern('x', anyOf('a'), 'x').toBe('xax'); - expectPattern(anyOf('ab')).toBe('[ab]'); - expectPattern('x', anyOf('ab')).toBe('x[ab]'); - expectPattern('x', anyOf('ab'), 'x').toBe('x[ab]x'); + expect(anyOf('a')).toHavePattern('a'); + expect(['x', anyOf('a'), 'x']).toHavePattern('xax'); + expect(anyOf('ab')).toHavePattern('[ab]'); + expect(['x', anyOf('ab')]).toHavePattern('x[ab]'); + expect(['x', anyOf('ab'), 'x']).toHavePattern('x[ab]x'); }); test('"anyOf" with quantifiers', () => { - expectPattern('x', oneOrMore(anyOf('abc')), 'x').toBe('x[abc]+x'); - expectPattern('x', optionally(anyOf('abc')), 'x').toBe('x[abc]?x'); - expectPattern('x', zeroOrMore(anyOf('abc')), 'x').toBe('x[abc]*x'); + expect(['x', oneOrMore(anyOf('abc')), 'x']).toHavePattern('x[abc]+x'); + expect(['x', optionally(anyOf('abc')), 'x']).toHavePattern('x[abc]?x'); + expect(['x', zeroOrMore(anyOf('abc')), 'x']).toHavePattern('x[abc]*x'); }); test('"anyOf" escapes special characters', () => { - expectPattern(anyOf('abc-+.')).toBe('[-abc\\+\\.]'); + expect(anyOf('abc-+.')).toHavePattern('[-abc\\+\\.]'); }); test('"anyOf" moves hyphen to the first position', () => { - expectPattern(anyOf('a-bc')).toBe('[-abc]'); + expect(anyOf('a-bc')).toHavePattern('[-abc]'); }); test('"anyOf" throws on empty text', () => { @@ -63,13 +63,13 @@ test('"anyOf" throws on empty text', () => { }); test('"inverted" character class', () => { - expectPattern(inverted(anyOf('a'))).toBe('[^a]'); - expectPattern(inverted(anyOf('abc'))).toBe('[^abc]'); + expect(inverted(anyOf('a'))).toHavePattern('[^a]'); + expect(inverted(anyOf('abc'))).toHavePattern('[^abc]'); }); test('"inverted" character class double inversion', () => { - expectPattern(inverted(inverted(anyOf('a')))).toBe('a'); - expectPattern(inverted(inverted(anyOf('abc')))).toBe('[abc]'); + expect(inverted(inverted(anyOf('a')))).toHavePattern('a'); + expect(inverted(inverted(anyOf('abc')))).toHavePattern('[abc]'); }); test('"inverted" character class execution', () => { diff --git a/src/components/__tests__/choice-of.test.ts b/src/components/__tests__/choice-of.test.ts index 8474ef0..0cf65f4 100644 --- a/src/components/__tests__/choice-of.test.ts +++ b/src/components/__tests__/choice-of.test.ts @@ -1,35 +1,32 @@ -import { buildPattern } from '../../builders'; import { oneOrMore, zeroOrMore } from '../quantifiers'; import { repeat } from '../repeat'; import { choiceOf } from '../choice-of'; -import { expectPattern } from '../../test-utils'; +import '../../test-utils'; test('"choiceOf" using basic strings', () => { - expectPattern(choiceOf('a')).toBe('a'); - expectPattern(choiceOf('a', 'b')).toBe('a|b'); - expectPattern(choiceOf('a', 'b', 'c')).toBe('a|b|c'); - expectPattern(choiceOf('aaa', 'bbb')).toBe('aaa|bbb'); + expect(choiceOf('a')).toHavePattern('a'); + expect(choiceOf('a', 'b')).toHavePattern('a|b'); + expect(choiceOf('a', 'b', 'c')).toHavePattern('a|b|c'); + expect(choiceOf('aaa', 'bbb')).toHavePattern('aaa|bbb'); }); test('"choiceOf" used in sequence', () => { - expectPattern('x', choiceOf('a'), 'x').toBe('xax'); - expectPattern(choiceOf('a', 'b'), 'x').toBe('(?:a|b)x'); - expectPattern('x', choiceOf('a', 'b')).toBe('x(?:a|b)'); + expect(['x', choiceOf('a'), 'x']).toHavePattern('xax'); + expect([choiceOf('a', 'b'), 'x']).toHavePattern('(?:a|b)x'); + expect(['x', choiceOf('a', 'b')]).toHavePattern('x(?:a|b)'); - expectPattern(choiceOf('a', 'b', 'c')).toBe('a|b|c'); - expectPattern('x', choiceOf('a', 'b', 'c')).toBe('x(?:a|b|c)'); - expectPattern(choiceOf('a', 'b', 'c'), 'x').toBe('(?:a|b|c)x'); + expect(choiceOf('a', 'b', 'c')).toHavePattern('a|b|c'); + expect(['x', choiceOf('a', 'b', 'c')]).toHavePattern('x(?:a|b|c)'); + expect([choiceOf('a', 'b', 'c'), 'x']).toHavePattern('(?:a|b|c)x'); - expectPattern(choiceOf('aaa', 'bbb')).toBe('aaa|bbb'); + expect(choiceOf('aaa', 'bbb')).toHavePattern('aaa|bbb'); }); test('"choiceOf" using nested regex', () => { - expectPattern(choiceOf(oneOrMore('a'), zeroOrMore('b'))).toBe('a+|b*'); + expect(choiceOf(oneOrMore('a'), zeroOrMore('b'))).toHavePattern('a+|b*'); expect( - buildPattern( - choiceOf(repeat({ min: 1, max: 3 }, 'a'), repeat({ count: 5 }, 'bx')) - ) - ).toBe('a{1,3}|(?:bx){5}'); + choiceOf(repeat({ min: 1, max: 3 }, 'a'), repeat({ count: 5 }, 'bx')) + ).toHavePattern('a{1,3}|(?:bx){5}'); }); test('`choiceOf` throws on empty options', () => { diff --git a/src/components/__tests__/quantifiers.test.tsx b/src/components/__tests__/quantifiers.test.tsx index 3563241..9b473b1 100644 --- a/src/components/__tests__/quantifiers.test.tsx +++ b/src/components/__tests__/quantifiers.test.tsx @@ -1,26 +1,26 @@ import { buildRegex } from '../../builders'; -import { expectPattern } from '../../test-utils'; import { digit } from '../character-class'; import { one, oneOrMore, optionally, zeroOrMore } from '../quantifiers'; +import '../../test-utils'; test('"oneOrMore" quantifier', () => { - expectPattern(oneOrMore('a')).toBe('a+'); - expectPattern(oneOrMore('ab')).toBe('(?:ab)+'); + expect(oneOrMore('a')).toHavePattern('a+'); + expect(oneOrMore('ab')).toHavePattern('(?:ab)+'); }); test('"one" quantifier', () => { - expectPattern(one('a')).toBe('a'); - expectPattern(one('ab')).toBe('ab'); + expect(one('a')).toHavePattern('a'); + expect(one('ab')).toHavePattern('ab'); }); test('"optionally" quantifier', () => { - expectPattern(optionally('a')).toBe('a?'); - expectPattern(optionally('ab')).toBe('(?:ab)?'); + expect(optionally('a')).toHavePattern('a?'); + expect(optionally('ab')).toHavePattern('(?:ab)?'); }); test('"zeroOrMore" quantifier', () => { - expectPattern(zeroOrMore('a')).toBe('a*'); - expectPattern(zeroOrMore('ab')).toBe('(?:ab)*'); + expect(zeroOrMore('a')).toHavePattern('a*'); + expect(zeroOrMore('ab')).toHavePattern('(?:ab)*'); }); test('oneOrMore does not generate capture when grouping', () => { @@ -48,12 +48,12 @@ test('zeroOrMore does not generate capture when grouping', () => { }); test('base quantifiers optimize grouping for atoms', () => { - expectPattern(one(digit)).toBe('\\d'); - expectPattern(oneOrMore(digit)).toBe('\\d+'); - expectPattern(optionally(digit)).toBe('\\d?'); - expectPattern(zeroOrMore(digit)).toBe('\\d*'); - - expectPattern(oneOrMore('a')).toBe('a+'); - expectPattern(optionally('a')).toBe('a?'); - expectPattern(zeroOrMore('a')).toBe('a*'); + expect(one(digit)).toHavePattern('\\d'); + expect(oneOrMore(digit)).toHavePattern('\\d+'); + expect(optionally(digit)).toHavePattern('\\d?'); + expect(zeroOrMore(digit)).toHavePattern('\\d*'); + + expect(oneOrMore('a')).toHavePattern('a+'); + expect(optionally('a')).toHavePattern('a?'); + expect(zeroOrMore('a')).toHavePattern('a*'); }); diff --git a/src/components/__tests__/repeat.test.tsx b/src/components/__tests__/repeat.test.tsx index 24b187d..6fc6cf2 100644 --- a/src/components/__tests__/repeat.test.tsx +++ b/src/components/__tests__/repeat.test.tsx @@ -1,25 +1,25 @@ -import { expectPattern } from '../../test-utils'; +import '../../test-utils'; import { digit } from '../character-class'; import { oneOrMore, zeroOrMore } from '../quantifiers'; import { repeat } from '../repeat'; test('"repeat" quantifier', () => { - expectPattern('a', repeat({ min: 1, max: 5 }, 'b')).toBe('ab{1,5}'); - expectPattern('a', repeat({ min: 1 }, 'b')).toBe('ab{1,}'); - expectPattern('a', repeat({ count: 1 }, 'b')).toBe('ab{1}'); + expect(['a', repeat({ min: 1, max: 5 }, 'b')]).toHavePattern('ab{1,5}'); + expect(['a', repeat({ min: 1 }, 'b')]).toHavePattern('ab{1,}'); + expect(['a', repeat({ count: 1 }, 'b')]).toHavePattern('ab{1}'); - expectPattern('a', repeat({ count: 1 }, 'a', zeroOrMore('b'))).toBe( + expect(['a', repeat({ count: 1 }, 'a', zeroOrMore('b'))]).toHavePattern( 'a(?:ab*){1}' ); - expectPattern(repeat({ count: 5 }, 'text', ' ', oneOrMore('d'))).toBe( + expect(repeat({ count: 5 }, 'text', ' ', oneOrMore('d'))).toHavePattern( '(?:text d+){5}' ); }); test('"repeat"" optimizes grouping for atoms', () => { - expectPattern(repeat({ count: 2 }, digit)).toBe('\\d{2}'); - expectPattern(repeat({ min: 2 }, digit)).toBe('\\d{2,}'); - expectPattern(repeat({ min: 1, max: 5 }, digit)).toBe('\\d{1,5}'); + expect(repeat({ count: 2 }, digit)).toHavePattern('\\d{2}'); + expect(repeat({ min: 2 }, digit)).toHavePattern('\\d{2,}'); + expect(repeat({ min: 1, max: 5 }, digit)).toHavePattern('\\d{1,5}'); }); test('`repeat` throws on no children', () => { diff --git a/src/encoder/__tests__/encoder.test.tsx b/src/encoder/__tests__/encoder.test.tsx index 5867c20..c00fcf1 100644 --- a/src/encoder/__tests__/encoder.test.tsx +++ b/src/encoder/__tests__/encoder.test.tsx @@ -6,27 +6,28 @@ import { zeroOrMore, } from '../../components/quantifiers'; import { repeat } from '../../components/repeat'; +import '../../test-utils'; test('basic quantifies', () => { - expect(buildPattern('a')).toEqual('a'); - expect(buildPattern('a', 'b')).toEqual('ab'); + expect('a').toHavePattern('a'); + expect(['a', 'b']).toHavePattern('ab'); - expect(buildPattern(oneOrMore('a'))).toEqual('a+'); - expect(buildPattern(optionally('a'))).toEqual('a?'); + expect(oneOrMore('a')).toHavePattern('a+'); + expect(optionally('a')).toHavePattern('a?'); - expect(buildPattern('a', oneOrMore('b'))).toEqual('ab+'); - expect(buildPattern('a', oneOrMore('bc'))).toEqual('a(?:bc)+'); - expect(buildPattern('a', oneOrMore('bc'))).toEqual('a(?:bc)+'); + expect(['a', oneOrMore('b')]).toHavePattern('ab+'); + expect(['a', oneOrMore('bc')]).toHavePattern('a(?:bc)+'); + expect(['a', oneOrMore('bc')]).toHavePattern('a(?:bc)+'); - expect(buildPattern('a', repeat({ min: 1, max: 5 }, 'b'))).toEqual('ab{1,5}'); + expect(['a', repeat({ min: 1, max: 5 }, 'b')]).toHavePattern('ab{1,5}'); - expect(buildPattern('a', zeroOrMore('b'))).toEqual('ab*'); - expect(buildPattern('a', zeroOrMore('bc'))).toEqual('a(?:bc)*'); - expect(buildPattern('a', zeroOrMore('bc'))).toEqual('a(?:bc)*'); + expect(['a', zeroOrMore('b')]).toHavePattern('ab*'); + expect(['a', zeroOrMore('bc')]).toHavePattern('a(?:bc)*'); + expect(['a', zeroOrMore('bc')]).toHavePattern('a(?:bc)*'); - expect(buildPattern(optionally('a'), 'b')).toEqual('a?b'); + expect([optionally('a'), 'b']).toHavePattern('a?b'); - expect(buildPattern(optionally('a'), 'b', one('d'))).toEqual('a?bd'); + expect([optionally('a'), 'b', one('d')]).toHavePattern('a?bd'); }); test('regex constructor', () => { @@ -35,22 +36,22 @@ test('regex constructor', () => { }); test('"buildPattern" escapes special characters', () => { - expect(buildPattern('.')).toBe('\\.'); - expect(buildPattern('*')).toBe('\\*'); - expect(buildPattern('+')).toBe('\\+'); - expect(buildPattern('?')).toBe('\\?'); - expect(buildPattern('^')).toBe('\\^'); - expect(buildPattern('$')).toBe('\\$'); - expect(buildPattern('{')).toBe('\\{'); - expect(buildPattern('}')).toBe('\\}'); - expect(buildPattern('|')).toBe('\\|'); - expect(buildPattern('[')).toBe('\\['); - expect(buildPattern(']')).toBe('\\]'); - expect(buildPattern('\\')).toBe('\\\\'); + expect('.').toHavePattern('\\.'); + expect('*').toHavePattern('\\*'); + expect('+').toHavePattern('\\+'); + expect('?').toHavePattern('\\?'); + expect('^').toHavePattern('\\^'); + expect('$').toHavePattern('\\$'); + expect('{').toHavePattern('\\{'); + expect('}').toHavePattern('\\}'); + expect('|').toHavePattern('\\|'); + expect('[').toHavePattern('\\['); + expect(']').toHavePattern('\\]'); + expect('\\').toHavePattern('\\\\'); - expect(buildPattern('*.*')).toBe('\\*\\.\\*'); + expect('*.*').toHavePattern('\\*\\.\\*'); - expect(buildPattern(oneOrMore('.*'), zeroOrMore('[]{}'))).toBe( + expect([oneOrMore('.*'), zeroOrMore('[]{}')]).toHavePattern( '(?:\\.\\*)+(?:\\[\\]\\{\\})*' ); }); diff --git a/src/test-utils.ts b/src/test-utils.ts index 31cabbb..5b2e296 100644 --- a/src/test-utils.ts +++ b/src/test-utils.ts @@ -1,5 +1,6 @@ import { buildPattern, buildRegex } from './builders'; import type { RegexElement } from './components/types'; +import { isRegexElement } from './utils'; export function execRegex( text: string, @@ -10,8 +11,48 @@ export function execRegex( return result ? [...result] : null; } -export function expectPattern(...elements: Array) { - const pattern = buildPattern(...elements); - // eslint-disable-next-line jest/valid-expect - return expect(pattern); +export function toHavePattern( + this: jest.MatcherContext, + elements: Array | RegexElement | string, + expected: string +) { + if (!Array.isArray(elements)) { + elements = [elements]; + } + + elements.forEach((e) => { + if (typeof e !== 'string' && !isRegexElement(e)) { + throw new Error( + `"toHavePattern()" received an array of RegexElements and strings.` + ); + } + }); + + const received = buildPattern(...elements); + + const options = { + isNot: this.isNot, + }; + + return { + pass: expected === received, + message: () => + this.utils.matcherHint('toHavePattern', undefined, undefined, options) + + '\n\n' + + `Expected: ${this.isNot ? 'not ' : ''}${this.utils.printExpected( + expected + )}\n` + + `Received: ${this.utils.printReceived(received)}`, + }; +} + +expect.extend({ toHavePattern }); + +declare global { + namespace jest { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface Matchers { + toHavePattern(expected: string): R; + } + } } From 343a295a9d67e15651d7e0020ec7575992bd433d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Thu, 21 Dec 2023 23:04:03 +0100 Subject: [PATCH 3/5] refactor: self code review --- package.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/package.json b/package.json index a27e959..7a9f5cc 100644 --- a/package.json +++ b/package.json @@ -125,15 +125,6 @@ "ignoreDeclarationSort": true } ], - "jest/expect-expect": [ - "error", - { - "assertFunctionNames": [ - "expect", - "expectPattern" - ] - } - ], "import/order": "error" } }, From 03b80b84745b81788183b9636cefa97a3ae43863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Fri, 22 Dec 2023 09:47:54 +0100 Subject: [PATCH 4/5] refactor: tweak strings --- src/__tests__/builder.test.ts | 2 +- src/components/__tests__/capture.test.tsx | 4 +-- .../__tests__/character-class.test.ts | 26 +++++++++---------- src/components/__tests__/choice-of.test.ts | 6 ++--- src/components/__tests__/quantifiers.test.tsx | 16 ++++++------ src/components/__tests__/repeat.test.tsx | 4 +-- src/encoder/__tests__/encoder.test.tsx | 8 +++--- src/encoder/encoder.ts | 2 +- src/test-utils.ts | 2 +- 9 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/__tests__/builder.test.ts b/src/__tests__/builder.test.ts index f9ed336..6aa5f59 100644 --- a/src/__tests__/builder.test.ts +++ b/src/__tests__/builder.test.ts @@ -1,6 +1,6 @@ import { buildRegex } from '../builders'; -test('"regexBuilder" flags', () => { +test('`regexBuilder` flags', () => { expect(buildRegex('a').flags).toBe(''); expect(buildRegex({}, 'a').flags).toBe(''); diff --git a/src/components/__tests__/capture.test.tsx b/src/components/__tests__/capture.test.tsx index 683b77a..d9c6df4 100644 --- a/src/components/__tests__/capture.test.tsx +++ b/src/components/__tests__/capture.test.tsx @@ -2,14 +2,14 @@ import { capture } from '../capture'; import { oneOrMore } from '../quantifiers'; import { execRegex } from '../../test-utils'; -test('"capture" base cases', () => { +test('`capture` base cases', () => { expect(capture('a')).toHavePattern('(a)'); expect(capture('abc')).toHavePattern('(abc)'); expect(capture(oneOrMore('abc'))).toHavePattern('((?:abc)+)'); expect(oneOrMore(capture('abc'))).toHavePattern('(abc)+'); }); -test('"capture" captures group', () => { +test('`capture` captures group', () => { expect(execRegex('ab', [capture('b')])).toEqual(['b', 'b']); expect(execRegex('ab', ['a', capture('b')])).toEqual(['ab', 'b']); expect(execRegex('abc', ['a', capture('b'), capture('c')])).toEqual([ diff --git a/src/components/__tests__/character-class.test.ts b/src/components/__tests__/character-class.test.ts index 40d6946..a666ff4 100644 --- a/src/components/__tests__/character-class.test.ts +++ b/src/components/__tests__/character-class.test.ts @@ -10,31 +10,31 @@ import { } from '../character-class'; import { execRegex } from '../../test-utils'; -test('"any" character class', () => { +test('`any` character class', () => { expect(any).toHavePattern('.'); expect(['x', any]).toHavePattern('x.'); expect(['x', any, 'x']).toHavePattern('x.x'); }); -test('"digit" character class', () => { +test('`digit` character class', () => { expect(digit).toHavePattern('\\d'); expect(['x', digit]).toHavePattern('x\\d'); expect(['x', digit, 'x']).toHavePattern('x\\dx'); }); -test('"word" character class', () => { +test('`word` character class', () => { expect(word).toHavePattern('\\w'); expect(['x', word]).toHavePattern('x\\w'); expect(['x', word, 'x']).toHavePattern('x\\wx'); }); -test('"whitespace" character class', () => { +test('`whitespace` character class', () => { expect(whitespace).toHavePattern('\\s'); expect(['x', whitespace]).toHavePattern('x\\s'); expect(['x', whitespace, 'x']).toHavePattern('x\\sx'); }); -test('"anyOf" base cases', () => { +test('`anyOf` base cases', () => { expect(anyOf('a')).toHavePattern('a'); expect(['x', anyOf('a'), 'x']).toHavePattern('xax'); expect(anyOf('ab')).toHavePattern('[ab]'); @@ -42,42 +42,42 @@ test('"anyOf" base cases', () => { expect(['x', anyOf('ab'), 'x']).toHavePattern('x[ab]x'); }); -test('"anyOf" with quantifiers', () => { +test('`anyOf` with quantifiers', () => { expect(['x', oneOrMore(anyOf('abc')), 'x']).toHavePattern('x[abc]+x'); expect(['x', optionally(anyOf('abc')), 'x']).toHavePattern('x[abc]?x'); expect(['x', zeroOrMore(anyOf('abc')), 'x']).toHavePattern('x[abc]*x'); }); -test('"anyOf" escapes special characters', () => { +test('`anyOf` escapes special characters', () => { expect(anyOf('abc-+.')).toHavePattern('[-abc\\+\\.]'); }); -test('"anyOf" moves hyphen to the first position', () => { +test('`anyOf` moves hyphen to the first position', () => { expect(anyOf('a-bc')).toHavePattern('[-abc]'); }); -test('"anyOf" throws on empty text', () => { +test('`anyOf` throws on empty text', () => { expect(() => anyOf('')).toThrowErrorMatchingInlineSnapshot( `"\`anyOf\` should received at least one character"` ); }); -test('"inverted" character class', () => { +test('`inverted` character class', () => { expect(inverted(anyOf('a'))).toHavePattern('[^a]'); expect(inverted(anyOf('abc'))).toHavePattern('[^abc]'); }); -test('"inverted" character class double inversion', () => { +test('`inverted` character class double inversion', () => { expect(inverted(inverted(anyOf('a')))).toHavePattern('a'); expect(inverted(inverted(anyOf('abc')))).toHavePattern('[abc]'); }); -test('"inverted" character class execution', () => { +test('`inverted` character class execution', () => { expect(execRegex('aa', [inverted(anyOf('a'))])).toBeNull(); expect(execRegex('aba', [inverted(anyOf('a'))])).toEqual(['b']); }); -test('buildPattern throws on empty text', () => { +test('`encodeCharacterClass` throws on empty text', () => { expect(() => encodeCharacterClass({ type: 'characterClass', diff --git a/src/components/__tests__/choice-of.test.ts b/src/components/__tests__/choice-of.test.ts index 0cf65f4..afae55f 100644 --- a/src/components/__tests__/choice-of.test.ts +++ b/src/components/__tests__/choice-of.test.ts @@ -3,14 +3,14 @@ import { repeat } from '../repeat'; import { choiceOf } from '../choice-of'; import '../../test-utils'; -test('"choiceOf" using basic strings', () => { +test('`choiceOf` using basic strings', () => { expect(choiceOf('a')).toHavePattern('a'); expect(choiceOf('a', 'b')).toHavePattern('a|b'); expect(choiceOf('a', 'b', 'c')).toHavePattern('a|b|c'); expect(choiceOf('aaa', 'bbb')).toHavePattern('aaa|bbb'); }); -test('"choiceOf" used in sequence', () => { +test('`choiceOf` used in sequence', () => { expect(['x', choiceOf('a'), 'x']).toHavePattern('xax'); expect([choiceOf('a', 'b'), 'x']).toHavePattern('(?:a|b)x'); expect(['x', choiceOf('a', 'b')]).toHavePattern('x(?:a|b)'); @@ -22,7 +22,7 @@ test('"choiceOf" used in sequence', () => { expect(choiceOf('aaa', 'bbb')).toHavePattern('aaa|bbb'); }); -test('"choiceOf" using nested regex', () => { +test('`choiceOf` using nested regex', () => { expect(choiceOf(oneOrMore('a'), zeroOrMore('b'))).toHavePattern('a+|b*'); expect( choiceOf(repeat({ min: 1, max: 3 }, 'a'), repeat({ count: 5 }, 'bx')) diff --git a/src/components/__tests__/quantifiers.test.tsx b/src/components/__tests__/quantifiers.test.tsx index 9b473b1..ad88b4b 100644 --- a/src/components/__tests__/quantifiers.test.tsx +++ b/src/components/__tests__/quantifiers.test.tsx @@ -3,45 +3,45 @@ import { digit } from '../character-class'; import { one, oneOrMore, optionally, zeroOrMore } from '../quantifiers'; import '../../test-utils'; -test('"oneOrMore" quantifier', () => { +test('`oneOrMore` quantifier', () => { expect(oneOrMore('a')).toHavePattern('a+'); expect(oneOrMore('ab')).toHavePattern('(?:ab)+'); }); -test('"one" quantifier', () => { +test('`one` quantifier', () => { expect(one('a')).toHavePattern('a'); expect(one('ab')).toHavePattern('ab'); }); -test('"optionally" quantifier', () => { +test('`optionally` quantifier', () => { expect(optionally('a')).toHavePattern('a?'); expect(optionally('ab')).toHavePattern('(?:ab)?'); }); -test('"zeroOrMore" quantifier', () => { +test('`zeroOrMore` quantifier', () => { expect(zeroOrMore('a')).toHavePattern('a*'); expect(zeroOrMore('ab')).toHavePattern('(?:ab)*'); }); -test('oneOrMore does not generate capture when grouping', () => { +test('`oneOrMore` does not generate capture when grouping', () => { const regex = buildRegex(oneOrMore('aa')); const groups = [...'aa'.match(regex)!]; expect(groups).toEqual(['aa']); }); -test('one does not generate capture when grouping', () => { +test('`one` does not generate capture when grouping', () => { const regex = buildRegex(one('aa')); const groups = [...'aa'.match(regex)!]; expect(groups).toEqual(['aa']); }); -test('optionally does not generate capture when grouping', () => { +test('`optionally` does not generate capture when grouping', () => { const regex = buildRegex(optionally('aa')); const groups = [...'aa'.match(regex)!]; expect(groups).toEqual(['aa']); }); -test('zeroOrMore does not generate capture when grouping', () => { +test('`zeroOrMore` does not generate capture when grouping', () => { const regex = buildRegex(zeroOrMore('aa')); const groups = [...'aa'.match(regex)!]; expect(groups).toEqual(['aa']); diff --git a/src/components/__tests__/repeat.test.tsx b/src/components/__tests__/repeat.test.tsx index 6fc6cf2..b095b6b 100644 --- a/src/components/__tests__/repeat.test.tsx +++ b/src/components/__tests__/repeat.test.tsx @@ -3,7 +3,7 @@ import { digit } from '../character-class'; import { oneOrMore, zeroOrMore } from '../quantifiers'; import { repeat } from '../repeat'; -test('"repeat" quantifier', () => { +test('`repeat` quantifier', () => { expect(['a', repeat({ min: 1, max: 5 }, 'b')]).toHavePattern('ab{1,5}'); expect(['a', repeat({ min: 1 }, 'b')]).toHavePattern('ab{1,}'); expect(['a', repeat({ count: 1 }, 'b')]).toHavePattern('ab{1}'); @@ -16,7 +16,7 @@ test('"repeat" quantifier', () => { ); }); -test('"repeat"" optimizes grouping for atoms', () => { +test('`repeat` optimizes grouping for atoms', () => { expect(repeat({ count: 2 }, digit)).toHavePattern('\\d{2}'); expect(repeat({ min: 2 }, digit)).toHavePattern('\\d{2,}'); expect(repeat({ min: 1, max: 5 }, digit)).toHavePattern('\\d{1,5}'); diff --git a/src/encoder/__tests__/encoder.test.tsx b/src/encoder/__tests__/encoder.test.tsx index c00fcf1..9a0fd0d 100644 --- a/src/encoder/__tests__/encoder.test.tsx +++ b/src/encoder/__tests__/encoder.test.tsx @@ -35,7 +35,7 @@ test('regex constructor', () => { expect(buildRegex('a').test('b')).toBeFalsy(); }); -test('"buildPattern" escapes special characters', () => { +test('`buildPattern` escapes special characters', () => { expect('.').toHavePattern('\\.'); expect('*').toHavePattern('\\*'); expect('+').toHavePattern('\\+'); @@ -56,14 +56,14 @@ test('"buildPattern" escapes special characters', () => { ); }); -test('buildRegex throws error on unknown element', () => { +test('`buildRegex` throws error on unknown element', () => { expect(() => // @ts-expect-error intentionally passing incorrect object buildRegex({ type: 'unknown' }) - ).toThrowErrorMatchingInlineSnapshot(`"Unknown elements type unknown"`); + ).toThrowErrorMatchingInlineSnapshot(`"Unknown element type unknown"`); }); -test('buildPattern throws on empty text', () => { +test('`buildPattern` throws on empty text', () => { expect(() => buildPattern('')).toThrowErrorMatchingInlineSnapshot( `"\`encodeText\`: received text should not be empty"` ); diff --git a/src/encoder/encoder.ts b/src/encoder/encoder.ts index 5a586aa..61c49a3 100644 --- a/src/encoder/encoder.ts +++ b/src/encoder/encoder.ts @@ -56,7 +56,7 @@ export function encodeElement(element: RegexElement | string): EncoderNode { } // @ts-expect-error User passed incorrect type - throw new Error(`Unknown elements type ${element.type}`); + throw new Error(`Unknown element type ${element.type}`); } function encodeText(text: string): EncoderNode { diff --git a/src/test-utils.ts b/src/test-utils.ts index 5b2e296..ca2930b 100644 --- a/src/test-utils.ts +++ b/src/test-utils.ts @@ -23,7 +23,7 @@ export function toHavePattern( elements.forEach((e) => { if (typeof e !== 'string' && !isRegexElement(e)) { throw new Error( - `"toHavePattern()" received an array of RegexElements and strings.` + `\`toHavePattern()\` received an array of RegexElements and strings.` ); } }); From d3a21706b2bc8541a12ef7458154c19ed2ffa600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Fri, 22 Dec 2023 10:16:26 +0100 Subject: [PATCH 5/5] refactor: cleanup --- jest-setup.ts | 2 + package.json | 6 +++ src/components/__tests__/capture.test.tsx | 7 ++- .../__tests__/character-class.test.ts | 5 +- src/components/__tests__/choice-of.test.ts | 1 - src/components/__tests__/quantifiers.test.tsx | 1 - src/components/__tests__/repeat.test.tsx | 1 - src/encoder/__tests__/encoder.test.tsx | 1 - src/utils.ts | 4 ++ .../to-have-pattern.ts | 17 ++---- test-utils/to-match-groups.ts | 52 +++++++++++++++++++ 11 files changed, 73 insertions(+), 24 deletions(-) create mode 100644 jest-setup.ts rename src/test-utils.ts => test-utils/to-have-pattern.ts (71%) create mode 100644 test-utils/to-match-groups.ts diff --git a/jest-setup.ts b/jest-setup.ts new file mode 100644 index 0000000..b58164c --- /dev/null +++ b/jest-setup.ts @@ -0,0 +1,2 @@ +import './test-utils/to-have-pattern'; +import './test-utils/to-match-groups'; diff --git a/package.json b/package.json index 7a9f5cc..86a00c7 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,12 @@ }, "jest": { "preset": "react-native", + "setupFilesAfterEnv": [ + "/jest-setup.ts" + ], + "collectCoverageFrom": [ + "src/**/*.{js,jsx,ts,tsx}" + ], "modulePathIgnorePatterns": [ "/lib/" ] diff --git a/src/components/__tests__/capture.test.tsx b/src/components/__tests__/capture.test.tsx index d9c6df4..a969eee 100644 --- a/src/components/__tests__/capture.test.tsx +++ b/src/components/__tests__/capture.test.tsx @@ -1,6 +1,5 @@ import { capture } from '../capture'; import { oneOrMore } from '../quantifiers'; -import { execRegex } from '../../test-utils'; test('`capture` base cases', () => { expect(capture('a')).toHavePattern('(a)'); @@ -10,9 +9,9 @@ test('`capture` base cases', () => { }); test('`capture` captures group', () => { - expect(execRegex('ab', [capture('b')])).toEqual(['b', 'b']); - expect(execRegex('ab', ['a', capture('b')])).toEqual(['ab', 'b']); - expect(execRegex('abc', ['a', capture('b'), capture('c')])).toEqual([ + expect(capture('b')).toMatchGroups('ab', ['b', 'b']); + expect(['a', capture('b')]).toMatchGroups('ab', ['ab', 'b']); + expect(['a', capture('b'), capture('c')]).toMatchGroups('abc', [ 'abc', 'b', 'c', diff --git a/src/components/__tests__/character-class.test.ts b/src/components/__tests__/character-class.test.ts index a666ff4..4e8ab2c 100644 --- a/src/components/__tests__/character-class.test.ts +++ b/src/components/__tests__/character-class.test.ts @@ -8,7 +8,6 @@ import { whitespace, word, } from '../character-class'; -import { execRegex } from '../../test-utils'; test('`any` character class', () => { expect(any).toHavePattern('.'); @@ -73,8 +72,8 @@ test('`inverted` character class double inversion', () => { }); test('`inverted` character class execution', () => { - expect(execRegex('aa', [inverted(anyOf('a'))])).toBeNull(); - expect(execRegex('aba', [inverted(anyOf('a'))])).toEqual(['b']); + expect(inverted(anyOf('a'))).toMatchGroups('aa', []); + expect(inverted(anyOf('a'))).toMatchGroups('aba', ['b']); }); test('`encodeCharacterClass` throws on empty text', () => { diff --git a/src/components/__tests__/choice-of.test.ts b/src/components/__tests__/choice-of.test.ts index afae55f..30502c8 100644 --- a/src/components/__tests__/choice-of.test.ts +++ b/src/components/__tests__/choice-of.test.ts @@ -1,7 +1,6 @@ import { oneOrMore, zeroOrMore } from '../quantifiers'; import { repeat } from '../repeat'; import { choiceOf } from '../choice-of'; -import '../../test-utils'; test('`choiceOf` using basic strings', () => { expect(choiceOf('a')).toHavePattern('a'); diff --git a/src/components/__tests__/quantifiers.test.tsx b/src/components/__tests__/quantifiers.test.tsx index ad88b4b..22934ff 100644 --- a/src/components/__tests__/quantifiers.test.tsx +++ b/src/components/__tests__/quantifiers.test.tsx @@ -1,7 +1,6 @@ import { buildRegex } from '../../builders'; import { digit } from '../character-class'; import { one, oneOrMore, optionally, zeroOrMore } from '../quantifiers'; -import '../../test-utils'; test('`oneOrMore` quantifier', () => { expect(oneOrMore('a')).toHavePattern('a+'); diff --git a/src/components/__tests__/repeat.test.tsx b/src/components/__tests__/repeat.test.tsx index b095b6b..0243930 100644 --- a/src/components/__tests__/repeat.test.tsx +++ b/src/components/__tests__/repeat.test.tsx @@ -1,4 +1,3 @@ -import '../../test-utils'; import { digit } from '../character-class'; import { oneOrMore, zeroOrMore } from '../quantifiers'; import { repeat } from '../repeat'; diff --git a/src/encoder/__tests__/encoder.test.tsx b/src/encoder/__tests__/encoder.test.tsx index 9a0fd0d..919573a 100644 --- a/src/encoder/__tests__/encoder.test.tsx +++ b/src/encoder/__tests__/encoder.test.tsx @@ -6,7 +6,6 @@ import { zeroOrMore, } from '../../components/quantifiers'; import { repeat } from '../../components/repeat'; -import '../../test-utils'; test('basic quantifies', () => { expect('a').toHavePattern('a'); diff --git a/src/utils.ts b/src/utils.ts index d0d930d..ef5b9dc 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -34,6 +34,10 @@ export function isRegexElement(element: unknown): element is RegexElement { return typeof element === 'object' && element !== null && 'type' in element; } +export function isRegexElementOrString(element: unknown): boolean { + return typeof element === 'string' || isRegexElement(element); +} + // Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping export function escapeText(text: string) { return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string diff --git a/src/test-utils.ts b/test-utils/to-have-pattern.ts similarity index 71% rename from src/test-utils.ts rename to test-utils/to-have-pattern.ts index ca2930b..2ed2459 100644 --- a/src/test-utils.ts +++ b/test-utils/to-have-pattern.ts @@ -1,15 +1,6 @@ -import { buildPattern, buildRegex } from './builders'; -import type { RegexElement } from './components/types'; -import { isRegexElement } from './utils'; - -export function execRegex( - text: string, - elements: Array -) { - const regex = buildRegex(...elements); - const result = regex.exec(text); - return result ? [...result] : null; -} +import { buildPattern } from '../src/builders'; +import type { RegexElement } from '../src/components/types'; +import { isRegexElementOrString } from '../src/utils'; export function toHavePattern( this: jest.MatcherContext, @@ -21,7 +12,7 @@ export function toHavePattern( } elements.forEach((e) => { - if (typeof e !== 'string' && !isRegexElement(e)) { + if (!isRegexElementOrString(e)) { throw new Error( `\`toHavePattern()\` received an array of RegexElements and strings.` ); diff --git a/test-utils/to-match-groups.ts b/test-utils/to-match-groups.ts new file mode 100644 index 0000000..b453a7d --- /dev/null +++ b/test-utils/to-match-groups.ts @@ -0,0 +1,52 @@ +import { buildRegex } from '../src/builders'; +import type { RegexElement } from '../src/components/types'; +import { isRegexElementOrString } from '../src/utils'; + +export function toMatchGroups( + this: jest.MatcherContext, + elements: Array | RegexElement | string, + input: string, + expected: string[] +) { + if (!Array.isArray(elements)) { + elements = [elements]; + } + + elements.forEach((e) => { + if (!isRegexElementOrString(e)) { + throw new Error( + `\`toHavePattern()\` received an array of RegexElements and strings.` + ); + } + }); + + const regex = buildRegex(...elements); + const options = { + isNot: this.isNot, + }; + + const execResult = regex.exec(input); + const actual = execResult ? [...execResult] : []; + + return { + pass: this.equals(actual, expected), + message: () => + this.utils.matcherHint('toMatchGroups', undefined, undefined, options) + + '\n\n' + + `Expected: ${this.isNot ? 'not ' : ''}${this.utils.printExpected( + expected + )}\n` + + `Received: ${this.utils.printReceived(actual)}`, + }; +} + +expect.extend({ toMatchGroups }); + +declare global { + namespace jest { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface Matchers { + toMatchGroups(input: string, expected: string[]): R; + } + } +}