1'use strict'; 2 3import React from 'react'; 4import { NativeModules, StyleSheet, Platform, ScrollView, Text, View } from 'react-native'; 5import jasmineModule from 'jasmine-core/lib/jasmine-core/jasmine'; 6import Immutable from 'immutable'; 7 8const { ExponentTest } = NativeModules; 9 10export default class TestScreen extends React.Component { 11 constructor(props, context) { 12 super(props, context); 13 this.state = TestScreen.initialState; 14 this._results = ''; 15 this._failures = ''; 16 this._scrollViewRef = null; 17 } 18 19 componentDidMount() { 20 const { navigation } = this.props; 21 const selectedModules = navigation.getParam('selected'); 22 this._runTests(selectedModules); 23 this._isMounted = true; 24 } 25 26 componentWillUnmount() { 27 this._isMounted = false; 28 } 29 30 static navigationOptions = { 31 title: 'Test Runner', 32 }; 33 34 static initialState = { 35 portalChildShouldBeVisible: false, 36 state: Immutable.fromJS({ 37 suites: [], 38 path: ['suites'], // Path to current 'children' List in state 39 }), 40 testPortal: null, 41 numFailed: 0, 42 done: false, 43 }; 44 45 setPortalChild = testPortal => { 46 if (this._isMounted) return this.setState({ testPortal }); 47 }; 48 49 cleanupPortal = () => { 50 return new Promise(resolve => { 51 if (this._isMounted) this.setState({ testPortal: null }, resolve); 52 }); 53 }; 54 55 _runTests = async modules => { 56 // Reset results state 57 this.setState(TestScreen.initialState); 58 59 const { jasmineEnv, jasmine } = await this._setupJasmine(); 60 61 await Promise.all( 62 modules.map(m => 63 m.test(jasmine, { 64 setPortalChild: this.setPortalChild, 65 cleanupPortal: this.cleanupPortal, 66 }) 67 ) 68 ); 69 70 jasmineEnv.execute(); 71 }; 72 73 async _setupJasmine() { 74 // Init 75 jasmineModule.DEFAULT_TIMEOUT_INTERVAL = 10000; 76 const jasmineCore = jasmineModule.core(jasmineModule); 77 const jasmineEnv = jasmineCore.getEnv(); 78 79 // Add our custom reporters too 80 jasmineEnv.addReporter(this._jasmineSetStateReporter()); 81 jasmineEnv.addReporter(this._jasmineConsoleReporter()); 82 83 // Get the interface and make it support `async ` by default 84 const jasmine = jasmineModule.interface(jasmineCore, jasmineEnv); 85 const doneIfy = fn => async done => { 86 try { 87 await Promise.resolve(fn()); 88 done(); 89 } catch (e) { 90 done.fail(e); 91 } 92 }; 93 const oldIt = jasmine.it; 94 jasmine.it = (desc, fn, t) => oldIt.apply(jasmine, [desc, doneIfy(fn), t]); 95 const oldXit = jasmine.xit; 96 jasmine.xit = (desc, fn, t) => oldXit.apply(jasmine, [desc, doneIfy(fn), t]); 97 const oldFit = jasmine.fit; 98 jasmine.fit = (desc, fn, t) => oldFit.apply(jasmine, [desc, doneIfy(fn), t]); 99 100 return { 101 jasmineCore, 102 jasmineEnv, 103 jasmine, 104 }; 105 } 106 107 // A jasmine reporter that writes results to the console 108 _jasmineConsoleReporter() { 109 const failedSpecs = []; 110 const app = this; 111 112 return { 113 specDone(result) { 114 if (result.status === 'passed' || result.status === 'failed') { 115 // Open log group if failed 116 const grouping = result.status === 'passed' ? '---' : '+++'; 117 if (ExponentTest && ExponentTest.log) { 118 ExponentTest.log(`${result.status === 'passed' ? 'PASS' : 'FAIL'} ${result.fullName}`); 119 } 120 const emoji = result.status === 'passed' ? ':green_heart:' : ':broken_heart:'; 121 console.log(`${grouping} ${emoji} ${result.fullName}`); 122 this._results += `${grouping} ${result.fullName}\n`; 123 124 if (result.status === 'failed') { 125 this._failures += `${grouping} ${result.fullName}\n`; 126 result.failedExpectations.forEach(({ matcherName, message }) => { 127 if (ExponentTest && ExponentTest.log) { 128 ExponentTest.log(`${matcherName}: ${message}`); 129 } 130 console.log(`${matcherName}: ${message}`); 131 this._results += `${matcherName}: ${message}\n`; 132 this._failures += `${matcherName}: ${message}\n`; 133 }); 134 failedSpecs.push(result); 135 } 136 } 137 }, 138 139 jasmineStarted() { 140 console.log('--- tests started'); 141 }, 142 143 jasmineDone() { 144 console.log('--- tests done'); 145 console.log('--- sending results to runner'); 146 if (app._isMounted) { 147 app.setState({ done: true, numFailed: failedSpecs.length }); 148 } 149 const result = { 150 magic: '[TEST-SUITE-END]', // NOTE: Runner/Run.js waits to see this 151 failed: failedSpecs.length, 152 results: this._results, 153 }; 154 if (Platform.OS === 'web') { 155 // This log needs to be an object for puppeteer tests 156 console.log(result); 157 } else { 158 const jsonResult = JSON.stringify(result); 159 console.log(jsonResult); 160 } 161 162 if (ExponentTest) { 163 ExponentTest.completed( 164 JSON.stringify({ 165 failed: failedSpecs.length, 166 failures: this._failures, 167 results: this._results, 168 }) 169 ); 170 } 171 }, 172 }; 173 } 174 175 // A jasmine reporter that writes results to this.state 176 _jasmineSetStateReporter() { 177 const app = this; 178 return { 179 suiteStarted(jasmineResult) { 180 if (app._isMounted) { 181 app.setState(({ state }) => ({ 182 state: state 183 .updateIn(state.get('path'), children => 184 children.push( 185 Immutable.fromJS({ 186 result: jasmineResult, 187 children: [], 188 specs: [], 189 }) 190 ) 191 ) 192 .update('path', path => path.push(state.getIn(path).size, 'children')), 193 })); 194 } 195 }, 196 197 suiteDone() { 198 if (app._isMounted) { 199 app.setState(({ state }) => ({ 200 state: state 201 .updateIn( 202 state 203 .get('path') 204 .pop() 205 .pop(), 206 children => 207 children.update(children.size - 1, child => 208 child.set('result', child.get('result')) 209 ) 210 ) 211 .update('path', path => path.pop().pop()), 212 })); 213 } 214 }, 215 216 specStarted(jasmineResult) { 217 if (app._isMounted) { 218 app.setState(({ state }) => ({ 219 state: state.updateIn( 220 state 221 .get('path') 222 .pop() 223 .pop(), 224 children => 225 children.update(children.size - 1, child => 226 child.update('specs', specs => specs.push(Immutable.fromJS(jasmineResult))) 227 ) 228 ), 229 })); 230 } 231 }, 232 233 specDone(jasmineResult) { 234 if (app.state.testPortal) { 235 console.warn( 236 `The test portal has not been cleaned up by \`${ 237 jasmineResult.fullName 238 }\`. Call \`cleanupPortal\` before finishing the test.` 239 ); 240 } 241 if (app._isMounted) { 242 app.setState(({ state }) => ({ 243 state: state.updateIn( 244 state 245 .get('path') 246 .pop() 247 .pop(), 248 children => 249 children.update(children.size - 1, child => 250 child.update('specs', specs => 251 specs.set(specs.size - 1, Immutable.fromJS(jasmineResult)) 252 ) 253 ) 254 ), 255 })); 256 } 257 }, 258 }; 259 } 260 261 _renderSpecResult = r => { 262 const status = r.get('status') || 'running'; 263 return ( 264 <View 265 key={r.get('id')} 266 style={{ 267 paddingLeft: 10, 268 marginVertical: 3, 269 borderColor: { 270 running: '#ff0', 271 passed: '#0f0', 272 failed: '#f00', 273 disabled: '#888', 274 }[status], 275 borderLeftWidth: 3, 276 }}> 277 <Text style={{ fontSize: 16 }}> 278 { 279 { 280 running: ' ', 281 passed: ' ', 282 failed: ' ', 283 }[status] 284 } 285 {r.get('description')} ({status}) 286 </Text> 287 {r.get('failedExpectations').map((e, i) => ( 288 <Text key={i}>{e.get('message')}</Text> 289 ))} 290 </View> 291 ); 292 }; 293 294 _renderSuiteResult = (r, depth) => { 295 const titleStyle = 296 depth === 0 297 ? { marginBottom: 8, fontSize: 16, fontWeight: 'bold' } 298 : { marginVertical: 8, fontSize: 16 }; 299 const containerStyle = 300 depth === 0 301 ? { 302 paddingLeft: 16, 303 paddingVertical: 16, 304 borderBottomWidth: 1, 305 borderColor: '#ddd', 306 } 307 : { paddingLeft: 16 }; 308 return ( 309 <View key={r.get('result').get('id')} style={containerStyle}> 310 <Text style={titleStyle}>{r.get('result').get('description')}</Text> 311 {r.get('specs').map(this._renderSpecResult)} 312 {r.get('children').map(r => this._renderSuiteResult(r, depth + 1))} 313 </View> 314 ); 315 }; 316 317 _onScrollViewContentSizeChange = () => { 318 if (this._scrollViewRef) { 319 this._scrollViewRef.scrollToEnd(); 320 } 321 }; 322 323 _renderDoneText = () => { 324 if (this.state.done) { 325 return ( 326 <Text style={styles.doneMessage}> 327 All done! {this.state.numFailed} {this.state.numFailed === 1 ? 'test' : 'tests'} failed. 328 </Text> 329 ); 330 } 331 }; 332 333 _renderPortal = () => { 334 const styles = { 335 top: 0, 336 left: 0, 337 right: 0, 338 bottom: 0, 339 position: 'absolute', 340 alignItems: 'center', 341 justifyContent: 'center', 342 backgroundColor: 'rgb(255, 255, 255)', 343 opacity: this.state.portalChildShouldBeVisible ? 0.5 : 0, 344 }; 345 346 if (this.state.testPortal) { 347 return ( 348 <View style={styles} pointerEvents="none"> 349 {this.state.testPortal} 350 </View> 351 ); 352 } 353 }; 354 355 render() { 356 return ( 357 <View style={styles.scrollViewContainer} testID="test_suite_container"> 358 <ScrollView 359 style={styles.scrollView} 360 contentContainerStyle={styles.scrollViewContent} 361 ref={ref => (this._scrollViewRef = ref)} 362 onContentSizeChange={this._onScrollViewContentSizeChange}> 363 {this.state.state.get('suites').map(r => this._renderSuiteResult(r, 0))} 364 {this._renderDoneText()} 365 </ScrollView> 366 {this._renderPortal()} 367 </View> 368 ); 369 } 370} 371 372const styles = StyleSheet.create({ 373 scrollViewContainer: { 374 flex: 1, 375 alignItems: 'stretch', 376 justifyContent: 'center', 377 }, 378 scrollViewContent: { 379 padding: 5, 380 }, 381 scrollView: { 382 flex: 1, 383 }, 384 doneMessage: { 385 textAlign: 'center', 386 fontWeight: 'bold', 387 fontSize: 20, 388 }, 389}); 390