Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dry-bees-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-component-mocker": patch
---

update to have prop matcher
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
134 changes: 123 additions & 11 deletions src/customMatchers/toHaveProp.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TestComponent>('title');
expect(element).toHaveProp<TestComponent>('count');
expect(element).toHaveProp<TestComponent>('onClick');
});

it('should fail when element does not have prop by key', () => {
Expand Down Expand Up @@ -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<TestComponent>({ title: 'Hello World' });
expect(element).toHaveProp<TestComponent>({ count: 42 });
expect(element).toHaveProp<TestComponent>({ onClick: mockClick });
});

it('should fail when prop value does not match expected', () => {
Expand All @@ -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<TestComponent>({ title: 'Expected Title' });
}).toThrow('Expected element to have prop "title" with correct value');
});

Expand Down Expand Up @@ -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<TestComponent>('isActive');
expect(element).toHaveProp<TestComponent>({ isActive: true });
});

it('should fail when element is not a mock component', () => {
Expand All @@ -120,7 +120,7 @@ describe('Custom Matcher: toHaveProp', () => {
const element = screen.getByTestId('regular-div');

expect(() => {
expect(element).toHaveProp('title');
expect(element).toHaveProp<TestComponent>('title');
}).toThrow(
'Element with testId "regular-div" is not a mock component. Please create a mock component first using createMockComponent("regular-div").'
);
Expand All @@ -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<TestComponent>({ title: 'Test', count: 1 });
}).toThrow('Expected exactly one key-value pair');
});

describe('two-argument syntax: toHaveProp<TestComponent>(key, value)', () => {
it('should pass when element has prop with expected value', () => {
const MockComponent =
createMockComponent<TestComponent>('test-component-8');
const mockClick = vi.fn();

render(
<MockComponent title="Hello World" count={42} onClick={mockClick} />
);

const element = screen.getByTestId('test-component-8');

expect(element).toHaveProp<TestComponent>('title', 'Hello World');
expect(element).toHaveProp<TestComponent>('count', 42);
expect(element).toHaveProp<TestComponent>('onClick', mockClick);
});

it('should pass with boolean false value', () => {
const MockComponent =
createMockComponent<TestComponent>('test-component-9');
const mockClick = vi.fn();

render(
<MockComponent
title="Test"
count={1}
onClick={mockClick}
isActive={false}
/>
);

const element = screen.getByTestId('test-component-9');

expect(element).toHaveProp<TestComponent>('isActive', false);
});

it('should pass with boolean true value', () => {
const MockComponent =
createMockComponent<TestComponent>('test-component-10');
const mockClick = vi.fn();

render(
<MockComponent
title="Test"
count={1}
onClick={mockClick}
isActive={true}
/>
);

const element = screen.getByTestId('test-component-10');

expect(element).toHaveProp<TestComponent>('isActive', true);
});

it('should fail when prop value does not match expected', () => {
const MockComponent =
createMockComponent<TestComponent>('test-component-11');
const mockClick = vi.fn();

render(
<MockComponent title="Actual Title" count={100} onClick={mockClick} />
);

const element = screen.getByTestId('test-component-11');

expect(() => {
expect(element).toHaveProp<TestComponent>('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<TestComponent>('test-component-12');
const mockClick = vi.fn();

render(<MockComponent title="Test" count={1} onClick={mockClick} />);

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<TestComponent>('test-component-13');
const mockClick = vi.fn();

render(<MockComponent title="Test" count={0} onClick={mockClick} />);

const element = screen.getByTestId('test-component-13');

expect(element).toHaveProp<TestComponent>('count', 0);
});

it('should work with empty string', () => {
const MockComponent =
createMockComponent<TestComponent>('test-component-14');
const mockClick = vi.fn();

render(<MockComponent title="" count={1} onClick={mockClick} />);

const element = screen.getByTestId('test-component-14');

expect(element).toHaveProp<TestComponent>('title', '');
});
});
});
55 changes: 53 additions & 2 deletions src/customMatchers/toHaveProp.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,11 +11,62 @@ interface MatcherResult {
expect.extend({
toHaveProp(
received: ComponentMockElement,
keyOrKeyValue: string | Record<string, any>
keyOrKeyValue: string | Record<string, any>,
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;

Expand Down
2 changes: 1 addition & 1 deletion src/customMatchers/toHaveProps.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
16 changes: 11 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@ type ToHavePropsMatcherType<R> = <TComponent extends ComponentType<any>>(
expectedProps?: Omit<ComponentProps<TComponent>, 'children'>
) => R;

type ToHavePropMatcherType<R> = <TComponent extends ComponentType<any>>(
keyOrKeyValue:
| keyof ComponentProps<TComponent>
| Partial<ComponentProps<TComponent>>
) => R;
type ToHavePropMatcherType<R> = {
<TComponent extends ComponentType<any>>(
keyOrKeyValue:
| keyof ComponentProps<TComponent>
| Partial<ComponentProps<TComponent>>
): R;
<TComponent extends ComponentType<any>>(
key: keyof ComponentProps<TComponent>,
value: ComponentProps<TComponent>[keyof ComponentProps<TComponent>]
): R;
};

declare module 'expect' {
interface Matchers<R> {
Expand Down