'use strict'; import Immutable from 'immutable'; import jasmineModule from 'jasmine-core/lib/jasmine-core/jasmine'; import React from 'react'; import { Platform, StyleSheet, View } from 'react-native'; import ExponentTest from '../ExponentTest'; import { getTestModules } from '../TestModules'; import Portal from '../components/Portal'; import RunnerError from '../components/RunnerError'; import Suites from '../components/Suites'; const initialState = { portalChildShouldBeVisible: false, state: Immutable.fromJS({ suites: [], path: ['suites'], // Path to current 'children' List in state }), testPortal: null, numFailed: 0, done: false, }; export default class TestScreen extends React.Component { state = initialState; _results = ''; _failures = ''; _scrollViewRef = null; componentDidMount() { const selectionQuery = this.props.route.params?.tests ?? []; const selectedTestNames = selectionQuery.split(' '); // We get test modules here to make sure that React Native will reload this component when tests were changed. const selectedModules = getTestModules().filter((m) => selectedTestNames.includes(m.name)); if (!selectedModules.length) { console.log('[TEST_SUITE]', 'No selected modules', selectedTestNames); } this._runTests(selectedModules); this._isMounted = true; } componentWillUnmount() { this._isMounted = false; } setPortalChild = (testPortal) => { if (this._isMounted) return this.setState({ testPortal }); }; cleanupPortal = () => { return new Promise((resolve) => { if (this._isMounted) this.setState({ testPortal: null }, resolve); }); }; _runTests = async (modules) => { // Reset results state this.setState(initialState); const { jasmineEnv, jasmine } = await this._setupJasmine(); 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() { const failedSpecs = []; const app = this; 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}`); app._results += `${grouping} ${result.fullName}\n`; if (result.status === 'failed') { app._failures += `${grouping} ${result.fullName}\n`; result.failedExpectations.forEach(({ matcherName = 'NO_MATCHER', message }) => { if (ExponentTest && ExponentTest.log) { ExponentTest.log(`${matcherName}: ${message}`); } console.log(`${matcherName}: ${message}`); app._results += `${matcherName}: ${message}\n`; app._failures += `${matcherName}: ${message}\n`; }); failedSpecs.push(result); if (app._isMounted) { const result = { magic: '[TEST-SUITE-INPROGRESS]', failed: failedSpecs.length, failures: app._failures, results: app._results, }; const jsonResult = JSON.stringify(result); app.setState({ numFailed: failedSpecs.length, results: jsonResult }); } } } }, jasmineStarted() { console.log('--- tests started'); }, jasmineDone() { console.log('--- tests done'); console.log('--- sending results to runner'); const result = { magic: '[TEST-SUITE-END]', // NOTE: Runner/Run.js waits to see this failed: failedSpecs.length, failures: app._failures, results: app._results, }; const jsonResult = JSON.stringify(result); if (app._isMounted) { app.setState({ done: true, numFailed: failedSpecs.length, results: jsonResult }); } if (Platform.OS === 'web') { // This log needs to be an object for puppeteer tests console.log(result); } else { console.log(jsonResult); } if (ExponentTest) { ExponentTest.completed( JSON.stringify({ failed: failedSpecs.length, failures: app._failures, results: app._results, }) ); } }, }; } // A jasmine reporter that writes results to this.state _jasmineSetStateReporter() { const app = this; return { suiteStarted(jasmineResult) { if (app._isMounted) { 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() { if (app._isMounted) { 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) { if (app._isMounted) { 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.` ); } if (app._isMounted) { 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)) ) ) ), })); } }, }; } render() { const { testRunnerError, results, done, numFailed, state, portalChildShouldBeVisible, testPortal, } = this.state; if (testRunnerError) { return {testRunnerError}; } return ( {testPortal} ); } } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'stretch', justifyContent: 'center', }, });