diff options
Diffstat (limited to 'preact/test/ts')
-rw-r--r-- | preact/test/ts/Component-test.tsx | 183 | ||||
-rw-r--r-- | preact/test/ts/VNode-test.tsx | 197 | ||||
-rw-r--r-- | preact/test/ts/custom-elements.tsx | 85 | ||||
-rw-r--r-- | preact/test/ts/hoc-test.tsx | 50 | ||||
-rw-r--r-- | preact/test/ts/jsx-namespacce-test.tsx | 16 | ||||
-rw-r--r-- | preact/test/ts/preact-global-test.tsx | 6 | ||||
-rw-r--r-- | preact/test/ts/preact.tsx | 297 | ||||
-rw-r--r-- | preact/test/ts/refs.tsx | 76 | ||||
-rw-r--r-- | preact/test/ts/tsconfig.json | 15 |
9 files changed, 925 insertions, 0 deletions
diff --git a/preact/test/ts/Component-test.tsx b/preact/test/ts/Component-test.tsx new file mode 100644 index 0000000..b037219 --- /dev/null +++ b/preact/test/ts/Component-test.tsx @@ -0,0 +1,183 @@ +import 'mocha'; +import { expect } from 'chai'; +import { createElement, Component, RenderableProps, Fragment } from '../../'; + +// Test `this` binding on event handlers +function onHandler(this: HTMLInputElement, event: any) { + return this.value; +} +const foo = <input onChange={onHandler} />; + +export class ContextComponent extends Component<{ foo: string }> { + getChildContext() { + return { something: 2 }; + } + + render() { + return null; + } +} + +export interface SimpleComponentProps { + initialName: string | null; +} + +export interface SimpleState { + name: string | null; +} + +export class SimpleComponent extends Component< + SimpleComponentProps, + SimpleState +> { + constructor(props: SimpleComponentProps) { + super(props); + this.state = { + name: props.initialName + }; + } + + render() { + if (!this.state.name) { + return null; + } + const { initialName, children } = this.props; + return ( + <div> + <span> + {initialName} / {this.state.name} + </span> + {children} + </div> + ); + } +} + +class DestructuringRenderPropsComponent extends Component< + SimpleComponentProps, + SimpleState +> { + constructor(props: SimpleComponentProps) { + super(props); + this.state = { + name: props.initialName + }; + } + + render({ initialName, children }: RenderableProps<SimpleComponentProps>) { + if (!this.state.name) { + return null; + } + return ( + <span> + {this.props.initialName} / {this.state.name} + </span> + ); + } +} + +interface RandomChildrenComponentProps { + num?: number; + val?: string; + span?: boolean; +} + +class RandomChildrenComponent extends Component<RandomChildrenComponentProps> { + render() { + const { num, val, span } = this.props; + if (num) { + return num; + } + if (val) { + return val; + } + if (span) { + return <span>hi</span>; + } + return null; + } +} + +class StaticComponent extends Component<SimpleComponentProps, SimpleState> { + static getDerivedStateFromProps( + props: SimpleComponentProps, + state: SimpleState + ): Partial<SimpleState> { + return { + ...props, + ...state + }; + } + + static getDerivedStateFromError(err: Error) { + return { + name: err.message + }; + } + + render() { + return null; + } +} + +function MapperItem(props: { foo: number }) { + return <div />; +} + +function Mapper() { + return [1, 2, 3].map(x => <MapperItem foo={x} key={x} />); +} + +describe('Component', () => { + const component = new SimpleComponent({ initialName: 'da name' }); + + it('has state', () => { + expect(component.state.name).to.eq('da name'); + }); + + it('has props', () => { + expect(component.props.initialName).to.eq('da name'); + }); + + it('has no base when not mounted', () => { + expect(component.base).to.not.exist; + }); + + describe('setState', () => { + // No need to execute these tests. because we only need to check if + // the types are working. Executing them would require the DOM. + // TODO: Run TS tests in our standard karma setup + it.skip('can be used with an object', () => { + component.setState({ name: 'another name' }); + }); + + it.skip('can be used with a function', () => { + const updater = (state: any, props: any) => ({ + name: `${state.name} - ${props.initialName}` + }); + component.setState(updater); + }); + }); + + describe('render', () => { + it('can return null', () => { + const comp = new SimpleComponent({ initialName: null }); + const actual = comp.render(); + + expect(actual).to.eq(null); + }); + }); + + describe('Fragment', () => { + it('should render nested Fragments', () => { + var vnode = ( + <Fragment> + <Fragment>foo</Fragment> + bar + </Fragment> + ); + + expect(vnode.type).to.be.equal(Fragment); + }); + }); +}); diff --git a/preact/test/ts/VNode-test.tsx b/preact/test/ts/VNode-test.tsx new file mode 100644 index 0000000..7225901 --- /dev/null +++ b/preact/test/ts/VNode-test.tsx @@ -0,0 +1,197 @@ +import 'mocha'; +import { expect } from 'chai'; +import { + createElement, + Component, + toChildArray, + FunctionalComponent, + ComponentConstructor, + ComponentFactory, + VNode, + ComponentChildren, + cloneElement +} from '../../'; + +function getDisplayType(vnode: VNode | string | number) { + if (typeof vnode === 'string' || typeof vnode == 'number') { + return vnode.toString(); + } else if (typeof vnode.type == 'string') { + return vnode.type; + } else { + return vnode.type.displayName; + } +} + +class SimpleComponent extends Component<{}, {}> { + render() { + return <div>{this.props.children}</div>; + } +} + +const SimpleFunctionalComponent = () => <div />; + +const a: ComponentFactory = SimpleComponent; +const b: ComponentFactory = SimpleFunctionalComponent; + +describe('VNode TS types', () => { + it('is returned by h', () => { + const actual = <div className="wow" />; + expect(actual).to.include.all.keys('type', 'props', 'key'); + }); + + it('has a nodeName of string when html element', () => { + const div = <div>Hi!</div>; + expect(div.type).to.equal('div'); + }); + + it('has a nodeName equal to the construction function when SFC', () => { + const sfc = <SimpleFunctionalComponent />; + expect(sfc.type).to.be.instanceOf(Function); + const constructor = sfc.type as FunctionalComponent<any>; + expect(constructor.name).to.eq('SimpleFunctionalComponent'); + }); + + it('has a nodeName equal to the constructor of a component', () => { + const sfc = <SimpleComponent />; + expect(sfc.type).to.be.instanceOf(Function); + const constructor = sfc.type as ComponentConstructor<any>; + expect(constructor.name).to.eq('SimpleComponent'); + }); + + it('has children which is an array of string or other vnodes', () => { + const comp = ( + <SimpleComponent> + <SimpleComponent>child1</SimpleComponent> + child2 + </SimpleComponent> + ); + + expect(comp.props.children).to.be.instanceOf(Array); + expect(comp.props.children[1]).to.be.a('string'); + }); + + it('children type should work with toChildArray', () => { + const comp: VNode = <SimpleComponent>child1 {1}</SimpleComponent>; + + const children = toChildArray(comp.props.children); + expect(children).to.have.lengthOf(2); + }); + + it('toChildArray should filter out some types', () => { + const compChild = <SimpleComponent />; + const comp: VNode = ( + <SimpleComponent> + a{null} + {true} + {false} + {2} + {undefined} + {['b', 'c']} + {compChild} + </SimpleComponent> + ); + + const children = toChildArray(comp.props.children); + expect(children).to.deep.equal(['a', 2, 'b', 'c', compChild]); + }); + + it('functions like getDisplayType should work', () => { + function TestComp(props: { children?: ComponentChildren }) { + return <div>{props.children}</div>; + } + TestComp.displayName = 'TestComp'; + + const compChild = <TestComp />; + const comp: VNode = ( + <SimpleComponent> + a{null} + {true} + {false} + {2} + {undefined} + {['b', 'c']} + {compChild} + </SimpleComponent> + ); + + const types = toChildArray(comp.props.children).map(getDisplayType); + expect(types).to.deep.equal(['a', '2', 'b', 'c', 'TestComp']); + }); + + it('component should work with cloneElement', () => { + const comp: VNode = ( + <SimpleComponent> + <div>child 1</div> + </SimpleComponent> + ); + const clone: VNode = cloneElement(comp); + + expect(comp.type).to.equal(clone.type); + expect(comp.props).not.to.equal(clone.props); + expect(comp.props).to.deep.equal(clone.props); + }); + + it('component should work with cloneElement using generics', () => { + const comp: VNode<string> = <SimpleComponent></SimpleComponent>; + const clone: VNode<string> = cloneElement<string>(comp); + + expect(comp.type).to.equal(clone.type); + expect(comp.props).not.to.equal(clone.props); + expect(comp.props).to.deep.equal(clone.props); + }); +}); + +class ComponentWithFunctionChild extends Component<{ + children: (num: number) => string; +}> { + render() { + return null; + } +} + +<ComponentWithFunctionChild> + {num => num.toFixed(2)} +</ComponentWithFunctionChild>; + +class ComponentWithStringChild extends Component<{ children: string }> { + render() { + return null; + } +} + +<ComponentWithStringChild>child</ComponentWithStringChild>; + +class ComponentWithNumberChild extends Component<{ children: number }> { + render() { + return null; + } +} + +<ComponentWithNumberChild>{1}</ComponentWithNumberChild>; + +class ComponentWithBooleanChild extends Component<{ children: boolean }> { + render() { + return null; + } +} + +<ComponentWithBooleanChild>{false}</ComponentWithBooleanChild>; + +class ComponentWithNullChild extends Component<{ children: null }> { + render() { + return null; + } +} + +<ComponentWithNullChild>{null}</ComponentWithNullChild>; + +class ComponentWithNumberChildren extends Component<{ children: number[] }> { + render() { + return null; + } +} + +<ComponentWithNumberChildren> + {1} + {2} +</ComponentWithNumberChildren>; diff --git a/preact/test/ts/custom-elements.tsx b/preact/test/ts/custom-elements.tsx new file mode 100644 index 0000000..0f8d29e --- /dev/null +++ b/preact/test/ts/custom-elements.tsx @@ -0,0 +1,85 @@ +import { createElement, Component, createContext } from '../../'; + +declare module '../../' { + namespace createElement.JSX { + interface IntrinsicElements { + // Custom element can use JSX EventHandler definitions + 'clickable-ce': { + optionalAttr?: string; + onClick?: MouseEventHandler<HTMLElement>; + }; + + // Custom Element that extends HTML attributes + 'color-picker': HTMLAttributes & { + // Required attribute + space: 'rgb' | 'hsl' | 'hsv'; + // Optional attribute + alpha?: boolean; + }; + + // Custom Element with custom interface definition + 'custom-whatever': WhateveElAttributes; + } + } +} + +// Whatever Element definition + +interface WhateverElement { + instanceProp: string; +} + +interface WhateverElementEvent { + eventProp: number; +} + +// preact.JSX.HTMLAttributes also appears to work here but for consistency, +// let's use createElement.JSX +interface WhateveElAttributes extends createElement.JSX.HTMLAttributes { + someattribute?: string; + onsomeevent?: (this: WhateverElement, ev: WhateverElementEvent) => void; +} + +// Ensure context still works +const { Provider, Consumer } = createContext({ contextValue: '' }); + +// Sample component that uses custom elements + +class SimpleComponent extends Component { + componentProp = 'componentProp'; + render() { + // Render inside div to ensure standard JSX elements still work + return ( + <Provider value={{ contextValue: 'value' }}> + <div> + <clickable-ce + onClick={e => { + // `this` should be instance of SimpleComponent since this is an + // arrow function + console.log(this.componentProp); + + // Validate `currentTarget` is HTMLElement + console.log('clicked ', e.currentTarget.style.display); + }} + ></clickable-ce> + <color-picker space="rgb" dir="rtl"></color-picker> + <custom-whatever + dir="auto" // Inherited prop from HTMLAttributes + someattribute="string" + onsomeevent={function(e) { + // Validate `this` and `e` are the right type + console.log('clicked', this.instanceProp, e.eventProp); + }} + ></custom-whatever> + + {/* Ensure context still works */} + <Consumer> + {({ contextValue }) => contextValue.toLowerCase()} + </Consumer> + </div> + </Provider> + ); + } +} + +const component = <SimpleComponent />; diff --git a/preact/test/ts/hoc-test.tsx b/preact/test/ts/hoc-test.tsx new file mode 100644 index 0000000..455d9e0 --- /dev/null +++ b/preact/test/ts/hoc-test.tsx @@ -0,0 +1,50 @@ +import { expect } from 'chai'; +import { + createElement, + ComponentFactory, + ComponentConstructor, + Component +} from '../../'; +import { SimpleComponent, SimpleComponentProps } from './Component-test'; + +export interface highlightedProps { + isHighlighted: boolean; +} + +export function highlighted<T>( + Wrappable: ComponentFactory<T> +): ComponentConstructor<T & highlightedProps> { + return class extends Component<T & highlightedProps> { + constructor(props: T & highlightedProps) { + super(props); + } + + render() { + let className = this.props.isHighlighted ? 'highlighted' : ''; + return ( + <div className={className}> + <Wrappable {...this.props} /> + </div> + ); + } + + toString() { + return `Highlighted ${Wrappable.name}`; + } + }; +} + +const HighlightedSimpleComponent = highlighted<SimpleComponentProps>( + SimpleComponent +); + +describe('hoc', () => { + it('wraps the given component', () => { + const highlight = new HighlightedSimpleComponent({ + initialName: 'initial name', + isHighlighted: true + }); + + expect(highlight.toString()).to.eq('Highlighted SimpleComponent'); + }); +}); diff --git a/preact/test/ts/jsx-namespacce-test.tsx b/preact/test/ts/jsx-namespacce-test.tsx new file mode 100644 index 0000000..d6e10bd --- /dev/null +++ b/preact/test/ts/jsx-namespacce-test.tsx @@ -0,0 +1,16 @@ +import { createElement, Component } from '../../'; + +// declare global JSX types that should not be mixed with preact's internal types +declare global { + namespace JSX { + interface Element { + unknownProperty: string; + } + } +} + +class SimpleComponent extends Component { + render() { + return <div>It works</div>; + } +} diff --git a/preact/test/ts/preact-global-test.tsx b/preact/test/ts/preact-global-test.tsx new file mode 100644 index 0000000..e6c3286 --- /dev/null +++ b/preact/test/ts/preact-global-test.tsx @@ -0,0 +1,6 @@ +import { createElement } from '../../src'; + +// Test that preact types are available via the global `preact` namespace. + +let component: preact.ComponentChild; +component = <div>Hello World</div>; diff --git a/preact/test/ts/preact.tsx b/preact/test/ts/preact.tsx new file mode 100644 index 0000000..9779d41 --- /dev/null +++ b/preact/test/ts/preact.tsx @@ -0,0 +1,297 @@ +import { + createElement, + render, + Component, + ComponentProps, + FunctionalComponent, + AnyComponent, + h +} from '../../'; + +interface DummyProps { + initialInput: string; +} + +interface DummyState { + input: string; +} + +class DummyComponent extends Component<DummyProps, DummyState> { + constructor(props: DummyProps) { + super(props); + this.state = { + input: `x${this.props}x` + }; + } + + private setRef = (el: AnyComponent<any>) => { + console.log(el); + }; + + render({ initialInput }: DummyProps, { input }: DummyState) { + return ( + <div> + <DummerComponent initialInput={initialInput} input={input} /> + {/* Can specify all Preact attributes on a typed FunctionalComponent */} + <ComponentWithChildren + initialInput={initialInput} + input={input} + key="1" + ref={this.setRef} + /> + </div> + ); + } +} + +interface DummerComponentProps extends DummyProps, DummyState {} + +function DummerComponent({ input, initialInput }: DummerComponentProps) { + return ( + <div> + Input: {input}, initial: {initialInput} + </div> + ); +} + +render(createElement('div', { title: 'test', key: '1' }), document); +render( + createElement(DummyComponent, { initialInput: 'The input', key: '1' }), + document +); +render( + createElement(DummerComponent, { + initialInput: 'The input', + input: 'New input', + key: '1' + }), + document +); +render(h('div', { title: 'test', key: '1' }), document); +render(h(DummyComponent, { initialInput: 'The input', key: '1' }), document); +render( + h(DummerComponent, { + initialInput: 'The input', + input: 'New input', + key: '1' + }), + document +); + +// Accessing children +const ComponentWithChildren: FunctionalComponent<DummerComponentProps> = ({ + input, + initialInput, + children +}) => { + return ( + <div> + <span>{initialInput}</span> + <span>{input}</span> + <span>{children}</span> + </div> + ); +}; + +const UseOfComponentWithChildren = () => { + return ( + <ComponentWithChildren initialInput="initial" input="input"> + <span>child 1</span> + <span>child 2</span> + </ComponentWithChildren> + ); +}; + +// using ref and or jsx +class ComponentUsingRef extends Component<any, any> { + private array: string[]; + private refs: (Element | null)[] = []; + + constructor() { + super(); + this.array = ['1', '2']; + } + + render() { + this.refs = []; + return ( + <div jsx> + {this.array.map(el => ( + <span ref={this.setRef}>{el}</span> + ))} + + {/* Can specify Preact attributes on a component */} + <DummyComponent initialInput="1" key="1" ref={this.setRef} /> + </div> + ); + } + + private setRef = (el: Element | null) => { + this.refs.push(el); + }; +} + +// using lifecycles +class ComponentWithLifecycle extends Component<DummyProps, DummyState> { + render() { + return <div>Hi</div>; + } + + componentWillMount() { + console.log('componentWillMount'); + } + + componentDidMount() { + console.log('componentDidMount'); + } + + componentWillUnmount() { + console.log('componentWillUnmount'); + } + + componentWillReceiveProps(nextProps: DummyProps, nextCtx: any) { + const { initialInput } = nextProps; + console.log('componentWillReceiveProps', initialInput, nextCtx); + } + + shouldComponentUpdate( + nextProps: DummyProps, + nextState: DummyState, + nextContext: any + ) { + return false; + } + + componentWillUpdate( + nextProps: DummyProps, + nextState: DummyState, + nextContext: any + ) { + console.log('componentWillUpdate', nextProps, nextState, nextContext); + } + + componentDidUpdate( + previousProps: DummyProps, + previousState: DummyState, + previousContext: any + ) { + console.log( + 'componentDidUpdate', + previousProps, + previousState, + previousContext + ); + } +} + +// Default props: JSX.LibraryManagedAttributes + +class DefaultProps extends Component<{ text: string; bool: boolean }> { + static defaultProps = { + text: 'hello' + }; + + render() { + return <div>{this.props.text}</div>; + } +} + +const d1 = <DefaultProps bool={false} text="foo" />; +const d2 = <DefaultProps bool={false} />; + +class DefaultPropsWithUnion extends Component< + { default: boolean } & ( + | { + type: 'string'; + str: string; + } + | { + type: 'number'; + num: number; + } + ) +> { + static defaultProps = { + default: true + }; + + render() { + return <div />; + } +} + +const d3 = <DefaultPropsWithUnion type="string" str={'foo'} />; +const d4 = <DefaultPropsWithUnion type="number" num={0xf00} />; +const d5 = <DefaultPropsWithUnion type="string" str={'foo'} default={false} />; +const d6 = <DefaultPropsWithUnion type="number" num={0xf00} default={false} />; + +class DefaultUnion extends Component< + | { + type: 'number'; + num: number; + } + | { + type: 'string'; + str: string; + } +> { + static defaultProps = { + type: 'number', + num: 1 + }; + + render() { + return <div />; + } +} + +const d7 = <DefaultUnion />; +const d8 = <DefaultUnion num={1} />; +const d9 = <DefaultUnion type="number" />; +const d10 = <DefaultUnion type="string" str="foo" />; + +class ComponentWithDefaultProps extends Component<{ value: string }> { + static defaultProps = { value: '' }; + render() { + return <div>{this.props.value}</div>; + } +} + +const withDefaultProps = <ComponentWithDefaultProps />; + +interface PartialState { + foo: string; + bar: number; +} + +class ComponentWithPartialSetState extends Component<{}, PartialState> { + render({}, { foo, bar }: PartialState) { + return ( + <button onClick={() => this.handleClick('foo')}> + {foo}-{bar} + </button> + ); + } + handleClick = (value: keyof PartialState) => { + this.setState({ [value]: 'updated' }); + }; +} + +const withPartialSetState = <ComponentWithPartialSetState />; + +let functionalProps: ComponentProps<typeof DummerComponent> = { + initialInput: '', + input: '' +}; + +let classProps: ComponentProps<typeof DummyComponent> = { + initialInput: '' +}; + +let elementProps: ComponentProps<'button'> = { + type: 'button' +}; + +// Typing of style property +const acceptsNumberAsLength = <div style={{ marginTop: 20 }} />; +const acceptsStringAsLength = <div style={{ marginTop: '20px' }} />; diff --git a/preact/test/ts/refs.tsx b/preact/test/ts/refs.tsx new file mode 100644 index 0000000..9edb730 --- /dev/null +++ b/preact/test/ts/refs.tsx @@ -0,0 +1,76 @@ +import { + createElement, + Component, + createRef, + FunctionalComponent, + Fragment, + RefObject, + RefCallback +} from '../../'; + +// Test Fixtures +const Foo: FunctionalComponent = () => <span>Foo</span>; +class Bar extends Component { + render() { + return <span>Bar</span>; + } +} + +// Using Refs +class CallbackRef extends Component { + divRef: RefCallback<HTMLDivElement> = div => { + if (div !== null) { + console.log(div.tagName); + } + }; + fooRef: RefCallback<Component> = foo => { + if (foo !== null) { + console.log(foo.base); + } + }; + barRef: RefCallback<Bar> = bar => { + if (bar !== null) { + console.log(bar.base); + } + }; + + render() { + return ( + <Fragment> + <div ref={this.divRef} /> + <Foo ref={this.fooRef} /> + <Bar ref={this.barRef} /> + </Fragment> + ); + } +} + +class CreateRefComponent extends Component { + private divRef: RefObject<HTMLDivElement> = createRef(); + private fooRef: RefObject<Component> = createRef(); + private barRef: RefObject<Bar> = createRef(); + + componentDidMount() { + if (this.divRef.current != null) { + console.log(this.divRef.current.tagName); + } + + if (this.fooRef.current != null) { + console.log(this.fooRef.current.base); + } + + if (this.barRef.current != null) { + console.log(this.barRef.current.base); + } + } + + render() { + return ( + <Fragment> + <div ref={this.divRef} /> + <Foo ref={this.fooRef} /> + <Bar ref={this.barRef} /> + </Fragment> + ); + } +} diff --git a/preact/test/ts/tsconfig.json b/preact/test/ts/tsconfig.json new file mode 100644 index 0000000..36621f3 --- /dev/null +++ b/preact/test/ts/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "es6", + "moduleResolution": "node", + "lib": ["es6", "dom"], + "strict": true, + "typeRoots": ["../../"], + "types": [], + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "jsxFactory": "createElement" + }, + "include": ["./**/*.ts", "./**/*.tsx"] +} |