Skip to content

Commit f7f06f9

Browse files
authored
Fix: Prop types are missing for TypeScript (styleguidist#1563)
Closes styleguidist#1551
1 parent 5db70b8 commit f7f06f9

File tree

7 files changed

+194
-129
lines changed

7 files changed

+194
-129
lines changed

src/client/rsg-components/Props/Props.spec.tsx

Lines changed: 151 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React from 'react';
33
import { render } from '@testing-library/react';
44
import { parse } from 'react-docgen';
55
import PropsRenderer, { columns, getRowKey } from './PropsRenderer';
6-
import { unquote, getType, showSpaces, PropDescriptorWithFlow } from './util';
6+
import { unquote, getType, showSpaces, PropDescriptor } from './util';
77

88
const propsToArray = (props: any) => Object.keys(props).map(name => ({ ...props[name], name }));
99

@@ -16,7 +16,7 @@ const getText = (node: { innerHTML: string }): string =>
1616
.trim();
1717

1818
// Test renderers with clean readable snapshot diffs
19-
export default function ColumnsRenderer({ props }: { props: PropDescriptorWithFlow[] }) {
19+
export default function ColumnsRenderer({ props }: { props: PropDescriptor[] }) {
2020
return (
2121
<>
2222
{props.map((row, rowIdx) => (
@@ -58,11 +58,12 @@ function renderJs(propTypes: string[], defaultProps: string[] = []) {
5858
return render(<ColumnsRenderer props={propsToArray(props.props)} />);
5959
}
6060

61-
function renderFlow(propsType: string[], defaultProps: string[] = []) {
61+
function renderFlow(propsType: string[], defaultProps: string[] = [], preparations: string[] = []) {
6262
const props = parse(
6363
`
6464
// @flow
6565
import * as React from 'react';
66+
${preparations.join(';')}
6667
type Props = {
6768
${propsType.join(',')}
6869
};
@@ -84,6 +85,36 @@ function renderFlow(propsType: string[], defaultProps: string[] = []) {
8485
return render(<ColumnsRenderer props={propsToArray(props.props)} />);
8586
}
8687

88+
function renderTypeScript(
89+
propsType: string[],
90+
defaultProps: string[] = [],
91+
preparations: string[] = []
92+
) {
93+
const props = parse(
94+
`
95+
import * as React from 'react';
96+
${preparations.join(';')}
97+
type Props = {
98+
${propsType.join(';')}
99+
};
100+
export default class Cmpnt extends React.Component<Props> {
101+
static defaultProps = {
102+
${defaultProps.join(',')}
103+
}
104+
render() {
105+
}
106+
}
107+
`,
108+
undefined,
109+
undefined,
110+
{ filename: 'Component.tsx' }
111+
);
112+
if (Array.isArray(props)) {
113+
return render(<div />);
114+
}
115+
return render(<ColumnsRenderer props={propsToArray(props.props)} />);
116+
}
117+
87118
describe('PropsRenderer', () => {
88119
test('should render a table', async () => {
89120
const { findAllByRole } = render(
@@ -509,94 +540,107 @@ describe('props columns', () => {
509540
`);
510541
});
511542

512-
test('should render type string', () => {
513-
const { container } = renderFlow(['foo: string']);
514-
515-
expect(getText(container)).toMatchInlineSnapshot(`
516-
"Prop name: foo
517-
Type: string
518-
Default: Required
519-
Description:"
520-
`);
521-
});
522-
523-
test('should render optional type string', () => {
524-
const { container } = renderFlow(['foo?: string']);
525-
526-
expect(getText(container)).toMatchInlineSnapshot(`
527-
"Prop name: foo
528-
Type: string
529-
Default:
530-
Description:"
531-
`);
532-
});
533-
534-
test('should render type string with a default value', () => {
535-
const { container } = renderFlow(['foo?: string'], ['foo: "bar"']);
536-
537-
expect(getText(container)).toMatchInlineSnapshot(`
538-
"Prop name: foo
539-
Type: string
540-
Default: bar
541-
Description:"
542-
`);
543-
});
544-
545-
test('should render literal type', () => {
546-
const { container } = renderFlow(['foo?: "bar"']);
547-
548-
expect(getText(container)).toMatchInlineSnapshot(`
549-
"Prop name: foo
550-
Type: \\"bar\\"
551-
Default:
552-
Description:"
553-
`);
554-
});
555-
556-
test('should render object type with body in tooltip', () => {
557-
const { getByText } = renderFlow(['foo: { bar: string }']);
558-
559-
expect(getByText('object').title).toMatchInlineSnapshot(`"{ bar: string }"`);
560-
});
561-
562-
test('should render function type with body in tooltip', () => {
563-
const { getByText } = renderFlow(['foo: () => void']);
564-
565-
expect(getByText('function').title).toMatchInlineSnapshot(`"() => void"`);
566-
});
567-
568-
test('should render union type with body in tooltip', () => {
569-
const { getByText } = renderFlow(['foo: "bar" | number']);
570-
571-
expect(getByText('union').title).toMatchInlineSnapshot(`"\\"bar\\" | number"`);
572-
});
573-
574-
test('should render enum type when union of literals', () => {
575-
const { container } = renderFlow(['foo: "bar" | "baz"']);
576-
577-
expect(getText(container)).toMatchInlineSnapshot(`
578-
"Prop name: foo
579-
Type: enum
580-
Default: Required
581-
Description:"
582-
`);
583-
});
584-
585-
test('should render tuple type with body in tooltip', () => {
586-
const { getByText } = renderFlow(['foo: ["bar", number]']);
587-
588-
expect(getByText('tuple').title).toMatchInlineSnapshot(`"[\\"bar\\", number]"`);
589-
});
590-
591-
test('should render custom class type', () => {
592-
const { container } = renderFlow(['foo: React.ReactNode']);
593-
594-
expect(getText(container)).toMatchInlineSnapshot(`
595-
"Prop name: foo
596-
Type: React.ReactNode
597-
Default: Required
598-
Description:"
599-
`);
543+
describe.each([
544+
[
545+
'flowType',
546+
renderFlow,
547+
{ enum: { declaration: "type MyEnum = 'One' | 'Two'", expect: { type: 'enum' } } },
548+
],
549+
[
550+
'TypeScript',
551+
renderTypeScript,
552+
{ enum: { declaration: 'enum MyEnum { One, Two }', expect: { type: 'MyEnum' } } },
553+
],
554+
])('%s', (_, renderFn, options) => {
555+
test('should render type string', () => {
556+
const { container } = renderFn(['foo: string']);
557+
558+
expect(getText(container)).toMatchInlineSnapshot(`
559+
"Prop name: foo
560+
Type: string
561+
Default: Required
562+
Description:"
563+
`);
564+
});
565+
566+
test('should render optional type string', () => {
567+
const { container } = renderFn(['foo?: string']);
568+
569+
expect(getText(container)).toMatchInlineSnapshot(`
570+
"Prop name: foo
571+
Type: string
572+
Default:
573+
Description:"
574+
`);
575+
});
576+
577+
test('should render type string with a default value', () => {
578+
const { container } = renderFn(['foo?: string'], ['foo: "bar"']);
579+
580+
expect(getText(container)).toMatchInlineSnapshot(`
581+
"Prop name: foo
582+
Type: string
583+
Default: bar
584+
Description:"
585+
`);
586+
});
587+
588+
test('should render object type with body in tooltip', () => {
589+
const { getByText } = renderFn(['foo: { bar: string }']);
590+
591+
expect(getByText('object').title).toMatchInlineSnapshot(`"{ bar: string }"`);
592+
});
593+
594+
test('should render function type with body in tooltip', () => {
595+
const { getByText } = renderFn(['foo: () => void']);
596+
597+
expect(getByText('function').title).toMatchInlineSnapshot(`"() => void"`);
598+
});
599+
600+
test('should render union type with body in tooltip', () => {
601+
const { getByText } = renderFn(['foo: "bar" | number']);
602+
603+
expect(getByText('union').title).toMatchInlineSnapshot(`"\\"bar\\" | number"`);
604+
});
605+
606+
test('should render enum type', () => {
607+
const { container } = renderFn(['foo: MyEnum'], [], [options.enum.declaration]);
608+
609+
expect(getText(container)).toMatchInlineSnapshot(`
610+
"Prop name: foo
611+
Type: ${options.enum.expect.type}
612+
Default: Required
613+
Description:"
614+
`);
615+
});
616+
617+
test('should render tuple type with body in tooltip', () => {
618+
const { getByText } = renderFn(['foo: ["bar", number]']);
619+
620+
expect(getByText('tuple').title).toMatchInlineSnapshot(`"[\\"bar\\", number]"`);
621+
});
622+
623+
test('should render custom class type', () => {
624+
const { container } = renderFn(['foo: React.ReactNode']);
625+
626+
expect(getText(container)).toMatchInlineSnapshot(`
627+
"Prop name: foo
628+
Type: React.ReactNode
629+
Default: Required
630+
Description:"
631+
`);
632+
});
633+
634+
test('should render unknown when a relevant prop type is not assigned', () => {
635+
const { container } = renderFn([], ['color: "pink"']);
636+
637+
expect(getText(container)).toMatchInlineSnapshot(`
638+
"Prop name: color
639+
Type:
640+
Default: pink
641+
Description:"
642+
`);
643+
});
600644
});
601645
});
602646

@@ -618,13 +662,28 @@ describe('unquote', () => {
618662
});
619663

620664
describe('getType', () => {
621-
test('should return .type or .flowType property', () => {
665+
test('should return not .type but .flowType property', () => {
622666
const result = getType({
623667
type: 'foo',
624668
flowType: 'bar',
625669
} as any);
626670
expect(result).toBe('bar');
627671
});
672+
673+
test('should return not .type but .tsType property', () => {
674+
const result = getType({
675+
type: 'foo',
676+
tsType: 'bar',
677+
} as any);
678+
expect(result).toBe('bar');
679+
});
680+
681+
test('should return .type property', () => {
682+
const result = getType({
683+
type: 'foo',
684+
} as any);
685+
expect(result).toBe('foo');
686+
});
628687
});
629688

630689
describe('showSpaces', () => {

src/client/rsg-components/Props/PropsRenderer.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import Table from 'rsg-components/Table';
1010
import renderTypeColumn from './renderType';
1111
import renderExtra from './renderExtra';
1212
import renderDefault from './renderDefault';
13-
import { PropDescriptorWithFlow } from './util';
13+
import { PropDescriptor } from './util';
1414

15-
function renderDescription(prop: PropDescriptorWithFlow) {
15+
function renderDescription(prop: PropDescriptor) {
1616
const { description, tags = {} } = prop;
1717
const extra = renderExtra(prop);
1818
const args = [...(tags.arg || []), ...(tags.argument || []), ...(tags.param || [])];
@@ -29,7 +29,7 @@ function renderDescription(prop: PropDescriptorWithFlow) {
2929
);
3030
}
3131

32-
function renderName(prop: PropDescriptorWithFlow) {
32+
function renderName(prop: PropDescriptor) {
3333
const { name, tags = {} } = prop;
3434
return <Name deprecated={!!tags.deprecated}>{name}</Name>;
3535
}
@@ -58,7 +58,7 @@ export const columns = [
5858
];
5959

6060
interface PropsProps {
61-
props: PropDescriptorWithFlow[];
61+
props: PropDescriptor[];
6262
}
6363

6464
const PropsRenderer: React.FunctionComponent<PropsProps> = ({ props }) => {

src/client/rsg-components/Props/renderDefault.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import React from 'react';
22
import Text from 'rsg-components/Text';
33
import Code from 'rsg-components/Code';
4-
import { showSpaces, unquote, PropDescriptorWithFlow } from './util';
4+
import { showSpaces, unquote, PropDescriptor } from './util';
55

66
const defaultValueBlacklist = ['null', 'undefined'];
77

8-
export default function renderDefault(prop: PropDescriptorWithFlow): React.ReactNode {
8+
export default function renderDefault(prop: PropDescriptor): React.ReactNode {
99
// Workaround for issue https://github.com/reactjs/react-docgen/issues/221
1010
// If prop has defaultValue it can not be required
1111
if (prop.defaultValue) {
1212
const defaultValueString = showSpaces(unquote(String(prop.defaultValue.value)));
13-
if (prop.type || prop.flowType) {
14-
const propName = prop.type ? prop.type.name : prop.flowType && prop.flowType.type;
13+
if (prop.type || prop.flowType || prop.tsType) {
14+
const propName = prop.type
15+
? prop.type.name
16+
: prop.flowType
17+
? prop.flowType.type
18+
: prop.tsType && prop.tsType.type;
1519

1620
if (defaultValueBlacklist.indexOf(prop.defaultValue.value) > -1) {
1721
return <Code>{defaultValueString}</Code>;

src/client/rsg-components/Props/renderExtra.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import Code from 'rsg-components/Code';
55
import Name from 'rsg-components/Name';
66
import Markdown from 'rsg-components/Markdown';
77

8-
import { unquote, getType, showSpaces, PropDescriptorWithFlow } from './util';
8+
import { unquote, getType, showSpaces, PropDescriptor } from './util';
99
import renderDefault from './renderDefault';
1010
import { renderType } from './renderType';
1111

12-
function renderEnum(prop: PropDescriptorWithFlow): React.ReactNode {
12+
function renderEnum(prop: PropDescriptor): React.ReactNode {
1313
const type = getType(prop);
1414
if (!type) {
1515
return undefined;
@@ -28,7 +28,7 @@ function renderEnum(prop: PropDescriptorWithFlow): React.ReactNode {
2828
);
2929
}
3030

31-
function renderUnion(prop: PropDescriptorWithFlow): React.ReactNode {
31+
function renderUnion(prop: PropDescriptor): React.ReactNode {
3232
const type = getType(prop);
3333
if (!type) {
3434
return undefined;
@@ -47,7 +47,7 @@ function renderUnion(prop: PropDescriptorWithFlow): React.ReactNode {
4747
);
4848
}
4949

50-
function renderShape(props: Record<string, PropDescriptorWithFlow>) {
50+
function renderShape(props: Record<string, PropDescriptor>) {
5151
return Object.keys(props).map(name => {
5252
const prop = props[name];
5353
const defaultValue = renderDefault(prop);
@@ -66,7 +66,7 @@ function renderShape(props: Record<string, PropDescriptorWithFlow>) {
6666
});
6767
}
6868

69-
export default function renderExtra(prop: PropDescriptorWithFlow): React.ReactNode {
69+
export default function renderExtra(prop: PropDescriptor): React.ReactNode {
7070
const type = getType(prop);
7171
if (!prop.type || !type) {
7272
return null;

0 commit comments

Comments
 (0)