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