Skip to content

Commit a057550

Browse files
committed
add tests and test coverage
Signed-off-by: Winner95 <Winner95@users.noreply.github.com>
1 parent 76d56f2 commit a057550

12 files changed

+500
-0
lines changed

__tests__/handler.test.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
const { parseFixture } = require('./helpers');
2+
const customHandler = require('../index'); // index.js (module under test)
3+
const {
4+
makePathWithEmptyTypePath,
5+
makePathWithPropTypes,
6+
makePathWithExportedType,
7+
makePathWithInterfaceType,
8+
makePathWithNoTypeAnnotation,
9+
} = require('../fixtures/ParsedProps');
10+
11+
describe('typescript-react-function-component-props-handler', () => {
12+
test('handles React.FC<Props> components', () => {
13+
const doc = parseFixture('Button.tsx');
14+
15+
expect(doc.displayName).toBe('Button');
16+
expect(doc.description).toBe('A simple button component');
17+
18+
expect(doc).toHaveProperty('props');
19+
expect(doc.props).toHaveProperty('label');
20+
21+
expect(doc.props.label.tsType).toMatchObject({ name: 'string' });
22+
expect(doc.props.label.required).toBe(true);
23+
expect(doc.props.label).toHaveProperty('description');
24+
expect(doc.props.label.description).toBe('Text to show inside the button');
25+
26+
expect(doc.props).toHaveProperty('onClick');
27+
expect(doc.props.onClick.tsType).toMatchObject({ name: 'signature' });
28+
expect(doc.props.onClick.tsType.type).toBe('function');
29+
expect(doc.props.onClick.tsType.raw).toBe('() => void');
30+
expect(doc.props.onClick.tsType.signature).toMatchObject({ arguments: [], return: { name: 'void' } });
31+
expect(doc.props.onClick.required).toBe(false);
32+
expect(doc.props.onClick).toHaveProperty('description');
33+
expect(doc.props.onClick.description).toBe('Optional click handler');
34+
});
35+
36+
test('handles React.forwardRef(...) components', () => {
37+
const doc = parseFixture('TooltipTarget.tsx');
38+
39+
expect(doc.displayName).toBe('TooltipTarget');
40+
});
41+
42+
// Currently return error
43+
// test('handles React.forwardRef(...) components - part 2', () => {
44+
// const doc = parseFixture('ForwardedButton.tsx');
45+
// console.log(doc)
46+
// expect(doc.displayName).toBe('Button');
47+
// });
48+
49+
// Line 31 in index.js without type - can't be tested directly because of early return
50+
test('handles components without type', () => {
51+
const doc = parseFixture('ComponentWithoutType.tsx');
52+
expect(doc.displayName).toBe('ComponentWithoutType');
53+
});
54+
55+
// Line 51 in index.js early-returns if hasPropTypes is true
56+
test('handles components without use of params', () => {
57+
const doc = parseFixture('NoParamsComponent.tsx');
58+
expect(doc.displayName).toBe('NoParamComponent');
59+
});
60+
61+
// Line 68 in index.js calls checkForProptypes
62+
test('handles components with propTypes', () => {
63+
const doc = parseFixture('ComponentWithPropTypes.tsx');
64+
expect(doc.displayName).toBe('ComponentWithPropTypes');
65+
});
66+
67+
// Line 78 in index.js uses ExportNamedDeclaration
68+
test('handles components with exported interface', () => {
69+
const doc = parseFixture('ExportedInterfacePropsComponent.tsx');
70+
expect(doc.displayName).toBe('ComponentWithExportedType');
71+
});
72+
73+
// Line 86 in index.js uses TSInterfaceDeclaration
74+
test('handles components with interface props', () => {
75+
const doc = parseFixture('InterfacePropsComponent.tsx');
76+
expect(doc.displayName).toBe('ComponentWithInterface');
77+
78+
expect(doc).toHaveProperty('props');
79+
expect(doc.props).toHaveProperty('label');
80+
expect(doc.props.label.tsType).toMatchObject({ name: 'string' });
81+
expect(doc.props.label.required).toBe(true);
82+
83+
expect(doc.props).toHaveProperty('disabled');
84+
expect(doc.props.disabled.tsType).toMatchObject({ name: 'boolean' });
85+
expect(doc.props.disabled.required).toBe(false);
86+
});
87+
88+
// @TODO: add more tests for various edge cases
89+
// Add one test per "Src/ui" example you care about:
90+
// - React.FC<Props> with generic
91+
// - Alias types
92+
// - Intersections
93+
// - Props from imported types, etc.
94+
95+
// Extra tests for the inner functions of the handler: mainly parsed values
96+
// Line 31 in index.js without type
97+
test('handles components without type and falls back to id.name and getTypePath() returns []', () => {
98+
const { path, param } = makePathWithEmptyTypePath();
99+
100+
// Should not throw and should leave params unchanged (no typePath found)
101+
expect(() => customHandler(null, path)).not.toThrow();
102+
expect(path.parentPath.node.init.params[0]).toBe(param);
103+
});
104+
105+
// Line 68 in index.js calls checkForProptypes
106+
test('detects propTypes and returns early', () => {
107+
const { path } = makePathWithPropTypes();
108+
109+
// Should not throw; handler will see hasPropTypes=true and early-return
110+
expect(() => customHandler(null, path)).not.toThrow();
111+
});
112+
113+
// Line 51 in index.js early-returns if hasPropTypes is true
114+
test('calls checkForProptypes and early-returns when propTypes exist', () => {
115+
const { path } = makePathWithNoTypeAnnotation();
116+
117+
customHandler(null, path);
118+
119+
expect(path.parentPath.node.init.params[0]).toBe(undefined);
120+
});
121+
122+
// Line 78 in index.js uses ExportNamedDeclaration
123+
test('uses ExportNamedDeclaration when finding typePath', () => {
124+
const { path, exportedType } = makePathWithExportedType();
125+
126+
customHandler(null, path);
127+
128+
const [typedParam] = path.parentPath.node.init.params;
129+
expect(typedParam.typeAnnotation).toBe(exportedType.declaration);
130+
});
131+
132+
// Line 86 in index.js uses TSInterfaceDeclaration
133+
test('uses TSInterfaceDeclaration when finding typePath', () => {
134+
const { path, interfaceType } = makePathWithInterfaceType();
135+
136+
customHandler(null, path);
137+
138+
const [typedParam] = path.parentPath.node.init.params;
139+
expect(typedParam.typeAnnotation).toBe(interfaceType);
140+
});
141+
});

__tests__/helpers.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
4+
const reactDocs = require('react-docgen');
5+
6+
const customHandler = require('../index.js'); // index.js (module under test)
7+
8+
// Small helper to normalize single vs multi-component output
9+
function getFirstDoc(doc) {
10+
return Array.isArray(doc) ? doc[0] : doc;
11+
}
12+
13+
// Parses a fixture file from __tests__/fixtures and returns the first component doc
14+
function parseFixture(relativePath) {
15+
const filePath = path.join(__dirname, '..', 'fixtures', relativePath);
16+
const src = fs.readFileSync(filePath, 'utf8');
17+
18+
const result = reactDocs.parse(
19+
src,
20+
null,
21+
[customHandler, ...reactDocs.defaultHandlers],
22+
// fix for node.js environment
23+
{
24+
filename: filePath, // 👈 tells react-docgen this is TSX
25+
}
26+
);
27+
28+
return getFirstDoc(result);
29+
}
30+
31+
module.exports = {
32+
parseFixture,
33+
};

fixtures/Button.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from 'react';
2+
3+
/** Props for the Button component */
4+
type ButtonProps = {
5+
/** Text to show inside the button */
6+
label: string;
7+
/** Optional click handler */
8+
onClick?: () => void;
9+
};
10+
11+
/** A simple button component */
12+
export const Button: React.FC<ButtonProps> = ({ label, onClick }) => {
13+
return <button onClick={onClick}>{label}</button>;
14+
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from 'react';
2+
3+
type PropsType = {
4+
label: string;
5+
disabled?: boolean;
6+
};
7+
8+
// @ts-ignore: Ignore prop-types validation errors
9+
PropsType.propTypes = {
10+
label: { required: true },
11+
disabled: { required: false },
12+
};
13+
14+
const ComponentWithPropTypes: React.FC<PropsType> = (props) => {
15+
return <button disabled={props.disabled}>{props.label}</button>;
16+
};
17+
18+
ComponentWithPropTypes.propTypes = {
19+
label: { required: true },
20+
disabled: { required: false },
21+
};
22+
23+
export default ComponentWithPropTypes;

fixtures/ComponentWithoutType.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from 'react';
2+
3+
const ComponentWithoutType = (props) => {
4+
return <div>{props.title}</div>;
5+
};
6+
7+
export default ComponentWithoutType;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react';
2+
3+
export interface ExportedProps {
4+
label: string;
5+
disabled?: boolean;
6+
}
7+
8+
const ComponentWithExportedType: React.FC<ExportedProps> = (props) => {
9+
return <button disabled={props.disabled}>{props.label}</button>;
10+
};
11+
12+
export default ComponentWithExportedType;

fixtures/ForwardedButton.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from 'react';
2+
3+
/** Props for the Button component */
4+
type ButtonProps = {
5+
/** Text to show inside the button */
6+
label: string;
7+
/** Optional click handler */
8+
onClick?: () => void;
9+
};
10+
11+
// TypeError: Cannot read properties of undefined (reading 'length')
12+
export const ForwardedButton = React.forwardRef((props: ButtonProps, ref: any) => (
13+
<button ref={ref} className="ForwardedButton">
14+
{props.children}
15+
</button>
16+
));
17+
18+
/** A simple button component */
19+
const Button: React.FC<ButtonProps> = ({ label, onClick }) => {
20+
// You can now get a ref directly to the DOM button:
21+
const ref = React.createRef();
22+
23+
return <ForwardedButton ref={ref}>{label}</ForwardedButton>;;
24+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react';
2+
3+
interface InterfaceProps {
4+
label: string;
5+
disabled?: boolean;
6+
}
7+
8+
const ComponentWithInterface: React.FC<InterfaceProps> = (props) => {
9+
return <button disabled={props.disabled}>{props.label}</button>;
10+
};
11+
12+
export default ComponentWithInterface;

fixtures/NoParamsComponent.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react';
2+
3+
type NoParamProps = {
4+
label: string;
5+
};
6+
7+
export const NoParamComponent: React.FC<NoParamProps> = () => {
8+
return <div>Hi</div>;
9+
};

0 commit comments

Comments
 (0)