'use strict'; import React from 'react'; import { Dimensions, Linking, NativeModules, Platform, ScrollView, Text, View } from 'react-native'; import { Constants } from 'expo'; import jasmineModule from 'jasmine-core/lib/jasmine-core/jasmine'; import Immutable from 'immutable'; import * as TestUtils from './TestUtils'; let { ExponentTest } = NativeModules; // List of all modules for tests. Each file path must be statically present for // the packager to pick them all up. async function getTestModulesAsync() { // The tests don't complete on CircleCI on iOS so we test just that the app launches and runs if (Platform.OS === 'ios') { let isInCI = await TestUtils.shouldSkipTestsRequiringPermissionsAsync(); if (isInCI) { return []; } } if (Platform.OS === 'web') { return [ require('./tests/Import1'), require('./tests/Crypto'), require('./tests/Random'), ]; } let modules = [ // require('./tests/Basic1'), // require('./tests/Basic2'), require('./tests/Import1'), require('./tests/Import2'), require('./tests/Import3'), require('./tests/Asset'), require('./tests/Audio'), require('./tests/Calendar'), require('./tests/Constants'), require('./tests/Contacts'), require('./tests/Crypto'), require('./tests/FileSystem'), require('./tests/GoogleSignIn'), require('./tests/Haptics'), require('./tests/Localization'), require('./tests/Location'), require('./tests/Linking'), require('./tests/Recording'), require('./tests/ScreenOrientation'), require('./tests/SecureStore'), require('./tests/Segment'), require('./tests/Speech'), require('./tests/SQLite'), require('./tests/Random'), require('./tests/Payments'), require('./tests/AdMobInterstitial'), require('./tests/AdMobBanner'), require('./tests/AdMobPublisherBanner'), require('./tests/AdMobRewarded'), require('./tests/Video'), require('./tests/Permissions'), require('./tests/MediaLibrary'), require('./tests/Notifications'), require('./tests/FBNativeAd'), require('./tests/FBBannerAd'), require('./tests/TaskManager'), ]; if (Constants.isDevice) { modules = modules.concat([require('./tests/Brightness')]); modules = modules.concat([require('./tests/BarCodeScanner')]); if (Platform.OS === 'android') { modules = modules.concat([require('./tests/JSC')]); // The Camera tests are flaky on iOS, i.e. they fail randomly modules = modules.concat([require('./tests/Camera')]); } } return modules; } export default class App extends React.Component { // --- Lifecycle ------------------------------------------------------------- constructor(props, context) { super(props, context); this.state = App.initialState; this._results = ''; this._failures = ''; this._scrollViewRef = null; } componentDidMount() { this._runTests(this.props.exp.initialUri); Linking.addEventListener('url', ({ url }) => url && this._runTests(url)); } // --- Test running ---------------------------------------------------------- static initialState = { portalChildShouldBeVisible: false, testRunnerError: null, state: Immutable.fromJS({ suites: [], path: ['suites'], // Path to current 'children' List in state }), }; setPortalChild = testPortal => this.setState({ testPortal }); cleanupPortal = () => new Promise(resolve => this.setState({ testPortal: null }, resolve)); async _runTests(uri) { // If the URL contains two pluses let's keep the existing state instead of rerunning tests. // This way we are able to test the Linking module. if (uri && uri.indexOf('++') > -1) { return; } // Reset results state this.setState(App.initialState); const { jasmineEnv, jasmine } = await this._setupJasmine(); // Load tests, confining to the ones named in the uri let modules = await getTestModulesAsync(); if (uri && uri.indexOf('--/') > -1) { const deepLink = uri.substring(uri.indexOf('--/') + 3); const filterJSON = JSON.parse(deepLink); if (filterJSON.includeModules) { console.log('Only testing these modules: ' + JSON.stringify(filterJSON.includeModules)); const includeModulesRegexes = filterJSON.includeModules.map(m => new RegExp(m)); modules = modules.filter(m => { for (let i = 0; i < includeModulesRegexes.length; i++) { if (includeModulesRegexes[i].test(m.name)) { return true; } } return false; }); if (modules.length === 0) { this.setState({ testRunnerError: `No tests were found that satisfy ${deepLink}`, }); return; } } } await Promise.all( modules.map(m => m.test(jasmine, { setPortalChild: this.setPortalChild, cleanupPortal: this.cleanupPortal, }) ) ); jasmineEnv.execute(); } async _setupJasmine() { // Init jasmineModule.DEFAULT_TIMEOUT_INTERVAL = 10000; const jasmineCore = jasmineModule.core(jasmineModule); const jasmineEnv = jasmineCore.getEnv(); // Add our custom reporters too jasmineEnv.addReporter(this._jasmineSetStateReporter()); jasmineEnv.addReporter(this._jasmineConsoleReporter()); // Get the interface and make it support `async ` by default const jasmine = jasmineModule.interface(jasmineCore, jasmineEnv); const doneIfy = fn => async done => { try { await Promise.resolve(fn()); done(); } catch (e) { done.fail(e); } }; const oldIt = jasmine.it; jasmine.it = (desc, fn, t) => oldIt.apply(jasmine, [desc, doneIfy(fn), t]); const oldXit = jasmine.xit; jasmine.xit = (desc, fn, t) => oldXit.apply(jasmine, [desc, doneIfy(fn), t]); const oldFit = jasmine.fit; jasmine.fit = (desc, fn, t) => oldFit.apply(jasmine, [desc, doneIfy(fn), t]); return { jasmineCore, jasmineEnv, jasmine, }; } // A jasmine reporter that writes results to the console _jasmineConsoleReporter(jasmineEnv) { const failedSpecs = []; return { specDone(result) { if (result.status === 'passed' || result.status === 'failed') { // Open log group if failed const grouping = result.status === 'passed' ? '---' : '+++'; if (ExponentTest && ExponentTest.log) { ExponentTest.log(`${result.status === 'passed' ? 'PASS' : 'FAIL'} ${result.fullName}`); } const emoji = result.status === 'passed' ? ':green_heart:' : ':broken_heart:'; console.log(`${grouping} ${emoji} ${result.fullName}`); this._results += `${grouping} ${result.fullName}\n`; if (result.status === 'failed') { this._failures += `${grouping} ${result.fullName}\n`; result.failedExpectations.forEach(({ matcherName, message }) => { if (ExponentTest && ExponentTest.log) { ExponentTest.log(`${matcherName}: ${message}`); } console.log(`${matcherName}: ${message}`); this._results += `${matcherName}: ${message}\n`; this._failures += `${matcherName}: ${message}\n`; }); failedSpecs.push(result); } } }, suiteDone(result) {}, jasmineStarted() { console.log('--- tests started'); }, jasmineDone() { console.log('--- tests done'); console.log('--- send results to runner'); const result = { magic: '[TEST-SUITE-END]', // NOTE: Runner/Run.js waits to see this failed: failedSpecs.length, results: this._results, }; if (Platform.OS === 'web') { // This log needs to be an object for puppeteer tests console.log(result); } else { const jsonResult = JSON.stringify(result); console.log(jsonResult); } if (ExponentTest) { // Native logs are truncated so log just the failures for now ExponentTest.completed( JSON.stringify({ failed: failedSpecs.length, failures: this._failures, }) ); } }, }; } // A jasmine reporter that writes results to this.state _jasmineSetStateReporter(jasmineEnv) { const app = this; return { suiteStarted(jasmineResult) { app.setState(({ state }) => ({ state: state .updateIn(state.get('path'), children => children.push( Immutable.fromJS({ result: jasmineResult, children: [], specs: [], }) ) ) .update('path', path => path.push(state.getIn(path).size, 'children')), })); }, suiteDone(jasmineResult) { app.setState(({ state }) => ({ state: state .updateIn( state .get('path') .pop() .pop(), children => children.update(children.size - 1, child => child.set('result', child.get('result')) ) ) .update('path', path => path.pop().pop()), })); }, specStarted(jasmineResult) { app.setState(({ state }) => ({ state: state.updateIn( state .get('path') .pop() .pop(), children => children.update(children.size - 1, child => child.update('specs', specs => specs.push(Immutable.fromJS(jasmineResult))) ) ), })); }, specDone(jasmineResult) { if (app.state.testPortal) { console.warn( `The test portal has not been cleaned up by \`${ jasmineResult.fullName }\`. Call \`cleanupPortal\` before finishing the test.` ); } app.setState(({ state }) => ({ state: state.updateIn( state .get('path') .pop() .pop(), children => children.update(children.size - 1, child => child.update('specs', specs => specs.set(specs.size - 1, Immutable.fromJS(jasmineResult)) ) ) ), })); }, }; } // --- Rendering ------------------------------------------------------------- _renderSpecResult = r => { const status = r.get('status') || 'running'; return ( { { running: '😮 ', passed: '😄 ', failed: '😞 ', }[status] } {r.get('description')} ({status}) {r.get('failedExpectations').map((e, i) => ( {e.get('message')} ))} ); }; _renderSuiteResult = (r, depth) => { const titleStyle = depth == 0 ? { marginBottom: 8, fontSize: 16, fontWeight: 'bold' } : { marginVertical: 8, fontSize: 16 }; const containerStyle = depth == 0 ? { paddingLeft: 16, paddingVertical: 16, borderBottomWidth: 1, borderColor: '#ddd', } : { paddingLeft: 16 }; return ( {r.get('result').get('description')} {r.get('specs').map(this._renderSpecResult)} {r.get('children').map(r => this._renderSuiteResult(r, depth + 1))} ); }; _onScrollViewContentSizeChange = (contentWidth, contentHeight) => { if (this._scrollViewRef) { this._scrollViewRef.scrollTo({ y: Math.max(0, contentHeight - Dimensions.get('window').height) + Constants.statusBarHeight, }); } }; _renderPortal = () => { const styles = { top: 0, left: 0, right: 0, bottom: 0, position: 'absolute', alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgb(255, 255, 255)', opacity: this.state.portalChildShouldBeVisible ? 0.5 : 0, }; if (this.state.testPortal) { return ( {this.state.testPortal} ); } }; render() { if (this.state.testRunnerError) { return ( {this.state.testRunnerError} ); } return ( (this._scrollViewRef = ref)} onContentSizeChange={this._onScrollViewContentSizeChange}> {this.state.state.get('suites').map(r => this._renderSuiteResult(r, 0))} {this._renderPortal()} ); } }