diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 00000000..5afd82db --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +__tests__/build/ diff --git a/tests/__tests__/src/tests/pendingTestTests.js b/tests/__tests__/src/tests/pendingTestTests.js index e7c128a9..1dafed1c 100644 --- a/tests/__tests__/src/tests/pendingTestTests.js +++ b/tests/__tests__/src/tests/pendingTestTests.js @@ -103,6 +103,84 @@ function pendingTestTests({ it: _it, describe: _describe }) { otherTest.should.be.called(); }); }); + + _describe('when an outer context is focused', () => { + _it('a pending test will still not run', async () => { + const pendingTest = sinon.spy(); + const otherTest = sinon.spy(); + const unfocusedTest = sinon.spy(); + + const testSuite = new TestSuite('', '', {}); + + testSuite.addTests(({ fdescribe, it, xit }) => { + fdescribe('', () => { + xit('', pendingTest); + + it('', otherTest); + }); + + it('', unfocusedTest); + }); + + testSuite.setStore({ + getState: () => { return {}; }, + }); + + const testIdsToRun = Object.keys(testSuite.testDefinitions.focusedTestIds).reduce((memo, testId) => { + if (!testSuite.testDefinitions.pendingTestIds[testId]) { + memo.push(testId); + } + + return memo; + }, []); + + await testSuite.run(testIdsToRun); + + pendingTest.should.not.be.called(); + otherTest.should.be.called(); + unfocusedTest.should.not.be.called(); + }); + }); + + _describe('when an outer context is focused', () => { + _it('a pending context will still not run', async () => { + const pendingTest = sinon.spy(); + const otherTest = sinon.spy(); + const unfocusedTest = sinon.spy(); + + const testSuite = new TestSuite('', '', {}); + + testSuite.addTests(({ fdescribe, it, xdescribe }) => { + fdescribe('', () => { + xdescribe('', () => { + it('', pendingTest); + }); + + it('', otherTest); + }); + + it('', unfocusedTest); + }); + + testSuite.setStore({ + getState: () => { return {}; }, + }); + + const testIdsToRun = Object.keys(testSuite.testDefinitions.focusedTestIds).reduce((memo, testId) => { + if (!testSuite.testDefinitions.pendingTestIds[testId]) { + memo.push(testId); + } + + return memo; + }, []); + + await testSuite.run(testIdsToRun); + + pendingTest.should.not.be.called(); + otherTest.should.be.called(); + unfocusedTest.should.not.be.called(); + }); + }); } export default pendingTestTests; diff --git a/tests/lib/TestRun.js b/tests/lib/TestRun.js index c927949d..82bd4484 100644 --- a/tests/lib/TestRun.js +++ b/tests/lib/TestRun.js @@ -286,8 +286,7 @@ class TestRun { suiteId: this.testSuite.id, status: RunStatus.ERR, time: Date.now() - this.runStartTime, - message: `Test suite failed: ${error.message}`, - stackTrace: error.stack, + message: `Test suite failed: ${error.message}` }); }); } @@ -306,7 +305,7 @@ class TestRun { } async _safelyRunFunction(func, timeOutDuration, description) { - const syncResultOrPromise = tryCatcher(func); + const syncResultOrPromise = captureThrownErrors(func); if (syncResultOrPromise.error) { // Synchronous Error @@ -314,49 +313,59 @@ class TestRun { } // Asynchronous Error - return promiseToCallBack(syncResultOrPromise.value, timeOutDuration, description); + return capturePromiseErrors(syncResultOrPromise.result, timeOutDuration, description); } } /** - * Try catch to object - * @returns {{}} + * Call a function and capture any errors that are immediately thrown. + * @returns {Object} Object containing result of executing the function, or the error + * message that was captured * @private */ -function tryCatcher(func) { +function captureThrownErrors(func) { const result = {}; try { - result.value = func(); - } catch (e) { - result.error = e; + result.result = func(); + } catch (error) { + result.error = error; } return result; } /** - * Make a promise callback-able to trap errors - * @param promise + * Wraps a promise so that if it's rejected or an error is thrown while it's being + * evaluated, it's captured and thrown no further + * @param {*} target - Target to wrap. If a thenable object, it's wrapped so if it's + * rejected or an error is thrown, it will be captured. If a non-thenable object, + * wrapped in resolved promise and returned. + * @param {Number} timeoutDuration - Number of milliseconds the promise is allowed + * to pend before it's considered timed out + * @param {String} description - Description of the context the promises is defined + * in, used for reporting where a timeout occurred in the resulting error message. * @private */ -function promiseToCallBack(promise, timeoutDuration, description) { +function capturePromiseErrors(target, timeoutDuration, description) { let returnValue = null; try { - returnValue = Promise.resolve(promise) + returnValue = Promise.resolve(target) .then(() => { return null; }, (error) => { return Promise.resolve(error); }) - .timeout(timeoutDuration, `${description} took longer than ${timeoutDuration}ms. This can be extended with the timeout option.`) .catch((error) => { return Promise.resolve(error); - }); + }) + .timeout(timeoutDuration, + `${description} took longer than ${timeoutDuration}ms. This can be extended with the timeout option.` + ); } catch (error) { returnValue = Promise.resolve(error); } diff --git a/tests/lib/TestSuite.js b/tests/lib/TestSuite.js index 3924bb8b..aaa6735c 100644 --- a/tests/lib/TestSuite.js +++ b/tests/lib/TestSuite.js @@ -111,19 +111,19 @@ class TestSuite { */ async run(testIds = undefined) { const testsToRun = (() => { - if (testIds) { - return testIds.map((id) => { - const test = this.testDefinitions.tests[id]; + return (testIds || Object.keys(this.testDefinitions.tests)).reduce((memo, id) => { + const test = this.testDefinitions.tests[id]; - if (!test) { - throw new RangeError(`ReactNativeFirebaseTests.TestRunError: Test with id ${id} not found in test suite ${this.name}`); - } + if (!test) { + throw new RangeError(`ReactNativeFirebaseTests.TestRunError: Test with id ${id} not found in test suite ${this.name}`); + } - return test; - }); - } + if (!this.testDefinitions.pendingTestIds[id]) { + memo.push(test); + } - return Object.values(this.testDefinitions.tests); + return memo; + }, []); })(); const testRun = new TestRun(this, testsToRun.reverse(), this.testDefinitions); diff --git a/tests/src/actions/TestActions.js b/tests/src/actions/TestActions.js index a2ed5fe3..71487b0a 100644 --- a/tests/src/actions/TestActions.js +++ b/tests/src/actions/TestActions.js @@ -14,13 +14,14 @@ export function setSuiteStatus({ suiteId, status, time, message, progress }) { }; } -export function setTestStatus({ testId, status, time = 0, message = null }) { +export function setTestStatus({ testId, status, stackTrace, time = 0, message = null }) { return { type: TEST_SET_STATUS, testId, status, message, + stackTrace, time, }; } diff --git a/tests/src/reducers/testsReducers.js b/tests/src/reducers/testsReducers.js index 204e600f..cfd6fb07 100644 --- a/tests/src/reducers/testsReducers.js +++ b/tests/src/reducers/testsReducers.js @@ -12,6 +12,7 @@ function testsReducers(state = initState.tests, action: Object): State { flattened[`${action.testId}.status`] = action.status; flattened[`${action.testId}.message`] = action.message; flattened[`${action.testId}.time`] = action.time; + flattened[`${action.testId}.stackTrace`] = action.stackTrace; return unflatten(flattened); } diff --git a/tests/src/screens/Test.js b/tests/src/screens/Test.js index e716ece1..d69cb412 100644 --- a/tests/src/screens/Test.js +++ b/tests/src/screens/Test.js @@ -51,35 +51,28 @@ class Test extends React.Component { setParams({ test }); } - renderError() { - const { test: { message } } = this.props; - - if (message) { - return ( - - Test Error - - {message} - - - ); - } - - return null; - } - - render() { - const { test: { func, status, time } } = this.props; + const { test: { stackTrace, description, func, status, time }, testContextName } = this.props; return ( {Test.renderBanner({ status, time })} - - {this.renderError()} - Test Code Preview - - + + + {testContextName} + {description} + + + Test Error + + {stackTrace || 'None.'} + + + + Test Code Preview + + + {beautify(removeLastLine(removeFirstLine(func.toString())), { indent_size: 4, break_chained_methods: true })} @@ -93,10 +86,13 @@ Test.propTypes = { test: PropTypes.shape({ status: PropTypes.string, time: PropTypes.number, - message: PropTypes.string, func: PropTypes.function, + stackTrace: PropTypes.function, + description: PropTypes.string, }).isRequired, + testContextName: PropTypes.string, + navigation: PropTypes.shape({ setParams: PropTypes.func.isRequired, }).isRequired, @@ -107,27 +103,32 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: '#ffffff', }, - content: {}, - code: { - backgroundColor: '#3F373A', - color: '#c3c3c3', - padding: 5, - fontSize: 12, + sectionContainer: { + minHeight: 100, }, - codeHeader: { - fontWeight: '600', - fontSize: 18, - backgroundColor: '#000', - color: '#fff', + heading: { padding: 5, + backgroundColor: '#0288d1', + fontWeight: '600', + color: '#ffffff', + fontSize: 16, + }, + description: { + padding: 5, + fontSize: 14, }, }); -function select({ tests }, { navigation: { state: { params: { testId } } } }) { +function select({ tests, testContexts }, { navigation: { state: { params: { testId } } } }) { const test = tests[testId]; + let testContext = testContexts[test.testContextId]; + while(testContext.parentContextId && testContexts[testContext.parentContextId].parentContextId) { + testContext = testContexts[testContext.parentContextId]; + } return { test, + testContextName: testContext.name, }; }