Skip to content

Commit

Permalink
Fix: Prop types are missing for TypeScript (#1563)
Browse files Browse the repository at this point in the history
Closes #1551
  • Loading branch information
mitsuruog committed Mar 17, 2020
1 parent 5db70b8 commit f7f06f9
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 129 deletions.
243 changes: 151 additions & 92 deletions src/client/rsg-components/Props/Props.spec.tsx
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
import { render } from '@testing-library/react';
import { parse } from 'react-docgen';
import PropsRenderer, { columns, getRowKey } from './PropsRenderer';
import { unquote, getType, showSpaces, PropDescriptorWithFlow } from './util';
import { unquote, getType, showSpaces, PropDescriptor } from './util';

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

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

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

function renderFlow(propsType: string[], defaultProps: string[] = []) {
function renderFlow(propsType: string[], defaultProps: string[] = [], preparations: string[] = []) {
const props = parse(
`
// @flow
import * as React from 'react';
${preparations.join(';')}
type Props = {
${propsType.join(',')}
};
Expand All @@ -84,6 +85,36 @@ function renderFlow(propsType: string[], defaultProps: string[] = []) {
return render(<ColumnsRenderer props={propsToArray(props.props)} />);
}

function renderTypeScript(
propsType: string[],
defaultProps: string[] = [],
preparations: string[] = []
) {
const props = parse(
`
import * as React from 'react';
${preparations.join(';')}
type Props = {
${propsType.join(';')}
};
export default class Cmpnt extends React.Component<Props> {
static defaultProps = {
${defaultProps.join(',')}
}
render() {
}
}
`,
undefined,
undefined,
{ filename: 'Component.tsx' }
);
if (Array.isArray(props)) {
return render(<div />);
}
return render(<ColumnsRenderer props={propsToArray(props.props)} />);
}

describe('PropsRenderer', () => {
test('should render a table', async () => {
const { findAllByRole } = render(
Expand Down Expand Up @@ -509,94 +540,107 @@ describe('props columns', () => {
`);
});

test('should render type string', () => {
const { container } = renderFlow(['foo: string']);

expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: string
Default: Required
Description:"
`);
});

test('should render optional type string', () => {
const { container } = renderFlow(['foo?: string']);

expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: string
Default:
Description:"
`);
});

test('should render type string with a default value', () => {
const { container } = renderFlow(['foo?: string'], ['foo: "bar"']);

expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: string
Default: bar
Description:"
`);
});

test('should render literal type', () => {
const { container } = renderFlow(['foo?: "bar"']);

expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: \\"bar\\"
Default:
Description:"
`);
});

test('should render object type with body in tooltip', () => {
const { getByText } = renderFlow(['foo: { bar: string }']);

expect(getByText('object').title).toMatchInlineSnapshot(`"{ bar: string }"`);
});

test('should render function type with body in tooltip', () => {
const { getByText } = renderFlow(['foo: () => void']);

expect(getByText('function').title).toMatchInlineSnapshot(`"() => void"`);
});

test('should render union type with body in tooltip', () => {
const { getByText } = renderFlow(['foo: "bar" | number']);

expect(getByText('union').title).toMatchInlineSnapshot(`"\\"bar\\" | number"`);
});

test('should render enum type when union of literals', () => {
const { container } = renderFlow(['foo: "bar" | "baz"']);

expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: enum
Default: Required
Description:"
`);
});

test('should render tuple type with body in tooltip', () => {
const { getByText } = renderFlow(['foo: ["bar", number]']);

expect(getByText('tuple').title).toMatchInlineSnapshot(`"[\\"bar\\", number]"`);
});

test('should render custom class type', () => {
const { container } = renderFlow(['foo: React.ReactNode']);

expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: React.ReactNode
Default: Required
Description:"
`);
describe.each([
[
'flowType',
renderFlow,
{ enum: { declaration: "type MyEnum = 'One' | 'Two'", expect: { type: 'enum' } } },
],
[
'TypeScript',
renderTypeScript,
{ enum: { declaration: 'enum MyEnum { One, Two }', expect: { type: 'MyEnum' } } },
],
])('%s', (_, renderFn, options) => {
test('should render type string', () => {
const { container } = renderFn(['foo: string']);

expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: string
Default: Required
Description:"
`);
});

test('should render optional type string', () => {
const { container } = renderFn(['foo?: string']);

expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: string
Default:
Description:"
`);
});

test('should render type string with a default value', () => {
const { container } = renderFn(['foo?: string'], ['foo: "bar"']);

expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: string
Default: bar
Description:"
`);
});

test('should render object type with body in tooltip', () => {
const { getByText } = renderFn(['foo: { bar: string }']);

expect(getByText('object').title).toMatchInlineSnapshot(`"{ bar: string }"`);
});

test('should render function type with body in tooltip', () => {
const { getByText } = renderFn(['foo: () => void']);

expect(getByText('function').title).toMatchInlineSnapshot(`"() => void"`);
});

test('should render union type with body in tooltip', () => {
const { getByText } = renderFn(['foo: "bar" | number']);

expect(getByText('union').title).toMatchInlineSnapshot(`"\\"bar\\" | number"`);
});

test('should render enum type', () => {
const { container } = renderFn(['foo: MyEnum'], [], [options.enum.declaration]);

expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: ${options.enum.expect.type}
Default: Required
Description:"
`);
});

test('should render tuple type with body in tooltip', () => {
const { getByText } = renderFn(['foo: ["bar", number]']);

expect(getByText('tuple').title).toMatchInlineSnapshot(`"[\\"bar\\", number]"`);
});

test('should render custom class type', () => {
const { container } = renderFn(['foo: React.ReactNode']);

expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: React.ReactNode
Default: Required
Description:"
`);
});

test('should render unknown when a relevant prop type is not assigned', () => {
const { container } = renderFn([], ['color: "pink"']);

expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: color
Type:
Default: pink
Description:"
`);
});
});
});

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

describe('getType', () => {
test('should return .type or .flowType property', () => {
test('should return not .type but .flowType property', () => {
const result = getType({
type: 'foo',
flowType: 'bar',
} as any);
expect(result).toBe('bar');
});

test('should return not .type but .tsType property', () => {
const result = getType({
type: 'foo',
tsType: 'bar',
} as any);
expect(result).toBe('bar');
});

test('should return .type property', () => {
const result = getType({
type: 'foo',
} as any);
expect(result).toBe('foo');
});
});

describe('showSpaces', () => {
Expand Down
8 changes: 4 additions & 4 deletions src/client/rsg-components/Props/PropsRenderer.tsx
Expand Up @@ -10,9 +10,9 @@ import Table from 'rsg-components/Table';
import renderTypeColumn from './renderType';
import renderExtra from './renderExtra';
import renderDefault from './renderDefault';
import { PropDescriptorWithFlow } from './util';
import { PropDescriptor } from './util';

function renderDescription(prop: PropDescriptorWithFlow) {
function renderDescription(prop: PropDescriptor) {
const { description, tags = {} } = prop;
const extra = renderExtra(prop);
const args = [...(tags.arg || []), ...(tags.argument || []), ...(tags.param || [])];
Expand All @@ -29,7 +29,7 @@ function renderDescription(prop: PropDescriptorWithFlow) {
);
}

function renderName(prop: PropDescriptorWithFlow) {
function renderName(prop: PropDescriptor) {
const { name, tags = {} } = prop;
return <Name deprecated={!!tags.deprecated}>{name}</Name>;
}
Expand Down Expand Up @@ -58,7 +58,7 @@ export const columns = [
];

interface PropsProps {
props: PropDescriptorWithFlow[];
props: PropDescriptor[];
}

const PropsRenderer: React.FunctionComponent<PropsProps> = ({ props }) => {
Expand Down
12 changes: 8 additions & 4 deletions src/client/rsg-components/Props/renderDefault.tsx
@@ -1,17 +1,21 @@
import React from 'react';
import Text from 'rsg-components/Text';
import Code from 'rsg-components/Code';
import { showSpaces, unquote, PropDescriptorWithFlow } from './util';
import { showSpaces, unquote, PropDescriptor } from './util';

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

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

if (defaultValueBlacklist.indexOf(prop.defaultValue.value) > -1) {
return <Code>{defaultValueString}</Code>;
Expand Down
10 changes: 5 additions & 5 deletions src/client/rsg-components/Props/renderExtra.tsx
Expand Up @@ -5,11 +5,11 @@ import Code from 'rsg-components/Code';
import Name from 'rsg-components/Name';
import Markdown from 'rsg-components/Markdown';

import { unquote, getType, showSpaces, PropDescriptorWithFlow } from './util';
import { unquote, getType, showSpaces, PropDescriptor } from './util';
import renderDefault from './renderDefault';
import { renderType } from './renderType';

function renderEnum(prop: PropDescriptorWithFlow): React.ReactNode {
function renderEnum(prop: PropDescriptor): React.ReactNode {
const type = getType(prop);
if (!type) {
return undefined;
Expand All @@ -28,7 +28,7 @@ function renderEnum(prop: PropDescriptorWithFlow): React.ReactNode {
);
}

function renderUnion(prop: PropDescriptorWithFlow): React.ReactNode {
function renderUnion(prop: PropDescriptor): React.ReactNode {
const type = getType(prop);
if (!type) {
return undefined;
Expand All @@ -47,7 +47,7 @@ function renderUnion(prop: PropDescriptorWithFlow): React.ReactNode {
);
}

function renderShape(props: Record<string, PropDescriptorWithFlow>) {
function renderShape(props: Record<string, PropDescriptor>) {
return Object.keys(props).map(name => {
const prop = props[name];
const defaultValue = renderDefault(prop);
Expand All @@ -66,7 +66,7 @@ function renderShape(props: Record<string, PropDescriptorWithFlow>) {
});
}

export default function renderExtra(prop: PropDescriptorWithFlow): React.ReactNode {
export default function renderExtra(prop: PropDescriptor): React.ReactNode {
const type = getType(prop);
if (!prop.type || !type) {
return null;
Expand Down

0 comments on commit f7f06f9

Please sign in to comment.