Merge pull request #90 from greena13/dev_focused_pending_tests

[Test Suite] Test Suite Improvements
This commit is contained in:
Michael Diarmid 2017-05-06 15:23:23 +01:00 committed by GitHub
commit 71a9bf4857
7 changed files with 155 additions and 64 deletions

1
tests/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
__tests__/build/

View File

@ -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;

View File

@ -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);
}

View File

@ -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);

View File

@ -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,
};
}

View File

@ -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);
}

View File

@ -51,35 +51,28 @@ class Test extends React.Component {
setParams({ test });
}
renderError() {
const { test: { message } } = this.props;
if (message) {
return (
<ScrollView>
<Text style={styles.codeHeader}>Test Error</Text>
<Text style={styles.code}>
<Text>{message}</Text>
</Text>
</ScrollView>
);
}
return null;
}
render() {
const { test: { func, status, time } } = this.props;
const { test: { stackTrace, description, func, status, time }, testContextName } = this.props;
return (
<View style={styles.container}>
{Test.renderBanner({ status, time })}
<View style={styles.content}>
{this.renderError()}
<Text style={styles.codeHeader}>Test Code Preview</Text>
<ScrollView>
<Text style={styles.code}>
<View >
<ScrollView style={styles.sectionContainer}>
<Text style={styles.heading}>{testContextName}</Text>
<Text style={styles.description}>{description}</Text>
</ScrollView>
<ScrollView style={styles.sectionContainer}>
<Text style={styles.heading}>Test Error</Text>
<Text style={styles.description}>
<Text>{stackTrace || 'None.'}</Text>
</Text>
</ScrollView>
<Text style={styles.heading}>
Test Code Preview
</Text>
<ScrollView style={styles.sectionContainer}>
<Text style={styles.description}>
{beautify(removeLastLine(removeFirstLine(func.toString())), { indent_size: 4, break_chained_methods: true })}
</Text>
</ScrollView>
@ -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,
};
}