Add Basic TypeScript Class Test
As part of the new class effort it is now possible to define React Components using any type of generic JavaScript class syntax. This includes TypeScript classes. This test ensures that we don't regress that support, and also serves as an example for using React in TypeScript. TypeScript provides a good demo of where we think property initializers are going. We don't have any official *type* support for TypeScript yet. This test trails the ReactES6Class-test file. Some manual tweaking is required when converting tests.
Sebastian Markbage committed
Jan 26, 2015 at 16:59 UTC
6c145c31f5b1f465bb4f86c2a052958cb7922b9f
6 files changed
+620
-2
jest/jest.d.ts
new
+71
@@ -0,0 +1,71 @@
1
+declare var jasmine: any;
2
+
3
+declare function afterEach(fn: any): any;
4
+declare function beforeEach(fn: any): any;
5
+declare function describe(name: string, fn: any): void;
6
+declare var it: {
7
+ (name: string, fn: any): void;
8
+ only: (name: string, fn: any) => void;
9
+}
10
+declare function expect(val: any): Expect;
11
+declare var jest: Jest;
12
+declare function pit(name: string, fn: any): void;
13
+declare function spyOn(obj: any, key: string): any;
14
+declare function xdescribe(name: string, fn: any): void;
15
+declare function xit(name: string, fn: any): void;
16
+
17
+interface Expect {
18
+ not: Expect
19
+ toThrow(message?: string): void
20
+ toBe(value: any): void
21
+ toEqual(value: any): void
22
+ toBeFalsy(): void
23
+ toBeTruthy(): void
24
+ toBeNull(): void
25
+ toBeUndefined(): void
26
+ toBeDefined(): void
27
+ toMatch(regexp: RegExp): void
28
+ toContain(string: string): void
29
+ toBeCloseTo(number: number, delta: number): void
30
+ toBeGreaterThan(number: number): void
31
+ toBeLessThan(number: number): void
32
+ toBeCalled(): void
33
+ toBeCalledWith(...arguments): void
34
+ lastCalledWith(...arguments): void
35
+}
36
+
37
+interface Jest {
38
+ autoMockOff(): void
39
+ autoMockOn(): void
40
+ clearAllTimers(): void
41
+ dontMock(moduleName: string): void
42
+ genMockFromModule(moduleObj: Object): Object
43
+ genMockFunction(): MockFunction
44
+ genMockFn(): MockFunction
45
+ mock(moduleName: string): void
46
+ runAllTicks(): void
47
+ runAllTimers(): void
48
+ runOnlyPendingTimers(): void
49
+ setMock(moduleName: string, moduleExports: Object): void
50
+}
51
+
52
+interface MockFunction {
53
+ (...arguments): any
54
+ mock: {
55
+ calls: Array<Array<any>>
56
+ instances: Array<Object>
57
+ }
58
+ mockClear(): void
59
+ mockImplementation(fn: Function): MockFunction
60
+ mockImpl(fn: Function): MockFunction
61
+ mockReturnThis(): MockFunction
62
+ mockReturnValue(value: any): MockFunction
63
+ mockReturnValueOnce(value: any): MockFunction
64
+}
65
+
66
+// Allow importing jasmine-check
67
+declare module 'jasmine-check' {
68
+ export function install(global?: any): void;
69
+}
70
+declare var check: any;
71
+declare var gen: any;
jest/preprocessor.js
+17
@@ -3,12 +3,29 @@
3
var ReactTools = require('../main.js');
4
5
var coffee = require('coffee-script');
6
+var ts = require('ts-compiler');
7
8
module.exports = {
9
process: function(src, path) {
10
if (path.match(/\.coffee$/)) {
11
return coffee.compile(src, {'bare': true});
12
}
13
+ if (path.match(/\.ts$/) && !path.match(/\.d\.ts$/)) {
14
+ ts.compile([path], {
15
+ skipWrite: true,
16
+ module: 'commonjs'
17
+ }, function(err, results) {
18
+ if (err) {
19
+ throw err;
20
+ }
21
+ results.forEach(function(file) {
22
+ // This is gross, but jest doesn't provide an asynchronous way to
23
+ // process a module, and ts currently runs syncronously.
24
+ src = file.text;
25
+ });
26
+ });
27
+ return src;
28
+ }
29
return ReactTools.transform(src, {harmony: true});
30
}
31
};
package.json
+3
-1
@@ -58,6 +58,7 @@
58
"populist": "~0.1.6",
59
"recast": "^0.9.11",
60
"sauce-tunnel": "~1.1.0",
61
+ "ts-compiler": "^2.0.0",
62
"tmp": "~0.0.18",
63
"uglify-js": "~2.4.0",
64
"uglifyify": "^2.4.0",
@@ -82,7 +83,8 @@
83
"setupEnvScriptFile": "jest/environment.js",
84
"testFileExtensions": [
85
"js",
85
- "coffee"
86
+ "coffee",
87
+ "ts"
88
],
89
"modulePathIgnorePatterns": [
90
"/build/",
src/modern/class/__tests__/ReactClassEquivalence-test.js
+6
-1
@@ -19,9 +19,14 @@ describe('ReactClassEquivalence', function() {
19
20
var es6 = () => require('./ReactES6Class-test.js');
21
var coffee = () => require('./ReactCoffeeScriptClass-test.coffee');
22
+ var ts = () => require('./ReactTypeScriptClass-test.ts');
23
23
- it('tests the same thing for es6 classes and coffee script', function() {
24
+ it('tests the same thing for es6 classes and CoffeeScript', function() {
25
expect(coffee).toEqualSpecsIn(es6);
26
});
27
28
+ it('tests the same thing for es6 classes and TypeScript', function() {
29
+ expect(ts).toEqualSpecsIn(es6);
30
+ });
31
+
32
});
src/modern/class/__tests__/ReactTypeScriptClass-test.ts
new
+491
@@ -0,0 +1,491 @@
1
+/*!
2
+ * Copyright 2015, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ */
9
+
10
+///<reference path='../../../../jest/jest.d.ts'/>
11
+///<reference path='./react.d.ts'/>
12
+
13
+import React = require('React');
14
+
15
+// Before Each
16
+
17
+var container;
18
+var attachedListener = null;
19
+var renderedName = null;
20
+
21
+class Inner extends React.Component {
22
+ getName() {
23
+ return this.props.name;
24
+ }
25
+ render() {
26
+ attachedListener = this.props.onClick;
27
+ renderedName = this.props.name;
28
+ return React.createElement('div', { className: this.props.name });
29
+ }
30
+};
31
+
32
+function test(element, expectedTag, expectedClassName) {
33
+ var instance = React.render(element, container);
34
+ expect(container.firstChild).not.toBeNull();
35
+ expect(container.firstChild.tagName).toBe(expectedTag);
36
+ expect(container.firstChild.className).toBe(expectedClassName);
37
+ return instance;
38
+}
39
+
40
+// Classes need to be declared at the top level scope, so we declare all the
41
+// classes that will be used by the tests below, instead of inlining them.
42
+// TODO: Consider redesigning this using modules so that we can use non-unique
43
+// names of classes and bundle them with the test code.
44
+
45
+// it preserves the name of the class for use in error messages
46
+// it throws if no render function is defined
47
+class Empty extends React.Component { }
48
+
49
+// it renders a simple stateless component with prop
50
+class SimpleStateless {
51
+ props : any;
52
+ render() {
53
+ return React.createElement(Inner, {name: this.props.bar});
54
+ }
55
+}
56
+
57
+// it renders based on state using initial values in this.props
58
+class InitialState extends React.Component {
59
+ state = {
60
+ bar: this.props.initialValue
61
+ };
62
+ render() {
63
+ return React.createElement('span', {className: this.state.bar});
64
+ }
65
+}
66
+
67
+// it renders based on state using props in the constructor
68
+class StateBasedOnProps extends React.Component {
69
+ constructor(props) {
70
+ super(props);
71
+ this.state = { bar: props.initialValue };
72
+ }
73
+ changeState() {
74
+ this.setState({ bar: 'bar' });
75
+ }
76
+ render() {
77
+ if (this.state.bar === 'foo') {
78
+ return React.createElement('div', {className: 'foo'});
79
+ }
80
+ return React.createElement('span', {className: this.state.bar});
81
+ }
82
+}
83
+
84
+// it renders based on context in the constructor
85
+class StateBasedOnContext extends React.Component {
86
+ static contextTypes = {
87
+ tag: React.PropTypes.string,
88
+ className: React.PropTypes.string
89
+ }
90
+ state = {
91
+ tag: this.context.tag,
92
+ className: this.context.className
93
+ }
94
+ render() {
95
+ var Tag = this.state.tag;
96
+ return React.createElement(Tag, {className: this.state.className});
97
+ }
98
+}
99
+
100
+class ProvideChildContextTypes extends React.Component {
101
+ static childContextTypes = {
102
+ tag: React.PropTypes.string,
103
+ className: React.PropTypes.string
104
+ }
105
+ getChildContext() {
106
+ return { tag: 'span', className: 'foo' };
107
+ }
108
+ render() {
109
+ return React.createElement(StateBasedOnContext);
110
+ }
111
+}
112
+
113
+// it renders only once when setting state in componentWillMount
114
+var renderCount = 0;
115
+class RenderOnce extends React.Component {
116
+ state = {
117
+ bar: this.props.initialValue
118
+ }
119
+ componentWillMount() {
120
+ this.setState({ bar: 'bar' });
121
+ }
122
+ render() {
123
+ renderCount++;
124
+ return React.createElement('span', {className: this.state.bar});
125
+ }
126
+}
127
+
128
+// it should throw with non-object in the initial state property
129
+class ArrayState {
130
+ state = ['an array']
131
+ render() {
132
+ return React.createElement('span');
133
+ }
134
+}
135
+class StringState {
136
+ state = 'a string'
137
+ render() {
138
+ return React.createElement('span');
139
+ }
140
+}
141
+class NumberState {
142
+ state = 1234
143
+ render() {
144
+ return React.createElement('span');
145
+ }
146
+}
147
+
148
+// it should render with null in the initial state property
149
+class NullState extends React.Component {
150
+ state = null
151
+ render() {
152
+ return React.createElement('span');
153
+ }
154
+}
155
+
156
+// it setState through an event handler
157
+class BoundEventHandler extends React.Component {
158
+ state = {
159
+ bar: this.props.initialValue
160
+ }
161
+ handleClick = () => {
162
+ this.setState({ bar: 'bar' });
163
+ }
164
+ render() {
165
+ return (
166
+ React.createElement(Inner, {
167
+ name: this.state.bar,
168
+ onClick: this.handleClick
169
+ })
170
+ );
171
+ }
172
+}
173
+
174
+// it should not implicitly bind event handlers
175
+class UnboundEventHandler extends React.Component {
176
+ state = {
177
+ bar: this.props.initialValue
178
+ }
179
+ handleClick() {
180
+ this.setState({ bar: 'bar' });
181
+ }
182
+ render() {
183
+ return React.createElement(
184
+ Inner, { name: this.state.bar, onClick: this.handleClick }
185
+ );
186
+ }
187
+}
188
+
189
+// it renders using forceUpdate even when there is no state
190
+class ForceUpdateWithNoState extends React.Component {
191
+ mutativeValue : string = this.props.initialValue
192
+ handleClick() {
193
+ this.mutativeValue = 'bar';
194
+ this.forceUpdate();
195
+ }
196
+ render() {
197
+ return (
198
+ React.createElement(Inner, {
199
+ name: this.mutativeValue,
200
+ onClick: this.handleClick.bind(this)}
201
+ )
202
+ );
203
+ }
204
+}
205
+
206
+// it will call all the normal life cycle methods
207
+var lifeCycles = [];
208
+class NormalLifeCycles {
209
+ props : any
210
+ state = {}
211
+ componentWillMount() {
212
+ lifeCycles.push('will-mount');
213
+ }
214
+ componentDidMount() {
215
+ lifeCycles.push('did-mount');
216
+ }
217
+ componentWillReceiveProps(nextProps) {
218
+ lifeCycles.push('receive-props', nextProps);
219
+ }
220
+ shouldComponentUpdate(nextProps, nextState) {
221
+ lifeCycles.push('should-update', nextProps, nextState);
222
+ return true;
223
+ }
224
+ componentWillUpdate(nextProps, nextState) {
225
+ lifeCycles.push('will-update', nextProps, nextState);
226
+ }
227
+ componentDidUpdate(prevProps, prevState) {
228
+ lifeCycles.push('did-update', prevProps, prevState);
229
+ }
230
+ componentWillUnmount() {
231
+ lifeCycles.push('will-unmount');
232
+ }
233
+ render() {
234
+ return React.createElement('span', {className: this.props.value});
235
+ }
236
+}
237
+
238
+// warns when classic properties are defined on the instance,
239
+// but does not invoke them.
240
+var getInitialStateWasCalled = false;
241
+class ClassicProperties extends React.Component {
242
+ contextTypes = {};
243
+ propTypes = {};
244
+ getInitialState() {
245
+ getInitialStateWasCalled = true;
246
+ return {};
247
+ }
248
+ render() {
249
+ return React.createElement('span', {className: 'foo'});
250
+ }
251
+}
252
+
253
+// it should warn when mispelling shouldComponentUpdate
254
+class NamedComponent {
255
+ componentShouldUpdate() {
256
+ return false;
257
+ }
258
+ render() {
259
+ return React.createElement('span', {className: 'foo'});
260
+ }
261
+}
262
+
263
+// it supports this.context passed via getChildContext
264
+class ReadContext extends React.Component {
265
+ static contextTypes = { bar: React.PropTypes.string };
266
+ render() {
267
+ return React.createElement('div', { className: this.context.bar });
268
+ }
269
+}
270
+class ProvideContext {
271
+ static childContextTypes = { bar: React.PropTypes.string };
272
+ getChildContext() {
273
+ return { bar: 'bar-through-context' };
274
+ }
275
+ render() {
276
+ return React.createElement(ReadContext);
277
+ }
278
+}
279
+
280
+// it supports classic refs
281
+class ClassicRefs {
282
+ render() {
283
+ return React.createElement(Inner, {name: 'foo', ref: 'inner'});
284
+ }
285
+}
286
+
287
+// Describe the actual test cases.
288
+
289
+describe('ReactTypeScriptClass', function() {
290
+
291
+ beforeEach(function() {
292
+ container = document.createElement('div');
293
+ attachedListener = null;
294
+ renderedName = null;
295
+ });
296
+
297
+ it('preserves the name of the class for use in error messages', function() {
298
+ expect(Empty.name).toBe('Empty');
299
+ });
300
+
301
+ it('throws if no render function is defined', function() {
302
+ expect(() => React.render(React.createElement(Empty), container)).toThrow();
303
+ });
304
+
305
+ it('renders a simple stateless component with prop', function() {
306
+ test(React.createElement(SimpleStateless, {bar: 'foo'}), 'DIV', 'foo');
307
+ test(React.createElement(SimpleStateless, {bar: 'bar'}), 'DIV', 'bar');
308
+ });
309
+
310
+ it('renders based on state using initial values in this.props', function() {
311
+ test(
312
+ React.createElement(InitialState, {initialValue: 'foo'}),
313
+ 'SPAN',
314
+ 'foo'
315
+ );
316
+ });
317
+
318
+ it('renders based on state using props in the constructor', function() {
319
+ var instance = test(
320
+ React.createElement(StateBasedOnProps, {initialValue: 'foo'}),
321
+ 'DIV',
322
+ 'foo'
323
+ );
324
+ instance.changeState();
325
+ test(React.createElement(StateBasedOnProps), 'SPAN', 'bar');
326
+ });
327
+
328
+ it('renders based on context in the constructor', function() {
329
+ test(React.createElement(ProvideChildContextTypes), 'SPAN', 'foo');
330
+ });
331
+
332
+ it('renders only once when setting state in componentWillMount', function() {
333
+ renderCount = 0;
334
+ test(React.createElement(RenderOnce, {initialValue: 'foo'}), 'SPAN', 'bar');
335
+ expect(renderCount).toBe(1);
336
+ });
337
+
338
+ it('should throw with non-object in the initial state property', function() {
339
+ expect(() => test(React.createElement(ArrayState), 'span', ''))
340
+ .toThrow(
341
+ 'Invariant Violation: ArrayState.state: ' +
342
+ 'must be set to an object or null'
343
+ );
344
+ expect(() => test(React.createElement(StringState), 'span', ''))
345
+ .toThrow(
346
+ 'Invariant Violation: StringState.state: ' +
347
+ 'must be set to an object or null'
348
+ );
349
+ expect(() => test(React.createElement(NumberState), 'span', ''))
350
+ .toThrow(
351
+ 'Invariant Violation: NumberState.state: ' +
352
+ 'must be set to an object or null'
353
+ );
354
+ });
355
+
356
+ it('should render with null in the initial state property', function() {
357
+ test(React.createElement(NullState), 'SPAN', '');
358
+ });
359
+
360
+ it('setState through an event handler', function() {
361
+ test(
362
+ React.createElement(BoundEventHandler, {initialValue: 'foo'}),
363
+ 'DIV',
364
+ 'foo'
365
+ );
366
+ attachedListener();
367
+ expect(renderedName).toBe('bar');
368
+ });
369
+
370
+ it('should not implicitly bind event handlers', function() {
371
+ test(
372
+ React.createElement(UnboundEventHandler, {initialValue: 'foo'}),
373
+ 'DIV',
374
+ 'foo'
375
+ );
376
+ expect(attachedListener).toThrow();
377
+ });
378
+
379
+ it('renders using forceUpdate even when there is no state', function() {
380
+ test(
381
+ React.createElement(ForceUpdateWithNoState, {initialValue: 'foo'}),
382
+ 'DIV',
383
+ 'foo'
384
+ );
385
+ attachedListener();
386
+ expect(renderedName).toBe('bar');
387
+ });
388
+
389
+ it('will call all the normal life cycle methods', function() {
390
+ lifeCycles = [];
391
+ test(React.createElement(NormalLifeCycles, {value: 'foo'}), 'SPAN', 'foo');
392
+ expect(lifeCycles).toEqual([
393
+ 'will-mount',
394
+ 'did-mount'
395
+ ]);
396
+ lifeCycles = []; // reset
397
+ test(React.createElement(NormalLifeCycles, {value: 'bar'}), 'SPAN', 'bar');
398
+ expect(lifeCycles).toEqual([
399
+ 'receive-props', { value: 'bar' },
400
+ 'should-update', { value: 'bar' }, {},
401
+ 'will-update', { value: 'bar' }, {},
402
+ 'did-update', { value: 'foo' }, {}
403
+ ]);
404
+ lifeCycles = []; // reset
405
+ React.unmountComponentAtNode(container);
406
+ expect(lifeCycles).toEqual([
407
+ 'will-unmount'
408
+ ]);
409
+ });
410
+
411
+ it('warns when classic properties are defined on the instance, ' +
412
+ 'but does not invoke them.', function() {
413
+ var warn = jest.genMockFn();
414
+ console.warn = warn;
415
+ getInitialStateWasCalled = false;
416
+ test(React.createElement(ClassicProperties), 'SPAN', 'foo');
417
+ expect(getInitialStateWasCalled).toBe(false);
418
+ expect(warn.mock.calls.length).toBe(3);
419
+ expect(warn.mock.calls[0][0]).toContain(
420
+ 'getInitialState was defined on ClassicProperties, ' +
421
+ 'a plain JavaScript class.'
422
+ );
423
+ expect(warn.mock.calls[1][0]).toContain(
424
+ 'propTypes was defined as an instance property on ClassicProperties.'
425
+ );
426
+ expect(warn.mock.calls[2][0]).toContain(
427
+ 'contextTypes was defined as an instance property on ClassicProperties.'
428
+ );
429
+ });
430
+
431
+ it('should warn when mispelling shouldComponentUpdate', function() {
432
+ var warn = jest.genMockFn();
433
+ console.warn = warn;
434
+
435
+ test(React.createElement(NamedComponent), 'SPAN', 'foo');
436
+
437
+ expect(warn.mock.calls.length).toBe(1);
438
+ expect(warn.mock.calls[0][0]).toBe(
439
+ 'Warning: ' +
440
+ 'NamedComponent has a method called componentShouldUpdate(). Did you ' +
441
+ 'mean shouldComponentUpdate()? The name is phrased as a question ' +
442
+ 'because the function is expected to return a value.'
443
+ );
444
+ });
445
+
446
+ it('should throw AND warn when trying to access classic APIs', function() {
447
+ var warn = jest.genMockFn();
448
+ console.warn = warn;
449
+ var instance = test(
450
+ React.createElement(Inner, {name: 'foo'}),
451
+ 'DIV','foo'
452
+ );
453
+ expect(() => instance.getDOMNode()).toThrow();
454
+ expect(() => instance.replaceState({})).toThrow();
455
+ expect(() => instance.isMounted()).toThrow();
456
+ expect(() => instance.setProps({ name: 'bar' })).toThrow();
457
+ expect(warn.mock.calls.length).toBe(4);
458
+ expect(warn.mock.calls[0][0]).toContain(
459
+ 'getDOMNode(...) is deprecated in plain JavaScript React classes'
460
+ );
461
+ expect(warn.mock.calls[1][0]).toContain(
462
+ 'replaceState(...) is deprecated in plain JavaScript React classes'
463
+ );
464
+ expect(warn.mock.calls[2][0]).toContain(
465
+ 'isMounted(...) is deprecated in plain JavaScript React classes'
466
+ );
467
+ expect(warn.mock.calls[3][0]).toContain(
468
+ 'setProps(...) is deprecated in plain JavaScript React classes'
469
+ );
470
+ });
471
+
472
+ it('supports this.context passed via getChildContext', function() {
473
+ test(React.createElement(ProvideContext), 'DIV', 'bar-through-context');
474
+ });
475
+
476
+ it('supports classic refs', function() {
477
+ var instance = test(React.createElement(ClassicRefs), 'DIV', 'foo');
478
+ expect(instance.refs.inner.getName()).toBe('foo');
479
+ });
480
+
481
+ it('supports drilling through to the DOM using findDOMNode', function() {
482
+ var instance = test(
483
+ React.createElement(Inner, {name: 'foo'}),
484
+ 'DIV',
485
+ 'foo'
486
+ );
487
+ var node = React.findDOMNode(instance);
488
+ expect(node).toBe(container.firstChild);
489
+ });
490
+
491
+});
src/modern/class/__tests__/react.d.ts
new
+32
@@ -0,0 +1,32 @@
1
+/*!
2
+ * Copyright 2015, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ */
9
+
10
+/**
11
+ * TypeScript Definition File for React.
12
+ *
13
+ * Full type definitions are not yet officially supported. These are mostly
14
+ * just helpers for the unit test.
15
+ */
16
+
17
+declare module 'React' {
18
+ export class Component {
19
+ props: any;
20
+ state: any;
21
+ context: any;
22
+ static name: string;
23
+ constructor(props?, context?);
24
+ setState(partial : any, callback ?: any) : void;
25
+ forceUpdate(callback ?: any) : void;
26
+ }
27
+ export var PropTypes : any;
28
+ export function createElement(tag : any, props ?: any, ...children : any[]) : any
29
+ export function render(element : any, container : any) : any
30
+ export function unmountComponentAtNode(container : any) : void
31
+ export function findDOMNode(instance : any) : any
32
+}