xref: /expo/apps/test-suite/screens/TestScreen.js (revision 22d1e005)
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