mirror of
https://github.com/zhigang1992/react-native-firebase.git
synced 2026-04-28 12:15:44 +08:00
[tests] Move test suite into same repo
This commit is contained in:
5
tests/lib/RunStatus.js
Normal file
5
tests/lib/RunStatus.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
RUNNING: 'running',
|
||||
OK: 'success',
|
||||
ERR: 'error',
|
||||
};
|
||||
251
tests/lib/TestDSL.js
Normal file
251
tests/lib/TestDSL.js
Normal file
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* Class that provides DSL for declaratively defining tests. Provides a declarative
|
||||
* interface for {@link TestSuiteDefinition} and only reveals methods that are part
|
||||
* of the test definition DSL.
|
||||
*/
|
||||
class TestDSL {
|
||||
/**
|
||||
* Create a new instance of TestDSL
|
||||
* @param {TestSuiteDefinition} testSuiteDefinition - Class to delegate the heavy lifting
|
||||
* of satisfying the DSL, to.
|
||||
* @param {Object} firebase - Object containing native and web firebase instances
|
||||
* @param {Object} firebase.native - Native firebase instance
|
||||
* @para {Object} firebase.web - Web firebase instance
|
||||
*/
|
||||
constructor(testSuiteDefinition, firebase) {
|
||||
this._testSuiteDefinition = testSuiteDefinition;
|
||||
|
||||
this.firebase = firebase;
|
||||
|
||||
this.after = this.after.bind(this);
|
||||
this.afterEach = this.afterEach.bind(this);
|
||||
|
||||
this.before = this.before.bind(this);
|
||||
this.beforeEach = this.beforeEach.bind(this);
|
||||
|
||||
this.describe = this.describe.bind(this);
|
||||
/** Alias for {@link TestDSL#describe } */
|
||||
this.context = this.describe;
|
||||
this.fdescribe = this.fdescribe.bind(this);
|
||||
/** Alias for {@link TestDSL#fdescribe } */
|
||||
this.fcontext = this.fdescribe;
|
||||
this.xdescribe = this.xdescribe.bind(this);
|
||||
/** Alias for {@link TestDSL#xdescribe } */
|
||||
this.xcontext = this.xdescribe;
|
||||
|
||||
this.tryCatch = this.tryCatch.bind(this);
|
||||
|
||||
this.it = this.it.bind(this);
|
||||
this.xit = this.xit.bind(this);
|
||||
this.fit = this.fit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a function as a before hook to the current test context
|
||||
* @param {Object=} options - Options object
|
||||
* @param {Number=10000} options.timeout - Number of milliseconds before callback times
|
||||
* out
|
||||
* @param {Function} callback - Function to add as before hook to current test context
|
||||
*/
|
||||
before(options, callback = undefined) {
|
||||
const _options = callback ? options : {};
|
||||
const _callback = callback || options;
|
||||
|
||||
this._testSuiteDefinition.addBeforeHook(_callback, _options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a function as a before each hook to the current test context
|
||||
* @param {Object=} options - Options object
|
||||
* @param {Number=10000} options.timeout - Number of milliseconds before callback times
|
||||
* out
|
||||
* @param {Function} callback - Function to add as before each hook to current test
|
||||
* context
|
||||
*/
|
||||
beforeEach(options, callback = undefined) {
|
||||
const _options = callback ? options : {};
|
||||
const _callback = callback || options;
|
||||
|
||||
this._testSuiteDefinition.addBeforeEachHook(_callback, _options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a function as a after each hook to the current test context
|
||||
* @param {Object=} options - Options object
|
||||
* @param {Number=10000} options.timeout - Number of milliseconds before callback times
|
||||
* out
|
||||
* @param {Function} callback - Function to add as after each hook to current test context
|
||||
*/
|
||||
afterEach(options, callback = undefined) {
|
||||
const _options = callback ? options : {};
|
||||
const _callback = callback || options;
|
||||
|
||||
this._testSuiteDefinition.addAfterEachHook(_callback, _options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a function as a after hook to the current test context
|
||||
* @param {Object=} options - Options object
|
||||
* @param {Number=10000} options.timeout - Number of milliseconds before callback times
|
||||
* out
|
||||
* @param {Function} callback - Function to add as after hook to current test context
|
||||
*/
|
||||
after(options, callback = undefined) {
|
||||
const _options = callback ? options : {};
|
||||
const _callback = callback || options;
|
||||
|
||||
this._testSuiteDefinition.addAfterHook(_callback, _options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new test context
|
||||
* @param {String} name - name of new test context
|
||||
* @param {ContextOptions} options - options for context
|
||||
* @param {Function} [testDefinitions={}] - function that defines further test contexts and
|
||||
* tests using the test DSL
|
||||
*/
|
||||
describe(name, options, testDefinitions) {
|
||||
let _testDefinitions;
|
||||
let _options;
|
||||
|
||||
if (testDefinitions) {
|
||||
_testDefinitions = testDefinitions;
|
||||
_options = options;
|
||||
} else {
|
||||
_testDefinitions = options;
|
||||
_options = {};
|
||||
}
|
||||
|
||||
this._testSuiteDefinition.pushTestContext(name, _options);
|
||||
_testDefinitions();
|
||||
this._testSuiteDefinition.popTestContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new pending test context. Tests in a pending test context are not
|
||||
* run when the test suite is executed. They also appear greyed out.
|
||||
* @param {String} name - name of new test context
|
||||
* @param {ContextOptions} [options={}] - options for context
|
||||
* @param {Function} testDefinitions - function that defines further test contexts and
|
||||
* tests using the test DSL
|
||||
*/
|
||||
xdescribe(name, options, testDefinitions = undefined) {
|
||||
let _options = {};
|
||||
let _testDefinitions;
|
||||
|
||||
if (typeof options === 'function') {
|
||||
_options = { pending: true };
|
||||
_testDefinitions = options;
|
||||
} else {
|
||||
Object.assign(_options, options, { pending: true });
|
||||
_testDefinitions = testDefinitions;
|
||||
}
|
||||
|
||||
this.describe(name, _options, _testDefinitions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new focused test context. Tests in a focused test context are the only
|
||||
* ones that appear and are run when the test suite is executed.
|
||||
* @param {String} name - name of new test context
|
||||
* @param {ContextOptions} [options={}] - options for context
|
||||
* @param {Function} testDefinitions - function that defines further test contexts and
|
||||
* tests using the test DSL
|
||||
*/
|
||||
fdescribe(name, options, testDefinitions = undefined) {
|
||||
let _options = {};
|
||||
let _testDefinitions;
|
||||
|
||||
if (typeof options === 'function') {
|
||||
_options = { focus: true };
|
||||
_testDefinitions = options;
|
||||
} else {
|
||||
Object.assign(_options, options, { focus: true });
|
||||
_testDefinitions = testDefinitions;
|
||||
}
|
||||
|
||||
this.describe(name, _options, _testDefinitions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a new test.
|
||||
* @param {String} description - Brief description of what the test expects
|
||||
* @param {TestOptions} options - Options of whether test should be focused or pending
|
||||
* @param {Function} testFunction - Body of the test containing setup and assertions
|
||||
*/
|
||||
it(description, options, testFunction = undefined) {
|
||||
this._testSuiteDefinition.addTest(description, options, testFunction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a new pending test. Pending tests are not run when the test suite is
|
||||
* executed. They also appear greyed out.
|
||||
* @param {String} description - Brief description of what the test expects
|
||||
* @param {ContextOptions} [options={}] - Options of whether test should be focused or pending
|
||||
* @param {Function} testFunction - Body of the test containing setup and assertions
|
||||
*/
|
||||
xit(description, options, testFunction = undefined) {
|
||||
let _options = {};
|
||||
let _testFunction;
|
||||
|
||||
if (typeof options === 'function') {
|
||||
_options = { pending: true };
|
||||
_testFunction = options;
|
||||
} else {
|
||||
Object.assign(_options, options, { pending: true });
|
||||
_testFunction = testFunction;
|
||||
}
|
||||
|
||||
this.it(description, _options, _testFunction);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Defines a new focused test. Focused tests are the only
|
||||
* ones that appear and are run when the test suite is executed.
|
||||
* @param {String} description - Brief description of what the test expects
|
||||
* @param {ContextOptions} [options={}] - Options of whether test should be focused or pending
|
||||
* @param {Function} testFunction - Body of the test containing setup and assertions
|
||||
*/
|
||||
fit(description, options, testFunction = undefined) {
|
||||
let _options = {};
|
||||
let _testFunction;
|
||||
|
||||
if (typeof options === 'function') {
|
||||
_options = { focus: true };
|
||||
_testFunction = options;
|
||||
} else {
|
||||
Object.assign(_options, options, { focus: true });
|
||||
_testFunction = testFunction;
|
||||
}
|
||||
|
||||
this.it(description, _options, _testFunction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries evaluating a function and calls a reject callback if it throws an error
|
||||
* @param {Function} callback - Function to evaluate
|
||||
* @param {Function} reject - Function to call if callback throws an error
|
||||
* @returns {function(...[*])} a function that will catch any errors thrown by callback,
|
||||
* passing them to reject instead.
|
||||
*/
|
||||
tryCatch(callback, reject) {
|
||||
return (...args) => {
|
||||
try {
|
||||
callback(...args);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a test DSL error to the console.
|
||||
* @param {String} error - Message to included in message logged to the console
|
||||
*/
|
||||
function testDSLError(error) {
|
||||
console.error(`ReactNativeFirebaseTests.TestDSLError: ${error}`);
|
||||
console.error('This test was ignored.');
|
||||
}
|
||||
export default TestDSL;
|
||||
386
tests/lib/TestRun.js
Normal file
386
tests/lib/TestRun.js
Normal file
@@ -0,0 +1,386 @@
|
||||
import Promise from 'bluebird';
|
||||
import RunStatus from './RunStatus';
|
||||
|
||||
const EVENTS = {
|
||||
TEST_SUITE_STATUS: 'TEST_SUITE_STATUS',
|
||||
TEST_STATUS: 'TEST_STATUS',
|
||||
};
|
||||
|
||||
/**
|
||||
* Class that encapsulates synchronously running a suite's tests.
|
||||
*/
|
||||
class TestRun {
|
||||
/**
|
||||
* The number of tests that have been executed so far
|
||||
* @type {number}
|
||||
*/
|
||||
completedTests = 0;
|
||||
|
||||
/**
|
||||
* Creates a new TestRun
|
||||
* @param {TestSuite} testSuite - Test suite that tests belong to
|
||||
* @param {Test[]} tests - List of test to run
|
||||
* @param {TestSuiteDefinition} testDefinitions - Definition of tests and contexts
|
||||
*/
|
||||
constructor(testSuite, tests, testDefinitions) {
|
||||
this.testSuite = testSuite;
|
||||
|
||||
this.tests = tests;
|
||||
|
||||
this.rootContextId = testDefinitions.rootTestContextId;
|
||||
|
||||
this.testContexts = tests.reduce((memo, test) => {
|
||||
const testContextId = test.testContextId;
|
||||
|
||||
this._recursivelyAddContextsTo(memo, testContextId, testDefinitions.testContexts);
|
||||
|
||||
memo[testContextId].tests.unshift(test);
|
||||
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
this.listeners = {
|
||||
[EVENTS.TEST_STATUS]: [],
|
||||
[EVENTS.TEST_SUITE_STATUS]: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a listener for a change event
|
||||
* @param {String} action - one of the actions in EVENTS
|
||||
* @param {Function} callback - Callback that accepts event object
|
||||
*/
|
||||
onChange(action, callback) {
|
||||
this.listeners[action].push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks up a context tree, copying test contexts from a source object to a target one.
|
||||
* Used for ensuring all of a test's parent contexts are added to the target object.
|
||||
* @param {Object<Number,TestContext>} target - Object to put test contexts in
|
||||
* @param {Number} id - Id of current context to add to target
|
||||
* @param {Object<Number,TestContext>} source - Object to get complete list of test contexts
|
||||
* from.
|
||||
* @param {Number} childContextId - id of child of current context
|
||||
* @private
|
||||
*/
|
||||
_recursivelyAddContextsTo(target, id, source, childContextId = null) {
|
||||
const testContext = source[id];
|
||||
|
||||
if (!target[id]) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
target[id] = {
|
||||
...testContext,
|
||||
tests: [],
|
||||
childContextIds: {},
|
||||
};
|
||||
}
|
||||
|
||||
if (childContextId) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
target[id].childContextIds[childContextId] = true;
|
||||
}
|
||||
|
||||
const parentContextId = testContext.parentContextId;
|
||||
|
||||
if (parentContextId) {
|
||||
this._recursivelyAddContextsTo(target, parentContextId, source, id);
|
||||
}
|
||||
}
|
||||
|
||||
_updateStatus(action, values) {
|
||||
const listeners = this.listeners[action];
|
||||
|
||||
listeners.forEach(listener => listener(values));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the tests TestRun was initialised with.
|
||||
* @returns {Promise.<void>} Resolves once all the tests in the test suite have
|
||||
* completed running.
|
||||
*/
|
||||
async execute() {
|
||||
const store = this.testSuite.reduxStore;
|
||||
|
||||
if (!store) {
|
||||
testRuntimeError(`Failed to run ${this.testSuite.name} tests as no Redux store has been provided`);
|
||||
}
|
||||
|
||||
this._updateStatus(EVENTS.TEST_SUITE_STATUS, {
|
||||
suiteId: this.testSuite.id,
|
||||
status: RunStatus.RUNNING,
|
||||
|
||||
progress: 0,
|
||||
time: 0,
|
||||
});
|
||||
|
||||
// Start timing
|
||||
|
||||
this.runStartTime = Date.now();
|
||||
|
||||
const rootContext = this.testContexts[this.rootContextId];
|
||||
|
||||
if (rootContext) {
|
||||
await this._runTestsInContext(rootContext);
|
||||
|
||||
const errors = this.tests.filter(test => test.status === RunStatus.ERR);
|
||||
|
||||
if (errors.length) {
|
||||
this._updateStatus(EVENTS.TEST_SUITE_STATUS, {
|
||||
suiteId: this.testSuite.id,
|
||||
status: RunStatus.ERR,
|
||||
progress: 100,
|
||||
|
||||
time: Date.now() - this.runStartTime,
|
||||
message: `${errors.length} test${errors.length > 1 ? 's' : ''} has error(s).`,
|
||||
});
|
||||
} else {
|
||||
this._updateStatus(EVENTS.TEST_SUITE_STATUS, ({
|
||||
suiteId: this.testSuite.id,
|
||||
status: RunStatus.OK,
|
||||
progress: 100,
|
||||
|
||||
time: Date.now() - this.runStartTime,
|
||||
message: '',
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively enter a test context and run its before, beforeEach hooks where
|
||||
* appropriate; execute the test and then run afterEach and after hooks where
|
||||
* appropriate.
|
||||
* @param {TestContext} testContext - context to run hooks for
|
||||
* @param {Function[][]} beforeEachHooks - stack of beforeEach hooks defined
|
||||
* in parent contexts that should be run beforeEach test in child contexts
|
||||
* @param {Function[][]} afterEachHooks - stack of afterEach hooks defined
|
||||
* in parent contexts that should be run afterEach test in child contexts
|
||||
* @returns {Promise.<void>} Resolves once all tests and their hooks have run
|
||||
* @private
|
||||
*/
|
||||
async _runTestsInContext(testContext, beforeEachHooks = [], afterEachHooks = []) {
|
||||
const beforeHookRan = await this._runContextHooks(testContext, 'before');
|
||||
|
||||
if (beforeHookRan) {
|
||||
beforeEachHooks.push(testContext.beforeEachHooks || []);
|
||||
afterEachHooks.unshift(testContext.afterEachHooks || []);
|
||||
|
||||
await this._runTests(testContext, testContext.tests, flatten(beforeEachHooks), flatten(afterEachHooks));
|
||||
|
||||
await Promise.each(Object.keys(testContext.childContextIds), (childContextId) => {
|
||||
const childContext = this.testContexts[childContextId];
|
||||
return this._runTestsInContext(childContext, beforeEachHooks, afterEachHooks);
|
||||
});
|
||||
|
||||
beforeEachHooks.pop();
|
||||
afterEachHooks.shift();
|
||||
|
||||
await this._runContextHooks(testContext, 'after');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously run hooks in context's (before|after) hooks, starting from the first
|
||||
* hook
|
||||
* @param {TestContext} testContext - context containing hooks
|
||||
* @param {('before'|'after')} hookName - name of hooks to run callbacks for
|
||||
* @returns {Promise.<*>} Resolves when last hook in list has been executed
|
||||
* @private
|
||||
*/
|
||||
async _runContextHooks(testContext, hookName) {
|
||||
const hooks = testContext[`${hookName}Hooks`] || [];
|
||||
|
||||
return this._runHookChain(null, Date.now(), testContext, hookName, hooks);
|
||||
}
|
||||
|
||||
_runHookChain(test, testStart, testContext, hookName, hooks) {
|
||||
return Promise.each(hooks, async (hook) => {
|
||||
const error = await this._safelyRunFunction(hook.callback, hook.timeout, `${hookName} hook`);
|
||||
|
||||
if (error) {
|
||||
const errorPrefix = `Error occurred in "${testContext.name}" ${hookName} Hook: `;
|
||||
|
||||
if (test) {
|
||||
this._reportTestError(test, error, Date.now() - testStart, errorPrefix);
|
||||
} else {
|
||||
this._reportAllTestsAsFailed(testContext, error, testStart, errorPrefix);
|
||||
}
|
||||
|
||||
throw new Error();
|
||||
}
|
||||
}).then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
_reportAllTestsAsFailed(testContext, error, testStart, errorPrefix) {
|
||||
testContext.tests.forEach((test) => {
|
||||
this._reportTestError(test, error, Date.now() - testStart, errorPrefix);
|
||||
});
|
||||
|
||||
testContext.childContextIds.forEach((contextId) => {
|
||||
this._reportAllTestsAsFailed(this.testContext[contextId], error, testStart, errorPrefix);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously run a list of tests
|
||||
* @param {TestContext} testContext - Test context to run beforeEach and AfterEach hooks
|
||||
* for
|
||||
* @param {Test[]} tests - List of tests to run
|
||||
* @param {Function[]} beforeEachHooks - list of functions to run before each test
|
||||
* @param {Function[]} afterEachHooks - list of functions to run after each test
|
||||
* @returns {Promise.<void>} - Resolves once all tests and their afterEach hooks have
|
||||
* been run
|
||||
* @private
|
||||
*/
|
||||
async _runTests(testContext, tests, beforeEachHooks, afterEachHooks) {
|
||||
return Promise.each(tests, async (test) => {
|
||||
this._updateStatus(EVENTS.TEST_STATUS, {
|
||||
testId: test.id,
|
||||
status: RunStatus.RUNNING,
|
||||
time: 0,
|
||||
message: '',
|
||||
});
|
||||
|
||||
const testStart = Date.now();
|
||||
|
||||
const beforeEachRan = await this._runHookChain(test, testStart, testContext, 'beforeEach', beforeEachHooks);
|
||||
|
||||
if (beforeEachRan) {
|
||||
const error = await this._safelyRunFunction(test.func.bind(null, [test, this.testSuite.reduxStore.getState()]), test.timeout, 'Test');
|
||||
|
||||
// Update test status
|
||||
|
||||
if (error) {
|
||||
this._reportTestError(test, error, Date.now() - testStart);
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
test.status = RunStatus.OK;
|
||||
|
||||
this._updateStatus(EVENTS.TEST_STATUS, {
|
||||
testId: test.id,
|
||||
status: RunStatus.OK,
|
||||
time: Date.now() - testStart,
|
||||
message: '',
|
||||
});
|
||||
}
|
||||
|
||||
// Update suite progress
|
||||
|
||||
this.completedTests += 1;
|
||||
|
||||
this._updateStatus(EVENTS.TEST_SUITE_STATUS, {
|
||||
suiteId: this.testSuite.id,
|
||||
status: RunStatus.RUNNING,
|
||||
progress: (this.completedTests / this.tests.length) * 100,
|
||||
time: Date.now() - this.runStartTime,
|
||||
message: '',
|
||||
});
|
||||
|
||||
await this._runHookChain(test, testStart, testContext, 'afterEach', afterEachHooks);
|
||||
}
|
||||
})
|
||||
|
||||
.catch((error) => {
|
||||
this._updateStatus(EVENTS.TEST_SUITE_STATUS, {
|
||||
suiteId: this.testSuite.id,
|
||||
status: RunStatus.ERR,
|
||||
time: Date.now() - this.runStartTime,
|
||||
message: `Test suite failed: ${error.message}`,
|
||||
stackTrace: error.stack,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_reportTestError(test, error, time, errorPrefix = '') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
test.status = RunStatus.ERR;
|
||||
|
||||
this._updateStatus(EVENTS.TEST_STATUS, {
|
||||
testId: test.id,
|
||||
status: RunStatus.ERR,
|
||||
time,
|
||||
message: `${errorPrefix}${error.message ? `${error.name}: ${error.message}` : error}`,
|
||||
stackTrace: error.stack,
|
||||
});
|
||||
}
|
||||
|
||||
async _safelyRunFunction(func, timeOutDuration, description) {
|
||||
const syncResultOrPromise = tryCatcher(func);
|
||||
|
||||
if (syncResultOrPromise.error) {
|
||||
// Synchronous Error
|
||||
return syncResultOrPromise.error;
|
||||
}
|
||||
|
||||
// Asynchronous Error
|
||||
return promiseToCallBack(syncResultOrPromise.value, timeOutDuration, description);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Try catch to object
|
||||
* @returns {{}}
|
||||
* @private
|
||||
*/
|
||||
|
||||
function tryCatcher(func) {
|
||||
const result = {};
|
||||
|
||||
try {
|
||||
result.value = func();
|
||||
} catch (e) {
|
||||
result.error = e;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a promise callback-able to trap errors
|
||||
* @param promise
|
||||
* @private
|
||||
*/
|
||||
|
||||
function promiseToCallBack(promise, timeoutDuration, description) {
|
||||
let returnValue = null;
|
||||
|
||||
try {
|
||||
returnValue = Promise.resolve(promise)
|
||||
.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);
|
||||
});
|
||||
} catch (error) {
|
||||
returnValue = Promise.resolve(error);
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten a two dimensional array to a single dimensional array
|
||||
* @param {*[]} list - two dimensional array
|
||||
* @returns {*[]} One-dimensional array
|
||||
*/
|
||||
function flatten(list) {
|
||||
return list.reduce((memo, contextHooks) => {
|
||||
return memo.concat(contextHooks);
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a runtime error to the console
|
||||
* @param {String} error - Message to log to the console
|
||||
*/
|
||||
function testRuntimeError(error) {
|
||||
console.error(`ReactNativeFirebaseTests.TestRuntimeError: ${error}`);
|
||||
}
|
||||
|
||||
export default TestRun;
|
||||
148
tests/lib/TestSuite.js
Normal file
148
tests/lib/TestSuite.js
Normal file
@@ -0,0 +1,148 @@
|
||||
import 'should';
|
||||
import 'should-sinon';
|
||||
|
||||
import TestSuiteDefinition from './TestSuiteDefinition';
|
||||
import TestRun from './TestRun';
|
||||
|
||||
/**
|
||||
* Incrementing counter to assign each test suite a globally unique id. Should be
|
||||
* accessed only through assignTestSuiteId
|
||||
* @type {number} Counter that maintains globally unique id and increments each time
|
||||
* a new id is assigned
|
||||
*/
|
||||
let testSuiteCounter = 0;
|
||||
|
||||
/**
|
||||
* Increment the testSuiteCounter and return the new value. Used
|
||||
* for assigning a new globally unique id to a test suite.
|
||||
* @returns {number} globally unique id assigned to each test suite
|
||||
*/
|
||||
function assignTestSuiteId() {
|
||||
testSuiteCounter += 1;
|
||||
return testSuiteCounter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class that provides imperative interface for constructing and running a test suite.
|
||||
* Used for instantiating a new test suite, adding tests to it and then running all or
|
||||
* a subset of those tests.
|
||||
*
|
||||
* @example
|
||||
* // Creates a new test suite
|
||||
* const testSuit = new TestSuite('Feature Group A', 'Feature a, b and c');
|
||||
*/
|
||||
class TestSuite {
|
||||
/**
|
||||
* Creates a new test suite.
|
||||
* @param {String} name - The name of the test suite
|
||||
* @param {String} description - A short description of the test suite
|
||||
* @param {Object} firebase - Object containing native and web firebase instances
|
||||
* @param {Object} firebase.native - Native firebase instance
|
||||
* @para {Object} firebase.web - Web firebase instance
|
||||
*/
|
||||
constructor(name, description, firebase) {
|
||||
this.id = assignTestSuiteId();
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
|
||||
this.reduxStore = null;
|
||||
this.testDefinitions = new TestSuiteDefinition(this, firebase);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Function} TestDefinitionFunction
|
||||
* @param {TestDSL} testDSL - class instance that defines the testing DSL that can
|
||||
* be used in defining tests
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds tests defined in a function to the test suite
|
||||
* @param {TestDefinitionFunction} testDefinition - A function that defines one or
|
||||
* more test suites using the test DSL.
|
||||
* @example
|
||||
* // Adding tests
|
||||
* const testDefinition = function({ describe, it }) {
|
||||
* describe('Some context', () => {
|
||||
* it('then does something', () => {
|
||||
* // Test assertions here
|
||||
* })
|
||||
* })
|
||||
* testSuite.addTests(testDefinition);
|
||||
*/
|
||||
addTests(testDefinition) {
|
||||
testDefinition(this.testDefinitions.DSL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} ReduxStore
|
||||
* @property {Function} getState - Returns the current state of the store
|
||||
* @property {Function} dispatch - Dispatches a new action to update to store
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sets the redux store assigned to the test suite
|
||||
* @param {ReduxStore} store - The redux store to add to the test suite
|
||||
* @param {Function} testSuiteAction - Function that accepts an object of
|
||||
* event values and returns another that is suitable to dispatch to the
|
||||
* redux store. Responsible for handling events when the test suite's status
|
||||
* has changed.
|
||||
* @param {Function} testAction - Function that accepts an object of
|
||||
* event values and returns another that is suitable to dispatch to the
|
||||
* redux store. Responsible for handling events when a test's status
|
||||
* has changed.
|
||||
*/
|
||||
setStore(store, testSuiteAction, testAction) {
|
||||
this.reduxStore = store;
|
||||
this.suiteChangHandler = testSuiteAction;
|
||||
this.testChangHandler = testAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all the tests matching an array of ids. If the array is not provided, run all
|
||||
* test in the test suite.
|
||||
* @param {number[]=} testIds - array of ids for tests to run
|
||||
* @throws {RangeError} testIds must correspond with tests in the test suite
|
||||
* @example
|
||||
* // Running all tests in the test suite
|
||||
* testSuite.run();
|
||||
* @example
|
||||
* // Run only tests with id 1 and 2
|
||||
* testSuite.run([1, 2]);
|
||||
*/
|
||||
async run(testIds = undefined) {
|
||||
const testsToRun = (() => {
|
||||
if (testIds) {
|
||||
return testIds.map((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}`);
|
||||
}
|
||||
|
||||
return test;
|
||||
});
|
||||
}
|
||||
|
||||
return Object.values(this.testDefinitions.tests);
|
||||
})();
|
||||
|
||||
const testRun = new TestRun(this, testsToRun.reverse(), this.testDefinitions);
|
||||
|
||||
testRun.onChange('TEST_SUITE_STATUS', (values) => {
|
||||
if (this.suiteChangHandler) {
|
||||
this.suiteChangHandler(values);
|
||||
}
|
||||
});
|
||||
|
||||
testRun.onChange('TEST_STATUS', (values) => {
|
||||
if (this.testChangHandler) {
|
||||
this.testChangHandler(values);
|
||||
}
|
||||
});
|
||||
|
||||
await testRun.execute();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default TestSuite;
|
||||
360
tests/lib/TestSuiteDefinition.js
Normal file
360
tests/lib/TestSuiteDefinition.js
Normal file
@@ -0,0 +1,360 @@
|
||||
import TestDSL from './TestDSL';
|
||||
|
||||
/**
|
||||
* Incrementing counter to assign each test context a globally unique id.
|
||||
* @type {number} Counter that maintains globally unique id and increments each time
|
||||
* a new id is assigned
|
||||
*/
|
||||
let testContextCounter = 0;
|
||||
|
||||
/**
|
||||
* Incrementing counter to assign each test a globally unique id.
|
||||
* @type {number} Counter that maintains globally unique id and increments each time
|
||||
* a new id is assigned
|
||||
*/
|
||||
let testCounter = 0;
|
||||
|
||||
/**
|
||||
* Increment the testCounter and return the new value. Used
|
||||
* for assigning a new globally unique id to a test.
|
||||
* @returns {number} globally unique id assigned to each test
|
||||
*/
|
||||
function assignTestId() {
|
||||
testCounter += 1;
|
||||
return testCounter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the testContextCounter and return the new value. Used
|
||||
* for assigning a new globally unique id to a test context.
|
||||
* @returns {number} globally unique id assigned to each test context
|
||||
*/
|
||||
function assignContextId() {
|
||||
testContextCounter += 1;
|
||||
return testContextCounter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum for operators that can be used when combining test properties with their
|
||||
* parents at definition time.
|
||||
* @readonly
|
||||
* @enum {String} ContextOperator
|
||||
*/
|
||||
const CONTEXT_OPERATORS = {
|
||||
/** Perform OR of test value with context chain values **/
|
||||
OR: 'OR',
|
||||
};
|
||||
|
||||
/**
|
||||
* Class that provides imperative interface for defining tests. When defining tests
|
||||
* the declarative interface for this class, {@link TestDSL} should be use instead.
|
||||
*/
|
||||
class TestSuiteDefinition {
|
||||
/**
|
||||
* Creates a new TestSuiteDefinition
|
||||
* @param {TestSuite} testSuite - The {@link TestSuite} instance for which to
|
||||
* define tests for.
|
||||
* @param {Object} firebase - Object containing native and web firebase instances
|
||||
* @param {Object} firebase.native - Native firebase instance
|
||||
* @para {Object} firebase.web - Web firebase instance
|
||||
*/
|
||||
constructor(testSuite, firebase) {
|
||||
this.testSuite = testSuite;
|
||||
|
||||
this.tests = {};
|
||||
this.pendingTestIds = {};
|
||||
this.focusedTestIds = {};
|
||||
|
||||
this.testContexts = {};
|
||||
|
||||
this.rootTestContextId = assignContextId();
|
||||
this.rootTestContext = this._initialiseContext(this.rootTestContextId, {
|
||||
name: '',
|
||||
focus: false,
|
||||
pending: false,
|
||||
parentContextId: null,
|
||||
});
|
||||
|
||||
this.currentTestContext = this.rootTestContext;
|
||||
|
||||
this._testDSL = new TestDSL(this, firebase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the instance of {@link TestDSL} used for declaratively defining tests
|
||||
* @returns {TestDSL} The TestDSL used for defining tests
|
||||
*/
|
||||
get DSL() {
|
||||
return this._testDSL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a function as a before hook to the current test context
|
||||
* @param {Function} callback - Function to add as before hook to current test context
|
||||
*/
|
||||
addBeforeHook(callback, options = {}) {
|
||||
this._addHook('before', callback, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a function as a before each hook to the current test context
|
||||
* @param {Function} callback - Function to add as before each hook to current test
|
||||
* context
|
||||
*/
|
||||
addBeforeEachHook(callback, options = {}) {
|
||||
this._addHook('beforeEach', callback, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a function as a after each hook to the current test context
|
||||
* @param {Function} callback - Function to add as after each hook to current test context
|
||||
*/
|
||||
addAfterEachHook(callback, options = {}) {
|
||||
this._addHook('afterEach', callback, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a function as a after hook to the current test context
|
||||
* @param {Function} callback - Function to add as after hook to current test context
|
||||
*/
|
||||
addAfterHook(callback, options = {}) {
|
||||
this._addHook('after', callback, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a function to the list of hooks matching hookName, for the current test context
|
||||
* @param {('before'|'beforeEach'|'afterEach'|'after')} hookName - The name of the hook to add the function to
|
||||
* @param {Function} callback - Function to add as a hook
|
||||
* @param {Object=} options - Hook configuration options
|
||||
* @private
|
||||
*/
|
||||
_addHook(hookName, callback, options = {}) {
|
||||
const hookAttribute = `${hookName}Hooks`;
|
||||
|
||||
if (callback && typeof callback === 'function') {
|
||||
this.currentTestContext[hookAttribute] = this.currentTestContext[hookAttribute] || [];
|
||||
this.currentTestContext[hookAttribute].push({
|
||||
callback,
|
||||
timeout: options.timeout || 5000,
|
||||
});
|
||||
} else {
|
||||
testDefinitionError(`non-function value ${callback} passed to ${hookName} for '${this.currentTestContext.name}'`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} ContextOptions
|
||||
* @property {Boolean} [focused=undefined] - whether context is focused or not.
|
||||
* @property {Boolean} [pending=undefined] - whether context is pending or not.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TestOptions
|
||||
* @extends ContextOptions
|
||||
* @property {Number} [timeout=5000] - Number of milliseconds before test times out
|
||||
*/
|
||||
|
||||
/**
|
||||
* Push a test context onto the context stack, making it the new current test context
|
||||
* @param {String} name - The name of the new context
|
||||
* @param {ContextOptions} options - Options for new context
|
||||
*/
|
||||
pushTestContext(name, options = {}) {
|
||||
const testContextId = assignContextId();
|
||||
const parentContext = this.currentTestContext;
|
||||
this.currentTestContext = this._initialiseContext(testContextId, Object.assign({ name, parentContextId: parentContext.id }, options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop test context off the context stack, making the previous context the new
|
||||
* current context.
|
||||
*/
|
||||
popTestContext() {
|
||||
const parentContextId = this.currentTestContext.parentContextId;
|
||||
this.currentTestContext = this.testContexts[parentContextId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a test to the current test context
|
||||
* @param {String} description - The new test's description
|
||||
* @param {ContextOptions} options - The options for the new test
|
||||
* @param {Function} testFunction - The function that comprises the test's body
|
||||
*/
|
||||
addTest(description, options, testFunction = undefined) {
|
||||
let _testFunction;
|
||||
let _options;
|
||||
|
||||
if (testFunction) {
|
||||
_testFunction = testFunction;
|
||||
_options = options;
|
||||
} else {
|
||||
_testFunction = options;
|
||||
_options = {};
|
||||
}
|
||||
|
||||
if (_testFunction && typeof _testFunction === 'function') {
|
||||
// Create test
|
||||
const testId = assignTestId();
|
||||
|
||||
this._createTest(testId, {
|
||||
testContextId: this.currentTestContext.id,
|
||||
testSuiteId: this.testSuite.id,
|
||||
description: this._testDescriptionContextPrefix(this.currentTestContext) + description,
|
||||
func: _testFunction,
|
||||
timeout: _options.timeout || 5000,
|
||||
});
|
||||
|
||||
// Add tests to context
|
||||
this.currentTestContext.testIds.push(testId);
|
||||
|
||||
if (_options.focus || this.currentTestContext.focus) {
|
||||
this.focusedTestIds[testId] = true;
|
||||
}
|
||||
|
||||
if (_options.pending || this.currentTestContext.pending) {
|
||||
this.pendingTestIds[testId] = true;
|
||||
}
|
||||
} else {
|
||||
testDefinitionError(`Invalid test function for "${description}".`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the prefix to prepend to a test to fully describe it. Any context that is
|
||||
* nested 2 or more deep, i.e. non the root context nor a child of the root context,
|
||||
* has its name recursively prepended to all tests in that context or contexts it
|
||||
* contains. This allows tests to be easily displayed in a LinkedList during viewing
|
||||
* and reporting the test suite.
|
||||
* @param {Object} contextProperties - Properties of current context
|
||||
* @param {Number} contextProperties.id - Id of context
|
||||
* @param {String} contextProperties.name - Name of context
|
||||
* @param {Number} contextProperties.parentContextId - Id of context's parent
|
||||
* @param {String} [suffix=''] - Accumulation of context prefixes so far. Starts empty
|
||||
* and collects context prefixes as it recursively calls itself to iterate up the
|
||||
* context tree.
|
||||
* @returns {String} Prefix to be prepended to current accumulative string of context
|
||||
* names
|
||||
* @private
|
||||
*/
|
||||
_testDescriptionContextPrefix({ id, name, parentContextId }, suffix = '') {
|
||||
if (id === this.rootTestContextId || parentContextId === this.rootTestContextId) {
|
||||
return suffix;
|
||||
}
|
||||
|
||||
return this._testDescriptionContextPrefix(this.testContexts[parentContextId], `${name} ${suffix}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} TestContext
|
||||
* @property {Number} id - Globally unique id
|
||||
* @property {String} name - Short description of context
|
||||
* @property {Boolean} [focus=false] - Whether context is focused
|
||||
* @property {Boolean} [pending=false] - Whether context is pending
|
||||
* @property {Number} [parentContextId=undefined] - Id of context that contains the current one
|
||||
* @property {Number[]} testIds - List of ids of tests to be run in current context
|
||||
* @property {Number} testSuiteId - Id of test suite test context is apart of
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a context from options provided
|
||||
* @param {Number} testContextId - Id to assign to new context once it's created
|
||||
* @param {Object} options - options to use to create the context
|
||||
* @param {String} options.name - Name of context to create
|
||||
* @param {Boolean} options.focus - Whether context is focused or not
|
||||
* @param {Boolean} options.pending - Whether context is pending or not
|
||||
* @param {Number} [options.parentContextId=undefined] - Id of context's parent
|
||||
* @returns {TestContext} New test context once it has been initialised
|
||||
* @private
|
||||
*/
|
||||
_initialiseContext(testContextId, { name, focus, pending, parentContextId }) {
|
||||
const existingContext = this.testContexts[testContextId];
|
||||
|
||||
if (existingContext) {
|
||||
return existingContext;
|
||||
}
|
||||
|
||||
const parentContext = this.testContexts[parentContextId];
|
||||
|
||||
const newTestContext = {
|
||||
id: testContextId,
|
||||
name,
|
||||
focus: this._incorporateParentValue(parentContext, 'focus', focus, CONTEXT_OPERATORS.OR),
|
||||
pending: this._incorporateParentValue(parentContext, 'pending', pending, CONTEXT_OPERATORS.OR),
|
||||
parentContextId,
|
||||
testIds: [],
|
||||
testSuiteId: this.testSuite.id,
|
||||
};
|
||||
|
||||
this.testContexts[testContextId] = newTestContext;
|
||||
|
||||
return newTestContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively use an operator to consolidate a test's value with that of its test
|
||||
* context chain.
|
||||
* @param {TestContext} parentContext - Parent context to examine for its value
|
||||
* @param {String} attributeName - name of the attribute to use from parent
|
||||
* @param {*} value - Value of current context or test to use as one operand with
|
||||
* the parent context's value
|
||||
* @param {('OR')} operator - Operator to use to consolidate current value and
|
||||
* parent context's value
|
||||
* @returns {*} Consolidated value, encorporating context parents' values
|
||||
* @private
|
||||
*/
|
||||
_incorporateParentValue(parentContext, attributeName, value, operator) {
|
||||
if (!parentContext) {
|
||||
return value;
|
||||
}
|
||||
|
||||
switch (operator) {
|
||||
case CONTEXT_OPERATORS.OR:
|
||||
return parentContext[attributeName] || value;
|
||||
default:
|
||||
throw new Error(`Unknown context operator ${operator}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new test from the options provided and add it to the suite
|
||||
* @param {Number} testId - Unique id to give to the test
|
||||
* @param {Object} testAttributes - attributes to create the test with
|
||||
* @param {Number} testAttributes.testContextId - Id of context test belongs to
|
||||
* @param {String} testAttributes.description - Short description of the test
|
||||
* @param {Function} testAttributes.func - Function that comprises the body of the test
|
||||
* @param {Number} testAttributes.testSuiteId - Id of test suite test belongs to
|
||||
* @param {Number} testAttributes.timeout - Number of milliseconds before test times out
|
||||
* @returns {Test} New test matching provided options
|
||||
* @private
|
||||
*/
|
||||
_createTest(testId, { testContextId, description, func, testSuiteId, timeout }) {
|
||||
const newTest = {
|
||||
id: testId,
|
||||
testContextId,
|
||||
description,
|
||||
func,
|
||||
testSuiteId,
|
||||
status: null,
|
||||
message: null,
|
||||
time: 0,
|
||||
timeout,
|
||||
};
|
||||
|
||||
this.tests[testId] = newTest;
|
||||
|
||||
return newTest;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Log test definition error to the console with a message indicating the test
|
||||
* definition was skipped.
|
||||
* @param {String} error - Error message to include in message logged to the console
|
||||
*/
|
||||
function testDefinitionError(error) {
|
||||
console.error(`ReactNativeFirebaseTests.TestDefinitionError: ${error}`);
|
||||
console.error('This test was ignored.');
|
||||
}
|
||||
|
||||
export default TestSuiteDefinition;
|
||||
Reference in New Issue
Block a user