1'use strict'; 2 3import React from 'react'; 4import { Dimensions, Linking, NativeModules, Platform, ScrollView, Text, View } from 'react-native'; 5import { Constants } from 'expo'; 6import jasmineModule from 'jasmine-core/lib/jasmine-core/jasmine'; 7import Immutable from 'immutable'; 8 9import * as TestUtils from './TestUtils'; 10 11let { ExponentTest } = NativeModules; 12 13// List of all modules for tests. Each file path must be statically present for 14// the packager to pick them all up. 15async function getTestModulesAsync() { 16 // The tests don't complete on CircleCI on iOS so we test just that the app launches and runs 17 if (Platform.OS === 'ios') { 18 let isInCI = await TestUtils.shouldSkipTestsRequiringPermissionsAsync(); 19 if (isInCI) { 20 return []; 21 } 22 } 23 24 if (Platform.OS === 'web') { 25 return [ 26 require('./tests/Import1'), 27 require('./tests/Crypto'), 28 require('./tests/Random'), 29 ]; 30 } 31 32 let modules = [ 33 // require('./tests/Basic1'), 34 // require('./tests/Basic2'), 35 require('./tests/Import1'), 36 require('./tests/Import2'), 37 require('./tests/Import3'), 38 require('./tests/Asset'), 39 require('./tests/Audio'), 40 require('./tests/Calendar'), 41 require('./tests/Constants'), 42 require('./tests/Contacts'), 43 require('./tests/Crypto'), 44 require('./tests/FileSystem'), 45 require('./tests/GoogleSignIn'), 46 require('./tests/Haptics'), 47 require('./tests/Localization'), 48 require('./tests/Location'), 49 require('./tests/Linking'), 50 require('./tests/Recording'), 51 require('./tests/ScreenOrientation'), 52 require('./tests/SecureStore'), 53 require('./tests/Segment'), 54 require('./tests/Speech'), 55 require('./tests/SQLite'), 56 require('./tests/Random'), 57 require('./tests/Payments'), 58 require('./tests/AdMobInterstitial'), 59 require('./tests/AdMobBanner'), 60 require('./tests/AdMobPublisherBanner'), 61 require('./tests/AdMobRewarded'), 62 require('./tests/Video'), 63 require('./tests/Permissions'), 64 require('./tests/MediaLibrary'), 65 require('./tests/Notifications'), 66 require('./tests/FBNativeAd'), 67 require('./tests/FBBannerAd'), 68 require('./tests/TaskManager'), 69 ]; 70 if (Constants.isDevice) { 71 modules = modules.concat([require('./tests/Brightness')]); 72 modules = modules.concat([require('./tests/BarCodeScanner')]); 73 if (Platform.OS === 'android') { 74 modules = modules.concat([require('./tests/JSC')]); 75 // The Camera tests are flaky on iOS, i.e. they fail randomly 76 modules = modules.concat([require('./tests/Camera')]); 77 } 78 } 79 return modules; 80} 81 82export default class App extends React.Component { 83 // --- Lifecycle ------------------------------------------------------------- 84 85 constructor(props, context) { 86 super(props, context); 87 this.state = App.initialState; 88 this._results = ''; 89 this._failures = ''; 90 this._scrollViewRef = null; 91 } 92 93 componentDidMount() { 94 this._runTests(this.props.exp.initialUri); 95 Linking.addEventListener('url', ({ url }) => url && this._runTests(url)); 96 } 97 98 // --- Test running ---------------------------------------------------------- 99 100 static initialState = { 101 portalChildShouldBeVisible: false, 102 testRunnerError: null, 103 state: Immutable.fromJS({ 104 suites: [], 105 path: ['suites'], // Path to current 'children' List in state 106 }), 107 }; 108 109 setPortalChild = testPortal => this.setState({ testPortal }); 110 cleanupPortal = () => new Promise(resolve => this.setState({ testPortal: null }, resolve)); 111 112 async _runTests(uri) { 113 // If the URL contains two pluses let's keep the existing state instead of rerunning tests. 114 // This way we are able to test the Linking module. 115 if (uri && uri.indexOf('++') > -1) { 116 return; 117 } 118 119 // Reset results state 120 this.setState(App.initialState); 121 122 const { jasmineEnv, jasmine } = await this._setupJasmine(); 123 124 // Load tests, confining to the ones named in the uri 125 let modules = await getTestModulesAsync(); 126 if (uri && uri.indexOf('--/') > -1) { 127 const deepLink = uri.substring(uri.indexOf('--/') + 3); 128 const filterJSON = JSON.parse(deepLink); 129 if (filterJSON.includeModules) { 130 console.log('Only testing these modules: ' + JSON.stringify(filterJSON.includeModules)); 131 const includeModulesRegexes = filterJSON.includeModules.map(m => new RegExp(m)); 132 modules = modules.filter(m => { 133 for (let i = 0; i < includeModulesRegexes.length; i++) { 134 if (includeModulesRegexes[i].test(m.name)) { 135 return true; 136 } 137 } 138 139 return false; 140 }); 141 142 if (modules.length === 0) { 143 this.setState({ 144 testRunnerError: `No tests were found that satisfy ${deepLink}`, 145 }); 146 return; 147 } 148 } 149 } 150 151 await Promise.all( 152 modules.map(m => 153 m.test(jasmine, { 154 setPortalChild: this.setPortalChild, 155 cleanupPortal: this.cleanupPortal, 156 }) 157 ) 158 ); 159 160 jasmineEnv.execute(); 161 } 162 163 async _setupJasmine() { 164 // Init 165 jasmineModule.DEFAULT_TIMEOUT_INTERVAL = 10000; 166 const jasmineCore = jasmineModule.core(jasmineModule); 167 const jasmineEnv = jasmineCore.getEnv(); 168 169 // Add our custom reporters too 170 jasmineEnv.addReporter(this._jasmineSetStateReporter()); 171 jasmineEnv.addReporter(this._jasmineConsoleReporter()); 172 173 // Get the interface and make it support `async ` by default 174 const jasmine = jasmineModule.interface(jasmineCore, jasmineEnv); 175 const doneIfy = fn => async done => { 176 try { 177 await Promise.resolve(fn()); 178 done(); 179 } catch (e) { 180 done.fail(e); 181 } 182 }; 183 const oldIt = jasmine.it; 184 jasmine.it = (desc, fn, t) => oldIt.apply(jasmine, [desc, doneIfy(fn), t]); 185 const oldXit = jasmine.xit; 186 jasmine.xit = (desc, fn, t) => oldXit.apply(jasmine, [desc, doneIfy(fn), t]); 187 const oldFit = jasmine.fit; 188 jasmine.fit = (desc, fn, t) => oldFit.apply(jasmine, [desc, doneIfy(fn), t]); 189 190 return { 191 jasmineCore, 192 jasmineEnv, 193 jasmine, 194 }; 195 } 196 197 // A jasmine reporter that writes results to the console 198 _jasmineConsoleReporter(jasmineEnv) { 199 const failedSpecs = []; 200 201 return { 202 specDone(result) { 203 if (result.status === 'passed' || result.status === 'failed') { 204 // Open log group if failed 205 const grouping = result.status === 'passed' ? '---' : '+++'; 206 if (ExponentTest && ExponentTest.log) { 207 ExponentTest.log(`${result.status === 'passed' ? 'PASS' : 'FAIL'} ${result.fullName}`); 208 } 209 const emoji = result.status === 'passed' ? ':green_heart:' : ':broken_heart:'; 210 console.log(`${grouping} ${emoji} ${result.fullName}`); 211 this._results += `${grouping} ${result.fullName}\n`; 212 213 if (result.status === 'failed') { 214 this._failures += `${grouping} ${result.fullName}\n`; 215 result.failedExpectations.forEach(({ matcherName, message }) => { 216 if (ExponentTest && ExponentTest.log) { 217 ExponentTest.log(`${matcherName}: ${message}`); 218 } 219 console.log(`${matcherName}: ${message}`); 220 this._results += `${matcherName}: ${message}\n`; 221 this._failures += `${matcherName}: ${message}\n`; 222 }); 223 failedSpecs.push(result); 224 } 225 } 226 }, 227 228 suiteDone(result) {}, 229 230 jasmineStarted() { 231 console.log('--- tests started'); 232 }, 233 234 jasmineDone() { 235 console.log('--- tests done'); 236 console.log('--- send results to runner'); 237 const result = { 238 magic: '[TEST-SUITE-END]', // NOTE: Runner/Run.js waits to see this 239 failed: failedSpecs.length, 240 results: this._results, 241 }; 242 if (Platform.OS === 'web') { 243 // This log needs to be an object for puppeteer tests 244 console.log(result); 245 } else { 246 const jsonResult = JSON.stringify(result); 247 console.log(jsonResult); 248 } 249 250 if (ExponentTest) { 251 // Native logs are truncated so log just the failures for now 252 ExponentTest.completed( 253 JSON.stringify({ 254 failed: failedSpecs.length, 255 failures: this._failures, 256 }) 257 ); 258 } 259 }, 260 }; 261 } 262 263 // A jasmine reporter that writes results to this.state 264 _jasmineSetStateReporter(jasmineEnv) { 265 const app = this; 266 return { 267 suiteStarted(jasmineResult) { 268 app.setState(({ state }) => ({ 269 state: state 270 .updateIn(state.get('path'), children => 271 children.push( 272 Immutable.fromJS({ 273 result: jasmineResult, 274 children: [], 275 specs: [], 276 }) 277 ) 278 ) 279 .update('path', path => path.push(state.getIn(path).size, 'children')), 280 })); 281 }, 282 283 suiteDone(jasmineResult) { 284 app.setState(({ state }) => ({ 285 state: state 286 .updateIn( 287 state 288 .get('path') 289 .pop() 290 .pop(), 291 children => 292 children.update(children.size - 1, child => 293 child.set('result', child.get('result')) 294 ) 295 ) 296 .update('path', path => path.pop().pop()), 297 })); 298 }, 299 300 specStarted(jasmineResult) { 301 app.setState(({ state }) => ({ 302 state: state.updateIn( 303 state 304 .get('path') 305 .pop() 306 .pop(), 307 children => 308 children.update(children.size - 1, child => 309 child.update('specs', specs => specs.push(Immutable.fromJS(jasmineResult))) 310 ) 311 ), 312 })); 313 }, 314 315 specDone(jasmineResult) { 316 if (app.state.testPortal) { 317 console.warn( 318 `The test portal has not been cleaned up by \`${ 319 jasmineResult.fullName 320 }\`. Call \`cleanupPortal\` before finishing the test.` 321 ); 322 } 323 324 app.setState(({ state }) => ({ 325 state: state.updateIn( 326 state 327 .get('path') 328 .pop() 329 .pop(), 330 children => 331 children.update(children.size - 1, child => 332 child.update('specs', specs => 333 specs.set(specs.size - 1, Immutable.fromJS(jasmineResult)) 334 ) 335 ) 336 ), 337 })); 338 }, 339 }; 340 } 341 342 // --- Rendering ------------------------------------------------------------- 343 344 _renderSpecResult = r => { 345 const status = r.get('status') || 'running'; 346 return ( 347 <View 348 key={r.get('id')} 349 style={{ 350 paddingLeft: 10, 351 marginVertical: 3, 352 borderColor: { 353 running: '#ff0', 354 passed: '#0f0', 355 failed: '#f00', 356 disabled: '#888', 357 }[status], 358 borderLeftWidth: 3, 359 }}> 360 <Text style={{ fontSize: 16 }}> 361 { 362 { 363 running: ' ', 364 passed: ' ', 365 failed: ' ', 366 }[status] 367 } 368 {r.get('description')} ({status}) 369 </Text> 370 {r.get('failedExpectations').map((e, i) => ( 371 <Text key={i}>{e.get('message')}</Text> 372 ))} 373 </View> 374 ); 375 }; 376 _renderSuiteResult = (r, depth) => { 377 const titleStyle = 378 depth == 0 379 ? { marginBottom: 8, fontSize: 16, fontWeight: 'bold' } 380 : { marginVertical: 8, fontSize: 16 }; 381 const containerStyle = 382 depth == 0 383 ? { 384 paddingLeft: 16, 385 paddingVertical: 16, 386 borderBottomWidth: 1, 387 borderColor: '#ddd', 388 } 389 : { paddingLeft: 16 }; 390 return ( 391 <View key={r.get('result').get('id')} style={containerStyle}> 392 <Text style={titleStyle}>{r.get('result').get('description')}</Text> 393 {r.get('specs').map(this._renderSpecResult)} 394 {r.get('children').map(r => this._renderSuiteResult(r, depth + 1))} 395 </View> 396 ); 397 }; 398 _onScrollViewContentSizeChange = (contentWidth, contentHeight) => { 399 if (this._scrollViewRef) { 400 this._scrollViewRef.scrollTo({ 401 y: Math.max(0, contentHeight - Dimensions.get('window').height) + Constants.statusBarHeight, 402 }); 403 } 404 }; 405 _renderPortal = () => { 406 const styles = { 407 top: 0, 408 left: 0, 409 right: 0, 410 bottom: 0, 411 position: 'absolute', 412 alignItems: 'center', 413 justifyContent: 'center', 414 backgroundColor: 'rgb(255, 255, 255)', 415 opacity: this.state.portalChildShouldBeVisible ? 0.5 : 0, 416 }; 417 418 if (this.state.testPortal) { 419 return ( 420 <View style={styles} pointerEvents="none"> 421 {this.state.testPortal} 422 </View> 423 ); 424 } 425 }; 426 427 render() { 428 if (this.state.testRunnerError) { 429 return ( 430 <View 431 style={{ 432 flex: 1, 433 marginTop: Constants.statusBarHeight || 18, 434 alignItems: 'center', 435 justifyContent: 'center', 436 }}> 437 <Text style={{ color: 'red' }}>{this.state.testRunnerError}</Text> 438 </View> 439 ); 440 } 441 442 return ( 443 <View 444 style={{ 445 flex: 1, 446 marginTop: Constants.statusBarHeight || 18, 447 alignItems: 'stretch', 448 justifyContent: 'center', 449 }} 450 testID="test_suite_container"> 451 <ScrollView 452 style={{ 453 flex: 1, 454 }} 455 contentContainerStyle={{ 456 padding: 5, 457 }} 458 ref={ref => (this._scrollViewRef = ref)} 459 onContentSizeChange={this._onScrollViewContentSizeChange}> 460 {this.state.state.get('suites').map(r => this._renderSuiteResult(r, 0))} 461 </ScrollView> 462 {this._renderPortal()} 463 </View> 464 ); 465 } 466} 467