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(state.get('path').pop().pop(), (children) => 220 children.update(children.size - 1, (child) => 221 child.set('result', child.get('result')) 222 ) 223 ) 224 .update('path', (path) => path.pop().pop()), 225 })); 226 } 227 }, 228 229 specStarted(jasmineResult) { 230 if (app._isMounted) { 231 app.setState(({ state }) => ({ 232 state: state.updateIn(state.get('path').pop().pop(), (children) => 233 children.update(children.size - 1, (child) => 234 child.update('specs', (specs) => specs.push(Immutable.fromJS(jasmineResult))) 235 ) 236 ), 237 })); 238 } 239 }, 240 241 specDone(jasmineResult) { 242 if (app.state.testPortal) { 243 console.warn( 244 `The test portal has not been cleaned up by \`${jasmineResult.fullName}\`. Call \`cleanupPortal\` before finishing the test.` 245 ); 246 } 247 if (app._isMounted) { 248 app.setState(({ state }) => ({ 249 state: state.updateIn(state.get('path').pop().pop(), (children) => 250 children.update(children.size - 1, (child) => 251 child.update('specs', (specs) => 252 specs.set(specs.size - 1, Immutable.fromJS(jasmineResult)) 253 ) 254 ) 255 ), 256 })); 257 } 258 }, 259 }; 260 } 261 262 render() { 263 const { 264 testRunnerError, 265 results, 266 done, 267 numFailed, 268 state, 269 portalChildShouldBeVisible, 270 testPortal, 271 } = this.state; 272 if (testRunnerError) { 273 return <RunnerError>{testRunnerError}</RunnerError>; 274 } 275 return ( 276 <View testID="test_suite_container" style={styles.container}> 277 <Suites numFailed={numFailed} results={results} done={done} suites={state.get('suites')} /> 278 <Portal isVisible={portalChildShouldBeVisible}>{testPortal}</Portal> 279 </View> 280 ); 281 } 282} 283 284const styles = StyleSheet.create({ 285 container: { 286 flex: 1, 287 alignItems: 'stretch', 288 justifyContent: 'center', 289 }, 290}); 291