2017-04-23 19:22:19 +00:00
|
|
|
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 = {
|
2018-01-25 18:25:39 +00:00
|
|
|
/** Perform OR of test value with context chain values * */
|
2017-04-23 19:22:19 +00:00
|
|
|
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') {
|
2018-01-25 18:25:39 +00:00
|
|
|
this.currentTestContext[hookAttribute] =
|
|
|
|
this.currentTestContext[hookAttribute] || [];
|
2017-04-23 19:22:19 +00:00
|
|
|
this.currentTestContext[hookAttribute].push({
|
|
|
|
callback,
|
2017-08-16 20:40:56 +00:00
|
|
|
timeout: options.timeout || 15000,
|
2017-04-23 19:22:19 +00:00
|
|
|
});
|
|
|
|
} else {
|
2018-01-25 18:25:39 +00:00
|
|
|
testDefinitionError(
|
|
|
|
`non-function value ${callback} passed to ${hookName} for '${
|
|
|
|
this.currentTestContext.name
|
|
|
|
}'`
|
|
|
|
);
|
2017-04-23 19:22:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @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;
|
2018-01-25 18:25:39 +00:00
|
|
|
this.currentTestContext = this._initialiseContext(
|
|
|
|
testContextId,
|
|
|
|
Object.assign({ name, parentContextId: parentContext.id }, options)
|
|
|
|
);
|
2017-04-23 19:22:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pop test context off the context stack, making the previous context the new
|
|
|
|
* current context.
|
|
|
|
*/
|
|
|
|
popTestContext() {
|
2018-01-25 18:25:39 +00:00
|
|
|
const { parentContextId } = this.currentTestContext;
|
2017-04-23 19:22:19 +00:00
|
|
|
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,
|
2018-01-25 18:25:39 +00:00
|
|
|
description:
|
|
|
|
this._testDescriptionContextPrefix(this.currentTestContext) +
|
|
|
|
description,
|
2017-04-23 19:22:19 +00:00
|
|
|
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 = '') {
|
2018-01-25 18:25:39 +00:00
|
|
|
if (
|
|
|
|
id === this.rootTestContextId ||
|
|
|
|
parentContextId === this.rootTestContextId
|
|
|
|
) {
|
2017-04-23 19:22:19 +00:00
|
|
|
return suffix;
|
|
|
|
}
|
|
|
|
|
2018-01-25 18:25:39 +00:00
|
|
|
return this._testDescriptionContextPrefix(
|
|
|
|
this.testContexts[parentContextId],
|
|
|
|
`${name} ${suffix}`
|
|
|
|
);
|
2017-04-23 19:22:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @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,
|
2018-01-25 18:25:39 +00:00
|
|
|
focus: this._incorporateParentValue(
|
|
|
|
parentContext,
|
|
|
|
'focus',
|
|
|
|
focus,
|
|
|
|
CONTEXT_OPERATORS.OR
|
|
|
|
),
|
|
|
|
pending: this._incorporateParentValue(
|
|
|
|
parentContext,
|
|
|
|
'pending',
|
|
|
|
pending,
|
|
|
|
CONTEXT_OPERATORS.OR
|
|
|
|
),
|
2017-04-23 19:22:19 +00:00
|
|
|
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
|
|
|
|
*/
|
2018-01-25 18:25:39 +00:00
|
|
|
_createTest(
|
|
|
|
testId,
|
|
|
|
{ testContextId, description, func, testSuiteId, timeout }
|
|
|
|
) {
|
2017-04-23 19:22:19 +00:00
|
|
|
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;
|