diff --git a/.changeset/dry-bees-start.md b/.changeset/dry-bees-start.md new file mode 100644 index 0000000..f3a86db --- /dev/null +++ b/.changeset/dry-bees-start.md @@ -0,0 +1,5 @@ +--- +"react-component-mocker": patch +--- + +update to have prop matcher diff --git a/package.json b/package.json index 43ce725..a590a7f 100644 --- a/package.json +++ b/package.json @@ -33,12 +33,11 @@ "prepublishOnly": "npm run check:all", "test": "vitest run && npm run test:jest", "test:jest": "jest", - "check:all": "npm run build && npm run check:lint && npm run check:format && npm run check:exports && npm run test", + "check:all": "npm run build && npm run check:biome && npm run check:exports && npm run test", "check:type": "tsc --noEmit", "check:exports": "attw --pack . --ignore-rules=cjs-resolves-to-esm", - "check:lint": "biome lint ./src ./examples", - "check:format": "biome format ./src ./examples", - "fix:format": "biome format --write ./src ./examples" + "check:biome": "biome check ./src ./examples", + "fix:biome": "biome check --write ./src ./examples" }, "publishConfig": { "access": "public" diff --git a/src/customMatchers/toHaveProp.test.tsx b/src/customMatchers/toHaveProp.test.tsx index a1fabb4..c0830f8 100644 --- a/src/customMatchers/toHaveProp.test.tsx +++ b/src/customMatchers/toHaveProp.test.tsx @@ -23,9 +23,9 @@ describe('Custom Matcher: toHaveProp', () => { const element = screen.getByTestId('test-component'); - expect(element).toHaveProp('title'); - expect(element).toHaveProp('count'); - expect(element).toHaveProp('onClick'); + expect(element).toHaveProp('title'); + expect(element).toHaveProp('count'); + expect(element).toHaveProp('onClick'); }); it('should fail when element does not have prop by key', () => { @@ -57,9 +57,9 @@ describe('Custom Matcher: toHaveProp', () => { const element = screen.getByTestId('test-component-3'); - expect(element).toHaveProp({ title: 'Hello World' }); - expect(element).toHaveProp({ count: 42 }); - expect(element).toHaveProp({ onClick: mockClick }); + expect(element).toHaveProp({ title: 'Hello World' }); + expect(element).toHaveProp({ count: 42 }); + expect(element).toHaveProp({ onClick: mockClick }); }); it('should fail when prop value does not match expected', () => { @@ -74,7 +74,7 @@ describe('Custom Matcher: toHaveProp', () => { const element = screen.getByTestId('test-component-4'); expect(() => { - expect(element).toHaveProp({ title: 'Expected Title' }); + expect(element).toHaveProp({ title: 'Expected Title' }); }).toThrow('Expected element to have prop "title" with correct value'); }); @@ -110,8 +110,8 @@ describe('Custom Matcher: toHaveProp', () => { const element = screen.getByTestId('test-component-6'); - expect(element).toHaveProp('isActive'); - expect(element).toHaveProp({ isActive: true }); + expect(element).toHaveProp('isActive'); + expect(element).toHaveProp({ isActive: true }); }); it('should fail when element is not a mock component', () => { @@ -120,7 +120,7 @@ describe('Custom Matcher: toHaveProp', () => { const element = screen.getByTestId('regular-div'); expect(() => { - expect(element).toHaveProp('title'); + expect(element).toHaveProp('title'); }).toThrow( 'Element with testId "regular-div" is not a mock component. Please create a mock component first using createMockComponent("regular-div").' ); @@ -136,7 +136,119 @@ describe('Custom Matcher: toHaveProp', () => { const element = screen.getByTestId('test-component-7'); expect(() => { - expect(element).toHaveProp({ title: 'Test', count: 1 }); + expect(element).toHaveProp({ title: 'Test', count: 1 }); }).toThrow('Expected exactly one key-value pair'); }); + + describe('two-argument syntax: toHaveProp(key, value)', () => { + it('should pass when element has prop with expected value', () => { + const MockComponent = + createMockComponent('test-component-8'); + const mockClick = vi.fn(); + + render( + + ); + + const element = screen.getByTestId('test-component-8'); + + expect(element).toHaveProp('title', 'Hello World'); + expect(element).toHaveProp('count', 42); + expect(element).toHaveProp('onClick', mockClick); + }); + + it('should pass with boolean false value', () => { + const MockComponent = + createMockComponent('test-component-9'); + const mockClick = vi.fn(); + + render( + + ); + + const element = screen.getByTestId('test-component-9'); + + expect(element).toHaveProp('isActive', false); + }); + + it('should pass with boolean true value', () => { + const MockComponent = + createMockComponent('test-component-10'); + const mockClick = vi.fn(); + + render( + + ); + + const element = screen.getByTestId('test-component-10'); + + expect(element).toHaveProp('isActive', true); + }); + + it('should fail when prop value does not match expected', () => { + const MockComponent = + createMockComponent('test-component-11'); + const mockClick = vi.fn(); + + render( + + ); + + const element = screen.getByTestId('test-component-11'); + + expect(() => { + expect(element).toHaveProp('title', 'Expected Title'); + }).toThrow('Expected element to have prop "title" with correct value'); + }); + + it('should fail when prop key does not exist', () => { + const MockComponent = + createMockComponent('test-component-12'); + const mockClick = vi.fn(); + + render(); + + const element = screen.getByTestId('test-component-12'); + + expect(() => { + expect(element).toHaveProp('nonExistentProp', 'value'); + }).toThrow( + /Expected element to have prop "nonExistentProp", but it doesn't. Available props:/ + ); + }); + + it('should work with number zero', () => { + const MockComponent = + createMockComponent('test-component-13'); + const mockClick = vi.fn(); + + render(); + + const element = screen.getByTestId('test-component-13'); + + expect(element).toHaveProp('count', 0); + }); + + it('should work with empty string', () => { + const MockComponent = + createMockComponent('test-component-14'); + const mockClick = vi.fn(); + + render(); + + const element = screen.getByTestId('test-component-14'); + + expect(element).toHaveProp('title', ''); + }); + }); }); diff --git a/src/customMatchers/toHaveProp.ts b/src/customMatchers/toHaveProp.ts index 7604dc8..1551fbf 100644 --- a/src/customMatchers/toHaveProp.ts +++ b/src/customMatchers/toHaveProp.ts @@ -1,7 +1,7 @@ import { getMockComponentProps } from '../lib/getMockComponentProps.js'; +import type { ComponentMockElement } from '../types/common.js'; import { deepEqual } from '../utils/deepEqual/index.js'; import { DiffError } from './helpers/DiffError.js'; -import type { ComponentMockElement } from '../types/common.js'; interface MatcherResult { pass: boolean; @@ -11,11 +11,62 @@ interface MatcherResult { expect.extend({ toHaveProp( received: ComponentMockElement, - keyOrKeyValue: string | Record + keyOrKeyValue: string | Record, + value?: any ): MatcherResult { try { const actualProps = getMockComponentProps(received); + // for syntax toHaveProp(key, value) + if (value) { + if (typeof keyOrKeyValue !== 'string') { + throw new Error( + 'Key must be a string when using two-argument syntax' + ); + } + + const key = keyOrKeyValue; + const expectedValue = value; + const hasKey = key in actualProps; + + if (!hasKey) { + return { + pass: false, + message: (): string => { + const availableKeys = Object.keys(actualProps); + const availableKeysFormatted = + availableKeys.length > 0 ? availableKeys.join(', ') : 'none'; + return `Expected element to have prop "${key}", but it doesn't. Available props: ${availableKeysFormatted}`; + }, + }; + } + + const actualValue = actualProps[key]; + const isMatch = deepEqual(actualValue, expectedValue); + + if (isMatch) { + return { + pass: true, + message: (): string => { + const expectedFormatted = JSON.stringify(expectedValue); + return `Expected element not to have prop "${key}" with value ${expectedFormatted}, but it does`; + }, + }; + } + + return { + pass: false, + message: (): string => { + throw new DiffError( + `Expected element to have prop "${key}" with correct value`, + actualValue, + expectedValue + ); + }, + }; + } + + // syntax toHaveProp(key) or toHaveProp({ key: value }) if (typeof keyOrKeyValue === 'string') { const hasKey = keyOrKeyValue in actualProps; diff --git a/src/customMatchers/toHaveProps.ts b/src/customMatchers/toHaveProps.ts index cbabfad..3569a9e 100644 --- a/src/customMatchers/toHaveProps.ts +++ b/src/customMatchers/toHaveProps.ts @@ -1,7 +1,7 @@ import { getMockComponentProps } from '../lib/getMockComponentProps.js'; +import type { ComponentMockElement } from '../types/common.js'; import { deepEqual } from '../utils/deepEqual/index.js'; import { DiffError } from './helpers/DiffError.js'; -import type { ComponentMockElement } from '../types/common.js'; interface MatcherResult { pass: boolean; diff --git a/src/index.ts b/src/index.ts index f4a00b6..d0421ef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,11 +11,17 @@ type ToHavePropsMatcherType = >( expectedProps?: Omit, 'children'> ) => R; -type ToHavePropMatcherType = >( - keyOrKeyValue: - | keyof ComponentProps - | Partial> -) => R; +type ToHavePropMatcherType = { + >( + keyOrKeyValue: + | keyof ComponentProps + | Partial> + ): R; + >( + key: keyof ComponentProps, + value: ComponentProps[keyof ComponentProps] + ): R; +}; declare module 'expect' { interface Matchers {