diff --git a/jest/jest.d.ts b/jest/jest.d.ts new file mode 100644 index 0000000000..31fcc07dfc --- /dev/null +++ b/jest/jest.d.ts @@ -0,0 +1,71 @@ +declare var jasmine: any; + +declare function afterEach(fn: any): any; +declare function beforeEach(fn: any): any; +declare function describe(name: string, fn: any): void; +declare var it: { + (name: string, fn: any): void; + only: (name: string, fn: any) => void; +} +declare function expect(val: any): Expect; +declare var jest: Jest; +declare function pit(name: string, fn: any): void; +declare function spyOn(obj: any, key: string): any; +declare function xdescribe(name: string, fn: any): void; +declare function xit(name: string, fn: any): void; + +interface Expect { + not: Expect + toThrow(message?: string): void + toBe(value: any): void + toEqual(value: any): void + toBeFalsy(): void + toBeTruthy(): void + toBeNull(): void + toBeUndefined(): void + toBeDefined(): void + toMatch(regexp: RegExp): void + toContain(string: string): void + toBeCloseTo(number: number, delta: number): void + toBeGreaterThan(number: number): void + toBeLessThan(number: number): void + toBeCalled(): void + toBeCalledWith(...arguments): void + lastCalledWith(...arguments): void +} + +interface Jest { + autoMockOff(): void + autoMockOn(): void + clearAllTimers(): void + dontMock(moduleName: string): void + genMockFromModule(moduleObj: Object): Object + genMockFunction(): MockFunction + genMockFn(): MockFunction + mock(moduleName: string): void + runAllTicks(): void + runAllTimers(): void + runOnlyPendingTimers(): void + setMock(moduleName: string, moduleExports: Object): void +} + +interface MockFunction { + (...arguments): any + mock: { + calls: Array> + instances: Array + } + mockClear(): void + mockImplementation(fn: Function): MockFunction + mockImpl(fn: Function): MockFunction + mockReturnThis(): MockFunction + mockReturnValue(value: any): MockFunction + mockReturnValueOnce(value: any): MockFunction +} + +// Allow importing jasmine-check +declare module 'jasmine-check' { + export function install(global?: any): void; +} +declare var check: any; +declare var gen: any; diff --git a/jest/preprocessor.js b/jest/preprocessor.js index 37fdd6ac5f..d52482c816 100644 --- a/jest/preprocessor.js +++ b/jest/preprocessor.js @@ -3,12 +3,29 @@ var ReactTools = require('../main.js'); var coffee = require('coffee-script'); +var ts = require('ts-compiler'); module.exports = { process: function(src, path) { if (path.match(/\.coffee$/)) { return coffee.compile(src, {'bare': true}); } + if (path.match(/\.ts$/) && !path.match(/\.d\.ts$/)) { + ts.compile([path], { + skipWrite: true, + module: 'commonjs' + }, function(err, results) { + if (err) { + throw err; + } + results.forEach(function(file) { + // This is gross, but jest doesn't provide an asynchronous way to + // process a module, and ts currently runs syncronously. + src = file.text; + }); + }); + return src; + } return ReactTools.transform(src, {harmony: true}); } }; diff --git a/package.json b/package.json index 1d2bb1b806..917c1dc51b 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "populist": "~0.1.6", "recast": "^0.9.11", "sauce-tunnel": "~1.1.0", + "ts-compiler": "^2.0.0", "tmp": "~0.0.18", "uglify-js": "~2.4.0", "uglifyify": "^2.4.0", @@ -82,7 +83,8 @@ "setupEnvScriptFile": "jest/environment.js", "testFileExtensions": [ "js", - "coffee" + "coffee", + "ts" ], "modulePathIgnorePatterns": [ "/build/", diff --git a/src/modern/class/__tests__/ReactClassEquivalence-test.js b/src/modern/class/__tests__/ReactClassEquivalence-test.js index ff48f5373a..cd342ad487 100644 --- a/src/modern/class/__tests__/ReactClassEquivalence-test.js +++ b/src/modern/class/__tests__/ReactClassEquivalence-test.js @@ -19,9 +19,14 @@ describe('ReactClassEquivalence', function() { var es6 = () => require('./ReactES6Class-test.js'); var coffee = () => require('./ReactCoffeeScriptClass-test.coffee'); + var ts = () => require('./ReactTypeScriptClass-test.ts'); - it('tests the same thing for es6 classes and coffee script', function() { + it('tests the same thing for es6 classes and CoffeeScript', function() { expect(coffee).toEqualSpecsIn(es6); }); + it('tests the same thing for es6 classes and TypeScript', function() { + expect(ts).toEqualSpecsIn(es6); + }); + }); diff --git a/src/modern/class/__tests__/ReactTypeScriptClass-test.ts b/src/modern/class/__tests__/ReactTypeScriptClass-test.ts new file mode 100644 index 0000000000..1cbe9e6427 --- /dev/null +++ b/src/modern/class/__tests__/ReactTypeScriptClass-test.ts @@ -0,0 +1,491 @@ +/*! + * Copyright 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/// +/// + +import React = require('React'); + +// Before Each + +var container; +var attachedListener = null; +var renderedName = null; + +class Inner extends React.Component { + getName() { + return this.props.name; + } + render() { + attachedListener = this.props.onClick; + renderedName = this.props.name; + return React.createElement('div', { className: this.props.name }); + } +}; + +function test(element, expectedTag, expectedClassName) { + var instance = React.render(element, container); + expect(container.firstChild).not.toBeNull(); + expect(container.firstChild.tagName).toBe(expectedTag); + expect(container.firstChild.className).toBe(expectedClassName); + return instance; +} + +// Classes need to be declared at the top level scope, so we declare all the +// classes that will be used by the tests below, instead of inlining them. +// TODO: Consider redesigning this using modules so that we can use non-unique +// names of classes and bundle them with the test code. + +// it preserves the name of the class for use in error messages +// it throws if no render function is defined +class Empty extends React.Component { } + +// it renders a simple stateless component with prop +class SimpleStateless { + props : any; + render() { + return React.createElement(Inner, {name: this.props.bar}); + } +} + +// it renders based on state using initial values in this.props +class InitialState extends React.Component { + state = { + bar: this.props.initialValue + }; + render() { + return React.createElement('span', {className: this.state.bar}); + } +} + +// it renders based on state using props in the constructor +class StateBasedOnProps extends React.Component { + constructor(props) { + super(props); + this.state = { bar: props.initialValue }; + } + changeState() { + this.setState({ bar: 'bar' }); + } + render() { + if (this.state.bar === 'foo') { + return React.createElement('div', {className: 'foo'}); + } + return React.createElement('span', {className: this.state.bar}); + } +} + +// it renders based on context in the constructor +class StateBasedOnContext extends React.Component { + static contextTypes = { + tag: React.PropTypes.string, + className: React.PropTypes.string + } + state = { + tag: this.context.tag, + className: this.context.className + } + render() { + var Tag = this.state.tag; + return React.createElement(Tag, {className: this.state.className}); + } +} + +class ProvideChildContextTypes extends React.Component { + static childContextTypes = { + tag: React.PropTypes.string, + className: React.PropTypes.string + } + getChildContext() { + return { tag: 'span', className: 'foo' }; + } + render() { + return React.createElement(StateBasedOnContext); + } +} + +// it renders only once when setting state in componentWillMount +var renderCount = 0; +class RenderOnce extends React.Component { + state = { + bar: this.props.initialValue + } + componentWillMount() { + this.setState({ bar: 'bar' }); + } + render() { + renderCount++; + return React.createElement('span', {className: this.state.bar}); + } +} + +// it should throw with non-object in the initial state property +class ArrayState { + state = ['an array'] + render() { + return React.createElement('span'); + } +} +class StringState { + state = 'a string' + render() { + return React.createElement('span'); + } +} +class NumberState { + state = 1234 + render() { + return React.createElement('span'); + } +} + +// it should render with null in the initial state property +class NullState extends React.Component { + state = null + render() { + return React.createElement('span'); + } +} + +// it setState through an event handler +class BoundEventHandler extends React.Component { + state = { + bar: this.props.initialValue + } + handleClick = () => { + this.setState({ bar: 'bar' }); + } + render() { + return ( + React.createElement(Inner, { + name: this.state.bar, + onClick: this.handleClick + }) + ); + } +} + +// it should not implicitly bind event handlers +class UnboundEventHandler extends React.Component { + state = { + bar: this.props.initialValue + } + handleClick() { + this.setState({ bar: 'bar' }); + } + render() { + return React.createElement( + Inner, { name: this.state.bar, onClick: this.handleClick } + ); + } +} + +// it renders using forceUpdate even when there is no state +class ForceUpdateWithNoState extends React.Component { + mutativeValue : string = this.props.initialValue + handleClick() { + this.mutativeValue = 'bar'; + this.forceUpdate(); + } + render() { + return ( + React.createElement(Inner, { + name: this.mutativeValue, + onClick: this.handleClick.bind(this)} + ) + ); + } +} + +// it will call all the normal life cycle methods +var lifeCycles = []; +class NormalLifeCycles { + props : any + state = {} + componentWillMount() { + lifeCycles.push('will-mount'); + } + componentDidMount() { + lifeCycles.push('did-mount'); + } + componentWillReceiveProps(nextProps) { + lifeCycles.push('receive-props', nextProps); + } + shouldComponentUpdate(nextProps, nextState) { + lifeCycles.push('should-update', nextProps, nextState); + return true; + } + componentWillUpdate(nextProps, nextState) { + lifeCycles.push('will-update', nextProps, nextState); + } + componentDidUpdate(prevProps, prevState) { + lifeCycles.push('did-update', prevProps, prevState); + } + componentWillUnmount() { + lifeCycles.push('will-unmount'); + } + render() { + return React.createElement('span', {className: this.props.value}); + } +} + +// warns when classic properties are defined on the instance, +// but does not invoke them. +var getInitialStateWasCalled = false; +class ClassicProperties extends React.Component { + contextTypes = {}; + propTypes = {}; + getInitialState() { + getInitialStateWasCalled = true; + return {}; + } + render() { + return React.createElement('span', {className: 'foo'}); + } +} + +// it should warn when mispelling shouldComponentUpdate +class NamedComponent { + componentShouldUpdate() { + return false; + } + render() { + return React.createElement('span', {className: 'foo'}); + } +} + +// it supports this.context passed via getChildContext +class ReadContext extends React.Component { + static contextTypes = { bar: React.PropTypes.string }; + render() { + return React.createElement('div', { className: this.context.bar }); + } +} +class ProvideContext { + static childContextTypes = { bar: React.PropTypes.string }; + getChildContext() { + return { bar: 'bar-through-context' }; + } + render() { + return React.createElement(ReadContext); + } +} + +// it supports classic refs +class ClassicRefs { + render() { + return React.createElement(Inner, {name: 'foo', ref: 'inner'}); + } +} + +// Describe the actual test cases. + +describe('ReactTypeScriptClass', function() { + + beforeEach(function() { + container = document.createElement('div'); + attachedListener = null; + renderedName = null; + }); + + it('preserves the name of the class for use in error messages', function() { + expect(Empty.name).toBe('Empty'); + }); + + it('throws if no render function is defined', function() { + expect(() => React.render(React.createElement(Empty), container)).toThrow(); + }); + + it('renders a simple stateless component with prop', function() { + test(React.createElement(SimpleStateless, {bar: 'foo'}), 'DIV', 'foo'); + test(React.createElement(SimpleStateless, {bar: 'bar'}), 'DIV', 'bar'); + }); + + it('renders based on state using initial values in this.props', function() { + test( + React.createElement(InitialState, {initialValue: 'foo'}), + 'SPAN', + 'foo' + ); + }); + + it('renders based on state using props in the constructor', function() { + var instance = test( + React.createElement(StateBasedOnProps, {initialValue: 'foo'}), + 'DIV', + 'foo' + ); + instance.changeState(); + test(React.createElement(StateBasedOnProps), 'SPAN', 'bar'); + }); + + it('renders based on context in the constructor', function() { + test(React.createElement(ProvideChildContextTypes), 'SPAN', 'foo'); + }); + + it('renders only once when setting state in componentWillMount', function() { + renderCount = 0; + test(React.createElement(RenderOnce, {initialValue: 'foo'}), 'SPAN', 'bar'); + expect(renderCount).toBe(1); + }); + + it('should throw with non-object in the initial state property', function() { + expect(() => test(React.createElement(ArrayState), 'span', '')) + .toThrow( + 'Invariant Violation: ArrayState.state: ' + + 'must be set to an object or null' + ); + expect(() => test(React.createElement(StringState), 'span', '')) + .toThrow( + 'Invariant Violation: StringState.state: ' + + 'must be set to an object or null' + ); + expect(() => test(React.createElement(NumberState), 'span', '')) + .toThrow( + 'Invariant Violation: NumberState.state: ' + + 'must be set to an object or null' + ); + }); + + it('should render with null in the initial state property', function() { + test(React.createElement(NullState), 'SPAN', ''); + }); + + it('setState through an event handler', function() { + test( + React.createElement(BoundEventHandler, {initialValue: 'foo'}), + 'DIV', + 'foo' + ); + attachedListener(); + expect(renderedName).toBe('bar'); + }); + + it('should not implicitly bind event handlers', function() { + test( + React.createElement(UnboundEventHandler, {initialValue: 'foo'}), + 'DIV', + 'foo' + ); + expect(attachedListener).toThrow(); + }); + + it('renders using forceUpdate even when there is no state', function() { + test( + React.createElement(ForceUpdateWithNoState, {initialValue: 'foo'}), + 'DIV', + 'foo' + ); + attachedListener(); + expect(renderedName).toBe('bar'); + }); + + it('will call all the normal life cycle methods', function() { + lifeCycles = []; + test(React.createElement(NormalLifeCycles, {value: 'foo'}), 'SPAN', 'foo'); + expect(lifeCycles).toEqual([ + 'will-mount', + 'did-mount' + ]); + lifeCycles = []; // reset + test(React.createElement(NormalLifeCycles, {value: 'bar'}), 'SPAN', 'bar'); + expect(lifeCycles).toEqual([ + 'receive-props', { value: 'bar' }, + 'should-update', { value: 'bar' }, {}, + 'will-update', { value: 'bar' }, {}, + 'did-update', { value: 'foo' }, {} + ]); + lifeCycles = []; // reset + React.unmountComponentAtNode(container); + expect(lifeCycles).toEqual([ + 'will-unmount' + ]); + }); + + it('warns when classic properties are defined on the instance, ' + + 'but does not invoke them.', function() { + var warn = jest.genMockFn(); + console.warn = warn; + getInitialStateWasCalled = false; + test(React.createElement(ClassicProperties), 'SPAN', 'foo'); + expect(getInitialStateWasCalled).toBe(false); + expect(warn.mock.calls.length).toBe(3); + expect(warn.mock.calls[0][0]).toContain( + 'getInitialState was defined on ClassicProperties, ' + + 'a plain JavaScript class.' + ); + expect(warn.mock.calls[1][0]).toContain( + 'propTypes was defined as an instance property on ClassicProperties.' + ); + expect(warn.mock.calls[2][0]).toContain( + 'contextTypes was defined as an instance property on ClassicProperties.' + ); + }); + + it('should warn when mispelling shouldComponentUpdate', function() { + var warn = jest.genMockFn(); + console.warn = warn; + + test(React.createElement(NamedComponent), 'SPAN', 'foo'); + + expect(warn.mock.calls.length).toBe(1); + expect(warn.mock.calls[0][0]).toBe( + 'Warning: ' + + 'NamedComponent has a method called componentShouldUpdate(). Did you ' + + 'mean shouldComponentUpdate()? The name is phrased as a question ' + + 'because the function is expected to return a value.' + ); + }); + + it('should throw AND warn when trying to access classic APIs', function() { + var warn = jest.genMockFn(); + console.warn = warn; + var instance = test( + React.createElement(Inner, {name: 'foo'}), + 'DIV','foo' + ); + expect(() => instance.getDOMNode()).toThrow(); + expect(() => instance.replaceState({})).toThrow(); + expect(() => instance.isMounted()).toThrow(); + expect(() => instance.setProps({ name: 'bar' })).toThrow(); + expect(warn.mock.calls.length).toBe(4); + expect(warn.mock.calls[0][0]).toContain( + 'getDOMNode(...) is deprecated in plain JavaScript React classes' + ); + expect(warn.mock.calls[1][0]).toContain( + 'replaceState(...) is deprecated in plain JavaScript React classes' + ); + expect(warn.mock.calls[2][0]).toContain( + 'isMounted(...) is deprecated in plain JavaScript React classes' + ); + expect(warn.mock.calls[3][0]).toContain( + 'setProps(...) is deprecated in plain JavaScript React classes' + ); + }); + + it('supports this.context passed via getChildContext', function() { + test(React.createElement(ProvideContext), 'DIV', 'bar-through-context'); + }); + + it('supports classic refs', function() { + var instance = test(React.createElement(ClassicRefs), 'DIV', 'foo'); + expect(instance.refs.inner.getName()).toBe('foo'); + }); + + it('supports drilling through to the DOM using findDOMNode', function() { + var instance = test( + React.createElement(Inner, {name: 'foo'}), + 'DIV', + 'foo' + ); + var node = React.findDOMNode(instance); + expect(node).toBe(container.firstChild); + }); + +}); diff --git a/src/modern/class/__tests__/react.d.ts b/src/modern/class/__tests__/react.d.ts new file mode 100644 index 0000000000..ff317606ae --- /dev/null +++ b/src/modern/class/__tests__/react.d.ts @@ -0,0 +1,32 @@ +/*! + * Copyright 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/** + * TypeScript Definition File for React. + * + * Full type definitions are not yet officially supported. These are mostly + * just helpers for the unit test. + */ + +declare module 'React' { + export class Component { + props: any; + state: any; + context: any; + static name: string; + constructor(props?, context?); + setState(partial : any, callback ?: any) : void; + forceUpdate(callback ?: any) : void; + } + export var PropTypes : any; + export function createElement(tag : any, props ?: any, ...children : any[]) : any + export function render(element : any, container : any) : any + export function unmountComponentAtNode(container : any) : void + export function findDOMNode(instance : any) : any +}