1'use strict'; 2import Immutable from 'immutable'; 3import jasmineModule from 'jasmine-core/lib/jasmine-core/jasmine'; 4import React from 'react'; 5import { Platform, StyleSheet, View } from 'react-native'; 6 7import ExponentTest from '../ExponentTest'; 8import { getTestModules } from '../TestModules'; 9import Portal from '../components/Portal'; 10import RunnerError from '../components/RunnerError'; 11import Suites from '../components/Suites'; 12 13const initialState = { 14 portalChildShouldBeVisible: false, 15 state: Immutable.fromJS({ 16 suites: [], 17 path: ['suites'], // Path to current 'children' List in state 18 }), 19 testPortal: null, 20 numFailed: 0, 21 done: false, 22}; 23 24export default class TestScreen extends React.Component { 25 state = initialState; 26 _results = ''; 27 _failures = ''; 28 _scrollViewRef = null; 29 30 componentDidMount() { 31 const selectionQuery = this.props.route.params?.tests ?? []; 32 const selectedTestNames = selectionQuery.split(' '); 33 34 // We get test modules here to make sure that React Native will reload this component when tests were changed. 35 const selectedModules = getTestModules().filter(m => selectedTestNames.includes(m.name)); 36 37 if (!selectedModules.length) { 38 console.log('[TEST_SUITE]', 'No selected modules', selectedTestNames); 39 } 40 41 this._runTests(selectedModules); 42 this._isMounted = true; 43 } 44 45 componentWillUnmount() { 46 this._isMounted = false; 47 } 48 49 setPortalChild = testPortal => { 50 if (this._isMounted) return this.setState({ testPortal }); 51 }; 52 53 cleanupPortal = () => { 54 return new Promise(resolve => { 55 if (this._isMounted) this.setState({ testPortal: null }, resolve); 56 }); 57 }; 58 59 _runTests = async modules => { 60 // Reset results state 61 this.setState(initialState); 62 63 const { jasmineEnv, jasmine } = await this._setupJasmine(); 64 65 await Promise.all( 66 modules.map(m => 67 m.test(jasmine, { 68 setPortalChild: this.setPortalChild, 69 cleanupPortal: this.cleanupPortal, 70 }) 71 ) 72 ); 73 74 jasmineEnv.execute(); 75 }; 76 77 async _setupJasmine() { 78 // Init 79 jasmineModule.DEFAULT_TIMEOUT_INTERVAL = 10000; 80 const jasmineCore = jasmineModule.core(jasmineModule); 81 const jasmineEnv = jasmineCore.getEnv(); 82 83 // Add our custom reporters too 84 jasmineEnv.addReporter(this._jasmineSetStateReporter()); 85 jasmineEnv.addReporter(this._jasmineConsoleReporter()); 86 87 // Get the interface and make it support `async ` by default 88 const jasmine = jasmineModule.interface(jasmineCore, jasmineEnv); 89 const doneIfy = fn => async done => { 90 try { 91 await Promise.resolve(fn()); 92 done(); 93 } catch (e) { 94 done.fail(e); 95 } 96 }; 97 const oldIt = jasmine.it; 98 jasmine.it = (desc, fn, t) => oldIt.apply(jasmine, [desc, doneIfy(fn), t]); 99 const oldXit = jasmine.xit; 100 jasmine.xit = (desc, fn, t) => oldXit.apply(jasmine, [desc, doneIfy(fn), t]); 101 const oldFit = jasmine.fit; 102 jasmine.fit = (desc, fn, t) => oldFit.apply(jasmine, [desc, doneIfy(fn), t]); 103 104 return { 105 jasmineCore, 106 jasmineEnv, 107 jasmine, 108 }; 109 } 110 111 // A jasmine reporter that writes results to the console 112 _jasmineConsoleReporter() { 113 const failedSpecs = []; 114 const app = this; 115 116 return { 117 specDone(result) { 118 if (result.status === 'passed' || result.status === 'failed') { 119 // Open log group if failed 120 const grouping = result.status === 'passed' ? '---' : '+++'; 121 if (ExponentTest && ExponentTest.log) { 122 ExponentTest.log(`${result.status === 'passed' ? 'PASS' : 'FAIL'} ${result.fullName}`); 123 } 124 const emoji = result.status === 'passed' ? ':green_heart:' : ':broken_heart:'; 125 console.log(`${grouping} ${emoji} ${result.fullName}`); 126 app._results += `${grouping} ${result.fullName}\n`; 127 128 if (result.status === 'failed') { 129 app._failures += `${grouping} ${result.fullName}\n`; 130 result.failedExpectations.forEach(({ matcherName = 'NO_MATCHER', message }) => { 131 if (ExponentTest && ExponentTest.log) { 132 ExponentTest.log(`${matcherName}: ${message}`); 133 } 134 console.log(`${matcherName}: ${message}`); 135 app._results += `${matcherName}: ${message}\n`; 136 app._failures += `${matcherName}: ${message}\n`; 137 }); 138 failedSpecs.push(result); 139 if (app._isMounted) { 140 const result = { 141 magic: '[TEST-SUITE-INPROGRESS]', 142 failed: failedSpecs.length, 143 failures: app._failures, 144 results: app._results, 145 }; 146 const jsonResult = JSON.stringify(result); 147 app.setState({ numFailed: failedSpecs.length, results: jsonResult }); 148 } 149 } 150 } 151 }, 152 153 jasmineStarted() { 154 console.log('--- tests started'); 155 }, 156 157 jasmineDone() { 158 console.log('--- tests done'); 159 console.log('--- sending results to runner'); 160 161 const result = { 162 magic: '[TEST-SUITE-END]', // NOTE: Runner/Run.js waits to see this 163 failed: failedSpecs.length, 164 failures: app._failures, 165 results: app._results, 166 }; 167 168 const jsonResult = JSON.stringify(result); 169 if (app._isMounted) { 170 app.setState({ done: true, numFailed: failedSpecs.length, results: jsonResult }); 171 } 172 173 if (Platform.OS === 'web') { 174 // This log needs to be an object for puppeteer tests 175 console.log(result); 176 } else { 177 console.log(jsonResult); 178 } 179 180 if (ExponentTest) { 181 ExponentTest.completed( 182 JSON.stringify({ 183 failed: failedSpecs.length, 184 failures: app._failures, 185 results: app._results, 186 }) 187 ); 188 } 189 }, 190 }; 191 } 192 193 // A jasmine reporter that writes results to this.state 194 _jasmineSetStateReporter() { 195 const app = this; 196 return { 197 suiteStarted(jasmineResult) { 198 if (app._isMounted) { 199 app.setState(({ state }) => ({ 200 state: state 201 .updateIn(state.get('path'), children => 202 children.push( 203 Immutable.fromJS({ 204 result: jasmineResult, 205 children: [], 206 specs: [], 207 }) 208 ) 209 ) 210 .update('path', path => path.push(state.getIn(path).size, 'children')), 211 })); 212 } 213 }, 214 215 suiteDone() { 216 if (app._isMounted) { 217 app.setState(({ state }) => ({ 218 state: state 219 .updateIn( 220 state 221 .get('path') 222 .pop() 223 .pop(), 224 children => 225 children.update(children.size - 1, child => 226 child.set('result', child.get('result')) 227 ) 228 ) 229 .update('path', path => path.pop().pop()), 230 })); 231 } 232 }, 233 234 specStarted(jasmineResult) { 235 if (app._isMounted) { 236 app.setState(({ state }) => ({ 237 state: state.updateIn( 238 state 239 .get('path') 240 .pop() 241 .pop(), 242 children => 243 children.update(children.size - 1, child => 244 child.update('specs', specs => specs.push(Immutable.fromJS(jasmineResult))) 245 ) 246 ), 247 })); 248 } 249 }, 250 251 specDone(jasmineResult) { 252 if (app.state.testPortal) { 253 console.warn( 254 `The test portal has not been cleaned up by \`${jasmineResult.fullName}\`. Call \`cleanupPortal\` before finishing the test.` 255 ); 256 } 257 if (app._isMounted) { 258 app.setState(({ state }) => ({ 259 state: state.updateIn( 260 state 261 .get('path') 262 .pop() 263 .pop(), 264 children => 265 children.update(children.size - 1, child => 266 child.update('specs', specs => 267 specs.set(specs.size - 1, Immutable.fromJS(jasmineResult)) 268 ) 269 ) 270 ), 271 })); 272 } 273 }, 274 }; 275 } 276 277 render() { 278 const { 279 testRunnerError, 280 results, 281 done, 282 numFailed, 283 state, 284 portalChildShouldBeVisible, 285 testPortal, 286 } = this.state; 287 if (testRunnerError) { 288 return <RunnerError>{testRunnerError}</RunnerError>; 289 } 290 return ( 291 <View testID="test_suite_container" style={styles.container}> 292 <Suites numFailed={numFailed} results={results} done={done} suites={state.get('suites')} /> 293 <Portal isVisible={portalChildShouldBeVisible}>{testPortal}</Portal> 294 </View> 295 ); 296 } 297} 298 299const styles = StyleSheet.create({ 300 container: { 301 flex: 1, 302 alignItems: 'stretch', 303 justifyContent: 'center', 304 }, 305}); 306