[tests] Move test suite into same repo

This commit is contained in:
Aleck Greenham
2017-04-23 20:22:19 +01:00
parent fac0efa9d0
commit 9308994db2
148 changed files with 11251 additions and 1 deletions

View File

@@ -0,0 +1,17 @@
export const APP_SET_NETWORK_STATE: string = 'APP_SET_NETWORK_STATE';
export const APP_SET_APP_STATE: string = 'APP_SET_APP_STATE';
export function setNetworkState(isConnected: boolean): Object {
return {
type: APP_SET_NETWORK_STATE,
isConnected,
};
}
export function setAppState(appState: 'active' | 'background' | 'inactive'): Object {
return {
type: APP_SET_APP_STATE,
appState,
};
}

View File

@@ -0,0 +1,8 @@
export const FCM_SET_TOKEN: string = 'FCM_SET_TOKEN';
export function setToken(token: string): Object {
return {
type: FCM_SET_TOKEN,
token,
};
}

View File

@@ -0,0 +1,26 @@
export const TEST_SET_SUITE_STATUS: string = 'TEST_SET_SUITE_STATUS';
export const TEST_SET_STATUS: string = 'TEST_SET_STATUS';
export function setSuiteStatus({ suiteId, status, time, message, progress }) {
return {
type: TEST_SET_SUITE_STATUS,
suiteId,
status,
message,
time,
progress,
};
}
export function setTestStatus({ testId, status, time = 0, message = null }) {
return {
type: TEST_SET_STATUS,
testId,
status,
message,
time,
};
}

View File

@@ -0,0 +1,51 @@
import React from 'react';
import { StyleSheet, View, Text } from 'react-native';
function Banner({ type, children, style, textStyle }) {
return (
<View style={[styles.banner, styles[type || 'default'], style]}>
<Text
numberOfLines={1}
style={[styles.bannerText, textStyle]}
>
{children}
</Text>
</View>
);
}
Banner.propTypes = {
type: React.PropTypes.oneOf([
'success',
'warning',
'error',
'info',
]),
children: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.array,
]).isRequired,
style: View.propTypes.style,
textStyle: Text.propTypes.style,
};
const styles = StyleSheet.create({
banner: {
alignItems: 'center',
elevation: 3,
},
bannerText: {
color: '#ffffff',
},
warning: {
backgroundColor: '#FFC107',
},
error: {
backgroundColor: '#f44336',
},
success: {
backgroundColor: '#4CAF50',
},
});
export default Banner;

View File

@@ -0,0 +1,75 @@
import React from 'react';
import { View, TouchableHighlight } from 'react-native';
import VectorIcon from 'react-native-vector-icons/MaterialIcons';
type Props = {
name: string,
size?: number,
color?: string,
allowFontScaling?: boolean,
style?: Object,
rotate?: number,
onPress?: () => void,
underlayColor?: string,
};
// TODO Spin?
class Icon extends React.Component {
constructor() {
super();
this.measured = false;
this.state = {
width: 0,
};
}
setDimensions(e) {
if (!this.measured) {
this.measured = true;
this.setState({
width: e.nativeEvent.layout.width,
});
}
}
props: Props;
render() {
const { name, size = 24, color = '#757575', allowFontScaling = true, style, rotate, onPress, underlayColor } = this.props;
const icon = (
<View
onLayout={(e) => this.setDimensions(e)}
style={[
style,
rotate ? { transform: [{ rotate: `${rotate}deg` }] } : null,
]}
>
<VectorIcon
name={name.toLowerCase().replace(/\s+/g, '-')}
size={size}
color={color}
allowFontScaling={allowFontScaling}
/>
</View>
);
if (!onPress) {
return icon;
}
return (
<TouchableHighlight
underlayColor={underlayColor || 'rgba(0, 0, 0, 0.054)'}
onPress={onPress}
style={{ padding: 8, borderRadius: (this.state.width + 8) / 2 }}
>
{icon}
</TouchableHighlight>
);
}
}
export default Icon;

View File

@@ -0,0 +1,70 @@
import React, { PropTypes, Component } from 'react';
import some from 'lodash.some';
import { connect } from 'react-redux';
import Toast from 'react-native-simple-toast';
import { runTests } from '../tests/index';
import RunStatus from '../../lib/RunStatus';
import Icon from '../components/Icon';
class OverviewControlButton extends Component {
constructor(props, context) {
super(props, context);
this.handleOnPress = this.handleOnPress.bind(this);
}
testSuitesAreRunning() {
const { testSuites } = this.props;
return some(Object.values(testSuites), ({ status }) => status === RunStatus.RUNNING);
}
handleOnPress() {
const { focusedTestIds, pendingTestIds, tests } = this.props;
runTests(tests, { focusedTestIds, pendingTestIds });
Toast.show('Running all suite tests.');
}
render() {
if (this.testSuitesAreRunning()) {
return (
<Icon
color={'#ffffff'}
size={28}
name="autorenew"
/>
);
}
return (
<Icon
color={'#ffffff'}
size={28}
name="play circle filled"
onPress={this.handleOnPress}
/>
);
}
}
OverviewControlButton.propTypes = {
tests: PropTypes.objectOf(PropTypes.object).isRequired,
testSuites: PropTypes.objectOf(PropTypes.object).isRequired,
focusedTestIds: PropTypes.objectOf(PropTypes.bool).isRequired,
pendingTestIds: PropTypes.objectOf(PropTypes.bool).isRequired,
};
function mapStateToProps({ tests, testSuites, focusedTestIds, pendingTestIds }) {
return {
tests,
testSuites,
focusedTestIds,
pendingTestIds,
};
}
export default connect(mapStateToProps)(OverviewControlButton);

View File

@@ -0,0 +1,52 @@
import { View, Text } from 'react-native';
import React, { PropTypes, Component } from 'react';
import RunStatus from '../../lib/RunStatus';
import Icon from './Icon';
class StatusIndicator extends Component {
render() {
const { status, progress } = this.props;
switch (status) {
case RunStatus.RUNNING:
if (progress > 0) {
return (
<View style={{ width: 30, flex: 1, justifyContent: 'flex-end' }}>
<Text style={{ fontSize: 11, marginBottom: 20 }}>
{progress.toFixed(0)}%
</Text>
</View>
);
}
return (
<Icon color={'rgba(0, 0, 0, 0.2)'} name="autorenew" />
);
case RunStatus.OK:
return (
<Icon name={'done'} />
);
case RunStatus.ERR:
return (
<Icon color={'#f44336'} name="clear" />
);
default:
return null;
}
}
}
StatusIndicator.propTypes = {
status: PropTypes.oneOf(Object.values(RunStatus)),
progress: PropTypes.number,
};
StatusIndicator.defaultProps = {
status: null,
progress: 0
};
module.exports = StatusIndicator;

View File

@@ -0,0 +1,71 @@
import React, { PropTypes, Component } from 'react';
import { connect } from 'react-redux';
import Toast from 'react-native-simple-toast';
import RunStatus from '../../lib/RunStatus';
import { runTest } from '../tests/index';
import Icon from './Icon';
class TestControlButton extends Component {
constructor(props, context) {
super(props, context);
this.handleOnPress = this.handleOnPress.bind(this);
}
testIsPending() {
const { test: { id }, pendingTestIds } = this.props;
return !!pendingTestIds[id];
}
handleOnPress() {
const { test: { id, description } } = this.props;
runTest(id);
Toast.show(`Running ${description}.`);
}
render() {
const { test: { status } } = this.props;
if (status !== RunStatus.STARTED && !this.testIsPending()) {
return (
<Icon
color={'#ffffff'}
size={28}
name="play circle filled"
onPress={this.handleOnPress}
/>
);
}
return null;
}
}
TestControlButton.propTypes = {
test: PropTypes.shape({
id: PropTypes.number.isRequired,
status: PropTypes.string,
description: PropTypes.string.isRequired,
}).isRequired,
pendingTestIds: PropTypes.objectOf(PropTypes.bool).isRequired,
};
TestControlButton.defaultProps = {
};
function mapStateToProps({ tests, pendingTestIds }, { testId }) {
const test = tests[testId];
return {
test,
pendingTestIds,
};
}
module.exports = connect(mapStateToProps)(TestControlButton);

View File

@@ -0,0 +1,96 @@
import React, { PropTypes, Component } from 'react';
import { connect } from 'react-redux';
import Toast from 'react-native-simple-toast';
import RunStatus from '../../lib/RunStatus';
import { runTests } from '../tests/index';
import Icon from './Icon';
class TestSuiteControlButton extends Component {
constructor(props, context) {
super(props, context);
this.toggleOnlyShowFailingTests = this.toggleOnlyShowFailingTests.bind(this);
this.startTestSuite = this.startTestSuite.bind(this);
}
startTestSuite() {
const { testSuite: { name, testIds }, tests, focusedTestIds, pendingTestIds } = this.props;
const testSuiteTests = testIds.reduce((memo, testId) => {
// eslint-disable-next-line no-param-reassign
memo[testId] = tests[testId];
return memo;
}, {});
runTests(testSuiteTests, { focusedTestIds, pendingTestIds });
Toast.show(`Running ${name} tests.`);
}
toggleOnlyShowFailingTests() {
const { onlyShowFailingTests, onFilterChange } = this.props;
onFilterChange({ onlyShowFailingTests: !onlyShowFailingTests });
}
render() {
const { testSuite: { status }, onlyShowFailingTests } = this.props;
if (status === RunStatus.ERR) {
return (
<Icon
color={onlyShowFailingTests ? '#ffffff' : 'rgba(255, 255, 255, 0.54)'}
size={28}
name="error outline"
onPress={this.toggleOnlyShowFailingTests}
/>
);
} else if (status !== RunStatus.RUNNING) {
return (
<Icon
color={'#ffffff'}
size={28}
name="play circle filled"
onPress={this.startTestSuite}
/>
);
}
return null;
}
}
TestSuiteControlButton.propTypes = {
testSuite: PropTypes.shape({
status: PropTypes.oneOf(Object.values(RunStatus)),
}).isRequired,
tests: PropTypes.objectOf(PropTypes.object).isRequired,
focusedTestIds: PropTypes.objectOf(PropTypes.bool).isRequired,
pendingTestIds: PropTypes.objectOf(PropTypes.bool).isRequired,
onlyShowFailingTests: PropTypes.bool,
onFilterChange: PropTypes.func.isRequired,
};
TestSuiteControlButton.defaultProps = {
onlyShowFailingTests: false,
};
function mapStateToProps({ tests, testSuites, focusedTestIds, pendingTestIds }, { testSuiteId }) {
const testSuite = testSuites[testSuiteId];
return {
tests,
testSuite,
focusedTestIds,
pendingTestIds,
};
}
module.exports = connect(mapStateToProps)(TestSuiteControlButton);

View File

@@ -0,0 +1,82 @@
// @flow
import React from 'react';
import { View, Text, AppState, NetInfo, StatusBar, Platform } from 'react-native';
import { connect } from 'react-redux';
import Navigator from '../navigator';
import { setNetworkState, setAppState } from '../actions/AppActions';
type Props = {
dispatch: () => void,
};
class CoreContainer extends React.Component {
constructor() {
super();
this._isConnected = false;
}
/**
* On app mount, listen for changes to app & network state
*/
componentDidMount() {
if (Platform.OS === 'android') {
StatusBar.setBackgroundColor('#0279ba');
}
if (Platform.OS === 'ios') {
StatusBar.setBarStyle('light-content')
}
AppState.addEventListener('change', this.handleAppStateChange);
NetInfo.isConnected.fetch().then((isConnected) => {
this.handleAppStateChange('active'); // Force connect (react debugger issue)
this.props.dispatch(setNetworkState(isConnected));
NetInfo.isConnected.addEventListener('change', this.handleNetworkChange);
});
}
/**
* Remove listeners on app unmount
*/
componentWillUnmount() {
AppState.removeEventListener('change', this.handleAppStateChange);
NetInfo.isConnected.removeEventListener('change', this.handleNetworkChange);
}
props: Props;
/**
* Handle app state changes
* https://facebook.github.io/react-native/docs/appstate.html
* @param state
*/
handleAppStateChange = (state) => {
this.props.dispatch(setAppState(state));
if (state === 'active' && this._isConnected) {
// firestack.database().goOnline();
} else if (state === 'background') {
// firestack.database().goOffline();
}
};
/**
* Handle app network changes
* https://facebook.github.io/react-native/docs/netinfo.html
* @param isConnected
*/
handleNetworkChange = (isConnected) => {
this._isConnected = isConnected;
this.props.dispatch(setNetworkState(isConnected));
if (isConnected) {
// firestack.database().goOnline();
} else {
// firestack.database().goOffline();
}
};
render() {
return <Navigator />;
}
}
export default connect()(CoreContainer);

32
tests/src/firebase.js Normal file
View File

@@ -0,0 +1,32 @@
import firebase from 'firebase';
import RNfirebase from 'react-native-firebase';
import DatabaseContents from './tests/support/DatabaseContents';
const config = {
apiKey: 'AIzaSyDnVqNhxU0Biit9nCo4RorAh5ulQQwko3E',
authDomain: 'rnfirebase-b9ad4.firebaseapp.com',
databaseURL: 'https://rnfirebase-b9ad4.firebaseio.com',
storageBucket: 'rnfirebase-b9ad4.appspot.com',
messagingSenderId: '305229645282',
};
const instances = {
web: firebase.initializeApp(config),
native: RNfirebase.initializeApp({
debug: __DEV__ ? '*' : false,
errorOnMissingPlayServices: false,
persistence: true,
}),
};
instances.web.database().ref('tests/types').set(DatabaseContents.DEFAULT);
instances.web.database().ref('tests/priority').setWithPriority({
foo: 'bar',
}, 666);
// instances.native.messaging().subscribeToTopic('fcm_test');
export default instances;

3
tests/src/helpers.js Normal file
View File

@@ -0,0 +1,3 @@
// import fs from 'fs';
import path from 'path';

64
tests/src/main.js Normal file
View File

@@ -0,0 +1,64 @@
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import CoreContainer from './containers/CoreContainer';
import setupStore from './store/setup';
import { setupSuites } from './tests/index';
global.Promise = require('bluebird');
type State = {
loading: boolean,
store: any,
};
function bootstrap() {
// Remove logging on production
if (!__DEV__) {
console.log = () => {
};
console.warn = () => {
};
console.error = () => {
};
console.disableYellowBox = true;
}
class Root extends Component {
constructor() {
super();
this.state = {
loading: true,
store: null,
};
}
state: State;
componentDidMount() {
setupStore((store) => {
setupSuites(store);
this.setState({
store,
loading: false,
});
});
}
render() {
if (this.state.loading) {
return null;
}
return (
<Provider store={this.state.store}>
<CoreContainer />
</Provider>
);
}
}
return Root;
}
export default bootstrap();

20
tests/src/navigator.js Normal file
View File

@@ -0,0 +1,20 @@
import { StackNavigator } from 'react-navigation';
import Overview from './screens/Overview';
import Suite from './screens/Suite';
import Test from './screens/Test';
export default StackNavigator({
Overview: { screen: Overview },
Suite: { screen: Suite },
Test: { screen: Test },
});
export const initialNavState = {
index: 0,
routes: [
{
key: 'Overview',
},
],
};

View File

@@ -0,0 +1,35 @@
import * as fcmTypes from '../actions/FCMActions';
import * as appTypes from '../actions/AppActions';
type State = {
appState: 'string',
isConnected: boolean,
fcmToken: string,
};
const initialState = {
appState: 'active',
isConnected: true,
fcmToken: '',
};
function device(state: State = initialState, action: Object): State {
if (action.type === appTypes.APP_SET_NETWORK_STATE) {
return {
...state,
isConnected: action.isConnected,
};
}
if (action.type === appTypes.APP_SET_APP_STATE) {
return {
...state,
appState: action.appState,
};
}
return state;
}
export default device;

View File

@@ -0,0 +1,9 @@
import { initialState } from '../tests/index';
const initState = initialState();
function focusedTestIdsReducers(state = initState.focusedTestIds): State {
return state;
}
export default focusedTestIdsReducers;

View File

@@ -0,0 +1,17 @@
import { combineReducers } from 'redux';
import device from './device';
import tests from './testsReducers';
import testContexts from './testContextsReducers';
import testSuites from './testSuitesReducers';
import pendingTestIds from './pendingTestIdsReducers';
import focusedTestIds from './focusedTestIdsReducers';
export default combineReducers({
device,
pendingTestIds,
focusedTestIds,
testContexts,
tests,
testSuites,
});

View File

@@ -0,0 +1,9 @@
import { initialState } from '../tests';
const initState = initialState();
function pendingTestIdsReducers(state = initState.pendingTestIds): State {
return state;
}
export default pendingTestIdsReducers;

View File

@@ -0,0 +1,9 @@
import { initialState } from '../tests/index';
const initState = initialState();
function testsReducers(state = initState.testContexts): State {
return state;
}
export default testsReducers;

View File

@@ -0,0 +1,23 @@
import * as testActions from '../actions/TestActions';
import { flatten, unflatten } from 'deeps';
import { initialState } from '../tests/index';
const initState = initialState();
function testsReducers(state = initState.testSuites, action: Object): State {
if (action.type === testActions.TEST_SET_SUITE_STATUS) {
const flattened = flatten(state);
if (action.status) flattened[`${action.suiteId}.status`] = action.status;
if (action.message) flattened[`${action.suiteId}.message`] = action.message;
if (action.progress) flattened[`${action.suiteId}.progress`] = action.progress;
if (!isNaN(action.time)) flattened[`${action.suiteId}.time`] = action.time;
return unflatten(flattened);
}
return state;
}
export default testsReducers;

View File

@@ -0,0 +1,22 @@
import * as testActions from '../actions/TestActions';
import { flatten, unflatten } from 'deeps';
import { initialState } from '../tests/index';
const initState = initialState();
function testsReducers(state = initState.tests, action: Object): State {
if (action.type === testActions.TEST_SET_STATUS) {
const flattened = flatten(state);
flattened[`${action.testId}.status`] = action.status;
flattened[`${action.testId}.message`] = action.message;
flattened[`${action.testId}.time`] = action.time;
return unflatten(flattened);
}
return state;
}
export default testsReducers;

View File

@@ -0,0 +1,299 @@
import React, { PropTypes } from 'react';
import { StyleSheet, View, Text, ListView, TouchableHighlight } from 'react-native';
import { connect } from 'react-redux';
import some from 'lodash.some';
import RunStatus from '../../lib/RunStatus';
import Banner from '../components/Banner';
import StatusIndicator from '../components/StatusIndicator';
import OverviewControlButton from '../components/OverviewControlButton';
class Overview extends React.Component {
// noinspection JSUnusedGlobalSymbols
static navigationOptions = {
title: 'Test Suites',
header: () => {
return {
style: { backgroundColor: '#0288d1' },
tintColor: '#ffffff',
right: (
<View style={{ marginRight: 8 }}>
<OverviewControlButton />
</View>
),
};
},
};
/**
* Renders separator between ListView sections
* @param {String} sectionID
* @param {String} rowID
* @returns {XML} JSX component used as ListView separator
*/
static renderSeparator(sectionID, rowID) {
return (
<View
key={`separator_${sectionID}_${rowID}`}
style={styles.separator}
/>
);
}
/**
* Filters test suites to those that have one or more tests that should be visible.
* If one or more tests are focused it only returns test suites with focused tests,
* otherwise, it returns all test suites.
* @param {IndexedTestSuiteGroup} testSuites - group of available test suites
* @param {IdLookup} focusedTestIds - lookup for focused tests
* @returns {IndexedTestSuiteGroup} - indexed group of test suites that should be shown
*/
static testSuitesToShow({ testSuites, focusedTestIds }) {
if (Object.keys(focusedTestIds).length > 0) {
return Object.keys(testSuites).reduce((memo, testSuiteId) => {
const testSuite = testSuites[testSuiteId];
const testSuiteHasFocusedTests = some(testSuite.testIds, (testId) => {
return focusedTestIds[testId];
});
if (testSuiteHasFocusedTests) {
// eslint-disable-next-line no-param-reassign
memo[testSuiteId] = testSuite;
}
return memo;
}, {});
}
return testSuites;
}
/**
* Copies initial values for test suites from props into state, so they may be
* rendered as a ListView
* @param {Object} props - props used to render component
* @param {Object} context - context used to render component
*/
constructor(props, context) {
super(props, context);
this.dataSource = new ListView.DataSource({
rowHasChanged: (r1, r2) => JSON.stringify(r1) !== JSON.stringify(r2),
});
this.state = {
dataBlob: this.dataSource.cloneWithRows(Overview.testSuitesToShow(props)),
};
}
/**
* Copies latest test suite status into state so they may be rendered as a ListView
* @param {Object} nextProps - next props used to render component
* @param {Object.<number,TestSuite>} nextProps.testSuites - test suites to render
* @param {IdLookup} nextProps.focusedTestIds - lookup for focus tests
*/
componentWillReceiveProps({ testSuites, focusedTestIds }) {
this.setState({
dataBlob: this.dataSource.cloneWithRows(Overview.testSuitesToShow({ testSuites, focusedTestIds })),
});
}
/**
* Navigate to test suite screen
* @param {TestSuiteId} testSuiteId - id of test suite to navigate to
*/
goToTestSuite(testSuite) {
const { navigation: { navigate } } = this.props;
navigate('Suite', { testSuiteId: testSuite.id, title: testSuite.name });
}
/**
*
* @param testSuite
* @param sectionId
* @param rowId
* @param highlight
* @returns {XML}
*/
renderRow(testSuite, sectionId, rowId, highlight) {
const { description, name, status, progress } = testSuite;
return (
<TouchableHighlight
key={`row_${rowId}`}
underlayColor={'rgba(0, 0, 0, 0.054)'}
onPress={() => {
this.goToTestSuite(testSuite);
highlight();
}}
>
<View style={[styles.row, status === RunStatus.ERR ? styles.error : null]}>
<View>
<Text style={styles.title}>{name}</Text>
<Text
style={styles.description}
numberOfLines={1}
>
{description}
</Text>
</View>
<View style={styles.statusContainer}>
<StatusIndicator status={status} progress={progress} />
</View>
</View>
</TouchableHighlight>
);
}
/**
* Renders a warning toast banner if there are one or more tests that are pending
* @returns {null|XML} Toast banner if there are test pending, else null
*/
renderPendingTestsBanner() {
const { pendingTestIds } = this.props;
const pendingTestsCount = Object.keys(pendingTestIds).length;
if (pendingTestsCount > 0) {
return (
<Banner type="warning">
{pendingTestsCount} pending test(s).
</Banner>
);
}
return null;
}
renderStatusBanner() {
const { testSuites } = this.props;
let totalProgress = 0;
let isRunning = false;
let isErrors = false;
let totalTime = 0;
Object.values(testSuites).forEach(({ progress, status, time }) => {
totalProgress += progress;
totalTime += time;
if (status === RunStatus.RUNNING) {
isRunning = true;
} else if (status === RunStatus.ERR) {
isErrors = true;
}
});
totalProgress /= Object.keys(testSuites).length;
if (isRunning) {
return (
<Banner type={isErrors ? 'error' : 'warning'}>Running ({(totalTime / 1000).toFixed(0)}s) {totalProgress.toFixed(2)}%</Banner>
);
} else if (totalProgress > 0) {
if (isErrors) {
return (
<Banner type={'error'}>Tests Complete with errors</Banner>
);
}
return (
<Banner type={'success'}>Tests Complete</Banner>
);
}
return null;
}
/**
* Renders ListView of test suites that should be visible, taking into consideration
* any focused tests
* @returns {XML} ListView of test suites
*/
render() {
return (
<View style={styles.container}>
{ this.renderPendingTestsBanner() }
{ this.renderStatusBanner() }
<ListView
enableEmptySections
dataSource={this.state.dataBlob}
renderRow={(...args) => this.renderRow(...args)}
renderSeparator={(...args) => Overview.renderSeparator(...args)}
/>
</View>
);
}
}
Overview.propTypes = {
testSuites: PropTypes.objectOf(PropTypes.shape({
id: PropTypes.number.isRequired,
description: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
status: PropTypes.oneOf(Object.values(RunStatus)),
})).isRequired,
tests: PropTypes.objectOf(PropTypes.shape({
testSuiteId: PropTypes.number.isRequired,
})).isRequired,
navigation: PropTypes.shape({
navigate: PropTypes.func.isRequired,
}).isRequired,
running: PropTypes.bool.isRequired,
pendingTestIds: PropTypes.objectOf(PropTypes.bool).isRequired,
focusedTestIds: PropTypes.objectOf(PropTypes.bool).isRequired,
};
const styles = StyleSheet.create({
rightContainer: {
marginRight: 16,
},
container: {
flex: 1,
backgroundColor: '#ffffff',
},
title: {
fontSize: 17,
fontWeight: '600',
},
description: {
fontSize: 11,
},
statusContainer: {
flex: 1,
alignItems: 'flex-end',
},
row: {
height: 56,
paddingHorizontal: 16,
flexDirection: 'row',
alignItems: 'center',
},
error: {
backgroundColor: 'rgba(255, 0, 0, 0.054)',
},
separator: {
height: 1,
backgroundColor: '#eeeeee',
},
});
function mapStateToProps({ testSuites, tests, pendingTestIds, focusedTestIds }) {
return {
testSuites,
tests,
pendingTestIds,
focusedTestIds,
running: Object.values(testSuites).filter(suite => suite.status === RunStatus.RUNNING).length > 0,
};
}
export default connect(mapStateToProps)(Overview);

404
tests/src/screens/Suite.js Normal file
View File

@@ -0,0 +1,404 @@
import React, { PropTypes } from 'react';
import { StyleSheet, View, Text, ListView, TouchableHighlight } from 'react-native';
import { connect } from 'react-redux';
import RunStatus from '../../lib/RunStatus';
import Banner from '../components/Banner';
import StatusIndicator from '../components/StatusIndicator';
import TestSuiteControlButton from '../components/TestSuiteControlButton';
class Suite extends React.Component {
static navigationOptions = {
title: ({ state: { params: { title } } }) => {
return title;
},
header: ({ state: { params: { testSuiteId, onlyShowFailingTests } }, setParams }) => {
return {
style: { backgroundColor: '#0288d1' },
tintColor: '#ffffff',
right: (
<View style={{ flexDirection: 'row', marginRight: 8 }}>
<TestSuiteControlButton
testSuiteId={testSuiteId}
onlyShowFailingTests={onlyShowFailingTests}
onFilterChange={setParams}
/>
</View>
),
};
},
};
/**
* Render test group header
* @param data
* @param title
* @returns {XML}
*/
static renderHeader(data, title) {
return (
<View
key={`header_${title}`}
style={styles.header}
>
<Text style={styles.headerText}>
{title.toUpperCase()}
</Text>
</View>
);
}
constructor(props) {
super(props);
this.dataSource = new ListView.DataSource({
rowHasChanged: (r1, r2) => JSON.stringify(r1) !== JSON.stringify(r2),
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
});
this.state = {
dataBlob: this.dataSource.cloneWithRowsAndSections(buildRowsWithSections(props)),
};
}
/**
* componentWillReceiveProps
* @param nextProps
*/
componentWillReceiveProps(nextProps) {
const {
tests,
testContexts,
navigation: { state: { params: { onlyShowFailingTests } } },
} = nextProps;
const newRowsWithSections = (() => {
if (onlyShowFailingTests) {
return Object.values(testContexts).reduce((sections, context) => {
const { name } = context;
context.testIds.forEach((testId) => {
const test = tests[testId];
if (test && test.status === RunStatus.ERR) {
// eslint-disable-next-line no-param-reassign
sections[name] = sections[name] || [];
sections[name].push(test);
}
});
return sections;
}, {});
}
return buildRowsWithSections(nextProps);
})();
this.setState({
dataBlob: this.dataSource.cloneWithRowsAndSections(newRowsWithSections),
});
}
/**
* Go to a single test
* @param testId
*/
goToTest(test) {
const { navigation: { navigate } } = this.props;
navigate('Test', { testId: test.id, title: test.description });
}
/**
* Render test row
* @param test
* @param sectionId
* @param rowId
* @param highlight
* @returns {XML}
*/
renderRow(test, sectionId, rowId, highlight) {
const { pendingTestIds } = this.props;
const { status, description, id } = test;
return (
<TouchableHighlight
key={`row_${rowId}`}
underlayColor={'rgba(0, 0, 0, 0.054)'}
onPress={() => {
this.goToTest(test);
highlight();
}}
>
<View style={[styles.row, status === RunStatus.ERR ? styles.error : null]}>
<View
style={[{ flex: 9 }, styles.rowContent]}
>
<Text
numberOfLines={2}
style={pendingTestIds[id] ? styles.disabledRow : {}}
>
{description}
</Text>
</View>
<View style={[{ flex: 1 }, styles.rowContent, [{ alignItems: 'center' }]]}>
<StatusIndicator status={status} />
</View>
</View>
</TouchableHighlight>
);
}
/**
*
* @param sectionID
* @param rowID
* @returns {XML}
*/
renderSeparator(sectionID, rowID) {
return (
<View
key={`separator_${sectionID}_${rowID}`}
style={styles.separator}
/>
);
}
renderPendingTestsBanner() {
const { testSuite: { testIds }, pendingTestIds } = this.props;
let pendingTestsCount = 0;
testIds.forEach((testId) => {
if (pendingTestIds[testId]) {
pendingTestsCount += 1;
}
});
if (pendingTestsCount) {
return (
<Banner type="warning">
{pendingTestsCount} pending test(s).
</Banner>
);
}
return null;
}
renderStatusBanner() {
const { testSuite: { status, progress, time, message } } = this.props;
switch (status) {
case RunStatus.RUNNING:
return (
<Banner type={'warning'}>
Tests are currently running ({ progress.toFixed(2) }%).
</Banner>
);
case RunStatus.OK:
return (
<Banner type={'success'}>
Tests passed. ({ time }ms)
</Banner>
);
case RunStatus.ERR:
return (
<Banner type={'error'}>
{message} ({time}ms)
</Banner>
);
default:
return null;
}
}
/**
*
* @returns {XML}
*/
render() {
const { dataBlob } = this.state;
return (
<View style={styles.container}>
{ this.renderPendingTestsBanner() }
{ this.renderStatusBanner() }
<ListView
dataSource={dataBlob}
renderSectionHeader={(...args) => Suite.renderHeader(...args)}
renderRow={(...args) => this.renderRow(...args)}
renderSeparator={(...args) => this.renderSeparator(...args)}
/>
</View>
);
}
}
Suite.propTypes = {
navigation: PropTypes.shape({
setParams: PropTypes.func.isRequired,
navigate: PropTypes.func.isRequired,
state: PropTypes.shape({
params: PropTypes.object,
onlyShowFailingTests: PropTypes.bool,
}).isRequired,
}).isRequired,
testSuite: PropTypes.shape({
status: PropTypes.string,
progress: PropTypes.number,
time: PropTypes.number,
message: PropTypes.string,
}).isRequired,
testContexts: PropTypes.objectOf(PropTypes.shape({
name: PropTypes.string.isRequired,
testIds: PropTypes.arrayOf(PropTypes.number).isRequired,
})).isRequired,
tests: PropTypes.objectOf(PropTypes.shape({
id: PropTypes.number,
description: PropTypes.string,
status: PropTypes.oneOf(Object.values(RunStatus)),
})).isRequired,
pendingTestIds: PropTypes.objectOf(PropTypes.bool).isRequired,
focusedTestIds: PropTypes.objectOf(PropTypes.bool).isRequired,
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#ffffff',
},
banner: {
alignItems: 'center',
elevation: 3,
},
bannerText: {
color: '#ffffff',
},
inProgress: {
backgroundColor: '#FFC107',
},
errorBanner: {
backgroundColor: '#f44336',
},
header: {
elevation: 3,
justifyContent: 'center',
height: 25,
paddingHorizontal: 16,
backgroundColor: '#ECEFF1',
},
headerText: {
fontWeight: '800',
},
row: {
paddingHorizontal: 16,
height: 48,
flexDirection: 'row',
},
rowContent: {
justifyContent: 'center',
},
disabledRow: {
color: '#c3c3c3',
},
error: {
backgroundColor: 'rgba(255, 0, 0, 0.054)',
},
separator: {
height: 1,
backgroundColor: '#eeeeee',
},
});
function buildRowsWithSections({ testContexts, tests, focusedTestIds }) {
const someTestsAreFocused = Object.keys(focusedTestIds).length > 0;
return Object.values(testContexts).reduce((sections, testContext) => {
const { testIds } = testContext;
const contextTestsToShow = testIds.reduce((memo, testId) => {
const test = tests[testId];
if (someTestsAreFocused) {
if (focusedTestIds[testId]) {
memo.push(test);
}
} else {
memo.push(test);
}
return memo;
}, []);
if (contextTestsToShow.length > 0) {
const effectiveContext = highestNonRootAncestor(testContext, testContexts);
// eslint-disable-next-line no-param-reassign
sections[effectiveContext.name] = sections[effectiveContext.name] || [];
// eslint-disable-next-line no-param-reassign
sections[effectiveContext.name] = sections[effectiveContext.name].concat(contextTestsToShow);
}
return sections;
}, {});
}
function highestNonRootAncestor(testContext, testContexts) {
const parentContextId = testContext.parentContextId;
if (parentContextId) {
const parentContext = testContexts[parentContextId];
const parentContextIsNotRoot = parentContext && parentContext.parentContextId;
if (parentContextIsNotRoot) {
return highestNonRootAncestor(parentContext, testContexts);
}
}
return testContext;
}
function mapStateToProps(state, { navigation: { state: { params: { testSuiteId } } } }) {
const { tests, testContexts, testSuites, pendingTestIds, focusedTestIds } = state;
const testSuite = testSuites[testSuiteId];
const testSuiteContexts = testSuite.testContextIds.reduce((suiteContexts, contextId) => {
// eslint-disable-next-line no-param-reassign
suiteContexts[contextId] = testContexts[contextId];
return suiteContexts;
}, {});
const testSuiteTests = testSuite.testIds.reduce((suiteTests, testId) => {
// eslint-disable-next-line no-param-reassign
suiteTests[testId] = tests[testId];
return suiteTests;
}, {});
return {
testSuite,
testContexts: testSuiteContexts,
tests: testSuiteTests,
pendingTestIds,
focusedTestIds,
};
}
export default connect(mapStateToProps)(Suite);

149
tests/src/screens/Test.js Normal file
View File

@@ -0,0 +1,149 @@
import React, { PropTypes } from 'react';
import { StyleSheet, View, Text, ScrollView } from 'react-native';
import { connect } from 'react-redux';
import { js_beautify as beautify } from 'js-beautify';
import Banner from '../components/Banner';
import RunStatus from '../../lib/RunStatus';
import TestControlButton from '../components/TestControlButton';
class Test extends React.Component {
static navigationOptions = {
title: ({ state: { params: { title } } }) => {
return title;
},
header: ({ state: { params: { testId } } }) => {
return {
style: { backgroundColor: '#0288d1' },
tintColor: '#ffffff',
right: (
<View style={{ marginRight: 8 }}>
<TestControlButton testId={testId} />
</View>
),
};
},
};
static renderBanner({ status, time }) {
switch (status) {
case RunStatus.RUNNING:
return (
<Banner type={'warning'}>
Test is currently running.
</Banner>
);
case RunStatus.OK:
return (
<Banner type={'success'}>
Test passed. ({time}ms)
</Banner>
);
case RunStatus.ERR:
return (
<Banner type={'error'}>
Test failed. ({time}ms)
</Banner>
);
default:
return null;
}
}
componentDidMount() {
const { navigation: { setParams }, test } = this.props;
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;
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}>
{beautify(removeLastLine(removeFirstLine(func.toString())), { indent_size: 4, break_chained_methods: true })}
</Text>
</ScrollView>
</View>
</View>
);
}
}
Test.propTypes = {
test: PropTypes.shape({
status: PropTypes.string,
time: PropTypes.number,
message: PropTypes.string,
func: PropTypes.function,
}).isRequired,
navigation: PropTypes.shape({
setParams: PropTypes.func.isRequired,
}).isRequired,
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#ffffff',
},
content: {},
code: {
backgroundColor: '#3F373A',
color: '#c3c3c3',
padding: 5,
fontSize: 12,
},
codeHeader: {
fontWeight: '600',
fontSize: 18,
backgroundColor: '#000',
color: '#fff',
padding: 5,
},
});
function select({ tests }, { navigation: { state: { params: { testId } } } }) {
const test = tests[testId];
return {
test,
};
}
function removeLastLine(multiLineString) {
const index = multiLineString.lastIndexOf('\n');
return multiLineString.substring(0, index);
}
function removeFirstLine(multiLineString) {
return multiLineString.substring(multiLineString.indexOf('\n') + 1);
}
export default connect(select)(Test);

43
tests/src/store/setup.js Normal file
View File

@@ -0,0 +1,43 @@
import { AsyncStorage } from 'react-native';
import { applyMiddleware, createStore, compose } from 'redux';
import thunk from 'redux-thunk';
import reduxLogger from 'redux-logger';
import { persistStore, autoRehydrate } from 'redux-persist';
import whitelist from './whitelist';
import reducers from '../reducers';
function setup(done) {
const isDev = global.isDebuggingInChrome || __DEV__;
const logger = reduxLogger({
predicate: () => isDev,
collapsed: true,
duration: true,
});
// AsyncStorage.clear();
// Setup redux middleware
const middlewares = [autoRehydrate()];
middlewares.push(applyMiddleware(...[thunk]));
if (isDev) {
middlewares.push(applyMiddleware(...[logger]));
middlewares.push(applyMiddleware(require('redux-immutable-state-invariant')()));
}
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducers, {}, composeEnhancers(...middlewares));
// Attach the store to the Chrome debug window
if (global.isDebuggingInChrome) {
window.store = store;
}
persistStore(store, { whitelist, storage: AsyncStorage }, () => done(store));
}
export default setup;

View File

@@ -0,0 +1 @@
export default [''];

View File

@@ -0,0 +1,64 @@
export default function addTests({ describe, it, firebase }) {
describe('Analytics', () => {
it('logEvent: it should log a text event without error', () => {
return new Promise((resolve) => {
firebase.native.analytics().logEvent('test_event');
resolve();
});
});
it('logEvent: it should log a text event with parameters without error', () => {
return new Promise((resolve) => {
firebase.native.analytics().logEvent('test_event', {
boolean: true,
number: 1,
string: 'string',
});
resolve();
});
});
it('setAnalyticsCollectionEnabled: it should run without error', () => {
return new Promise((resolve) => {
firebase.native.analytics().setAnalyticsCollectionEnabled(true);
resolve();
});
});
it('setCurrentScreen: it should run without error', () => {
return new Promise((resolve) => {
firebase.native.analytics().setCurrentScreen('test screen', 'test class override');
resolve();
});
});
it('setMinimumSessionDuration: it should run without error', () => {
return new Promise((resolve) => {
firebase.native.analytics().setMinimumSessionDuration(10000);
resolve();
});
});
it('setSessionTimeoutDuration: it should run without error', () => {
return new Promise((resolve) => {
firebase.native.analytics().setSessionTimeoutDuration(1800000);
resolve();
});
});
it('setUserId: it should run without error', () => {
return new Promise((resolve) => {
firebase.native.analytics().setUserId('test-id');
resolve();
});
});
it('setUserProperty: it should run without error', () => {
return new Promise((resolve) => {
firebase.native.analytics().setUserProperty('test-property', 'test-value');
resolve();
});
});
});
}

View File

@@ -0,0 +1,9 @@
import firebase from '../../firebase';
import TestSuite from '../../../lib/TestSuite';
import analyticsTests from './analytics';
const suite = new TestSuite('Analytics', 'firebase.analytics()', firebase);
suite.addTests(analyticsTests);
export default suite;

View File

@@ -0,0 +1,311 @@
import should from 'should';
function randomString(length, chars) {
let mask = '';
if (chars.indexOf('a') > -1) mask += 'abcdefghijklmnopqrstuvwxyz';
if (chars.indexOf('A') > -1) mask += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
if (chars.indexOf('#') > -1) mask += '0123456789';
if (chars.indexOf('!') > -1) mask += '~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\';
let result = '';
for (let i = length; i > 0; --i) result += mask[Math.round(Math.random() * (mask.length - 1))];
return result;
}
function authTests({ tryCatch, describe, it, firebase }) {
describe('Anonymous', () => {
it('it should sign in anonymously', () => {
const successCb = (currentUser) => {
currentUser.should.be.an.Object();
currentUser.uid.should.be.a.String();
currentUser.toJSON().should.be.an.Object();
should.equal(currentUser.toJSON().email, null);
currentUser.isAnonymous.should.equal(true);
currentUser.providerId.should.equal('firebase');
firebase.native.auth().currentUser.uid.should.be.a.String();
return firebase.native.auth().signOut();
};
return firebase.native.auth().signInAnonymously().then(successCb);
});
});
describe('Link', () => {
it('it should link anonymous account <-> email account', () => {
const random = randomString(12, '#aA');
const email = `${random}@${random}.com`;
const pass = random;
const successCb = (currentUser) => {
currentUser.should.be.an.Object();
currentUser.uid.should.be.a.String();
currentUser.toJSON().should.be.an.Object();
should.equal(currentUser.toJSON().email, null);
currentUser.isAnonymous.should.equal(true);
currentUser.providerId.should.equal('firebase');
firebase.native.auth().currentUser.uid.should.be.a.String();
const credential = firebase.native.auth.EmailAuthProvider.credential(email, pass);
return currentUser
.link(credential)
.then((linkedUser) => {
linkedUser.should.be.an.Object();
linkedUser.uid.should.be.a.String();
linkedUser.toJSON().should.be.an.Object();
linkedUser.toJSON().email.should.eql(email);
linkedUser.isAnonymous.should.equal(false);
linkedUser.providerId.should.equal('firebase');
return firebase.native.auth().signOut();
}).catch((error) => {
return firebase.native.auth().signOut().then(() => {
return Promise.reject(error);
});
});
};
return firebase.native.auth().signInAnonymously().then(successCb);
});
it('it should error on link anon <-> email if email already exists', () => {
const email = 'test@test.com';
const pass = 'test1234';
const successCb = (currentUser) => {
currentUser.should.be.an.Object();
currentUser.uid.should.be.a.String();
currentUser.toJSON().should.be.an.Object();
should.equal(currentUser.toJSON().email, null);
currentUser.isAnonymous.should.equal(true);
currentUser.providerId.should.equal('firebase');
firebase.native.auth().currentUser.uid.should.be.a.String();
const credential = firebase.native.auth.EmailAuthProvider.credential(email, pass);
return currentUser
.link(credential)
.then(() => {
return firebase.native.auth().signOut().then(() => {
return Promise.reject(new Error('Did not error on link'));
});
}).catch((error) => {
return firebase.native.auth().signOut().then(() => {
error.code.should.equal('auth/email-already-in-use');
error.message.should.equal('The email address is already in use by another account.');
return Promise.resolve();
});
});
};
return firebase.native.auth().signInAnonymously().then(successCb);
});
});
describe('Email - Login', () => {
it('it should login with email and password', () => {
const email = 'test@test.com';
const pass = 'test1234';
const successCb = (currentUser) => {
currentUser.should.be.an.Object();
currentUser.uid.should.be.a.String();
currentUser.toJSON().should.be.an.Object();
currentUser.toJSON().email.should.eql('test@test.com');
currentUser.isAnonymous.should.equal(false);
currentUser.providerId.should.equal('firebase');
firebase.native.auth().currentUser.uid.should.be.a.String();
return firebase.native.auth().signOut();
};
return firebase.native.auth().signInWithEmailAndPassword(email, pass).then(successCb);
});
it('it should error on login if user is disabled', () => {
const email = 'disabled@account.com';
const pass = 'test1234';
const successCb = () => {
return Promise.reject(new Error('Did not error.'));
};
const failureCb = (error) => {
error.code.should.equal('auth/user-disabled');
error.message.should.equal('The user account has been disabled by an administrator.');
return Promise.resolve();
};
return firebase.native.auth().signInWithEmailAndPassword(email, pass).then(successCb).catch(failureCb);
});
it('it should error on login if password incorrect', () => {
const email = 'test@test.com';
const pass = 'test1234666';
const successCb = () => {
return Promise.reject(new Error('Did not error.'));
};
const failureCb = (error) => {
error.code.should.equal('auth/wrong-password');
error.message.should.equal('The password is invalid or the user does not have a password.');
return Promise.resolve();
};
return firebase.native.auth().signInWithEmailAndPassword(email, pass).then(successCb).catch(failureCb);
});
it('it should error on login if user not found', () => {
const email = 'randomSomeone@fourOhFour.com';
const pass = 'test1234';
const successCb = () => {
return Promise.reject(new Error('Did not error.'));
};
const failureCb = (error) => {
error.code.should.equal('auth/user-not-found');
error.message.should.equal('There is no user record corresponding to this identifier. The user may have been deleted.');
return Promise.resolve();
};
return firebase.native.auth().signInWithEmailAndPassword(email, pass).then(successCb).catch(failureCb);
});
});
describe('Email - Create', () => {
it('it should create a user with an email and password', () => {
const random = randomString(12, '#aA');
const email = `${random}@${random}.com`;
const pass = random;
const successCb = (newUser) => {
newUser.uid.should.be.a.String();
newUser.email.should.equal(email.toLowerCase());
newUser.emailVerified.should.equal(false);
newUser.isAnonymous.should.equal(false);
newUser.providerId.should.equal('firebase');
};
return firebase.native.auth().createUserWithEmailAndPassword(email, pass).then(successCb);
});
it('it should error on create with invalid email', () => {
const random = randomString(12, '#aA');
const email = `${random}${random}.com.boop.shoop`;
const pass = random;
const successCb = () => {
return Promise.reject(new Error('Did not error.'));
};
const failureCb = (error) => {
error.code.should.equal('auth/invalid-email');
error.message.should.equal('The email address is badly formatted.');
return Promise.resolve();
};
return firebase.native.auth().createUserWithEmailAndPassword(email, pass).then(successCb).catch(failureCb);
});
it('it should error on create if email in use', () => {
const email = 'test@test.com';
const pass = 'test123456789';
const successCb = () => {
return Promise.reject(new Error('Did not error.'));
};
const failureCb = (error) => {
error.code.should.equal('auth/email-already-in-use');
error.message.should.equal('The email address is already in use by another account.');
return Promise.resolve();
};
return firebase.native.auth().createUserWithEmailAndPassword(email, pass).then(successCb).catch(failureCb);
});
it('it should error on create if password weak', () => {
const email = 'testy@testy.com';
const pass = '123';
const successCb = () => {
return Promise.reject(new Error('Did not error.'));
};
const failureCb = (error) => {
error.code.should.equal('auth/weak-password');
// cannot test this message - it's different on the web client than ios/android return
// error.message.should.equal('The given password is invalid.');
return Promise.resolve();
};
return firebase.native.auth().createUserWithEmailAndPassword(email, pass).then(successCb).catch(failureCb);
});
});
describe('Misc', () => {
it('it should delete a user', () => {
const random = randomString(12, '#aA');
const email = `${random}@${random}.com`;
const pass = random;
const successCb = (newUser) => {
newUser.uid.should.be.a.String();
newUser.email.should.equal(email.toLowerCase());
newUser.emailVerified.should.equal(false);
newUser.isAnonymous.should.equal(false);
newUser.providerId.should.equal('firebase');
return firebase.native.auth().currentUser.delete();
};
return firebase.native.auth().createUserWithEmailAndPassword(email, pass).then(successCb);
});
it('it should return a token via getToken', () => {
const random = randomString(12, '#aA');
const email = `${random}@${random}.com`;
const pass = random;
const successCb = (newUser) => {
newUser.uid.should.be.a.String();
newUser.email.should.equal(email.toLowerCase());
newUser.emailVerified.should.equal(false);
newUser.isAnonymous.should.equal(false);
newUser.providerId.should.equal('firebase');
return newUser.getToken().then((token) => {
token.should.be.a.String();
token.length.should.be.greaterThan(24);
return firebase.native.auth().currentUser.delete();
});
};
return firebase.native.auth().createUserWithEmailAndPassword(email, pass).then(successCb);
});
it('it should reject signOut if no currentUser', () => {
return new Promise((resolve, reject) => {
if (firebase.native.auth().currentUser) {
return reject(new Error(`A user is currently signed in. ${firebase.native.auth().currentUser.uid}`));
}
const successCb = tryCatch(() => {
reject(new Error('No signOut error returned'));
}, reject);
const failureCb = tryCatch((error) => {
error.code.should.equal('auth/no_current_user');
error.message.should.equal('No user currently signed in.');
resolve();
}, reject);
return firebase.native.auth().signOut().then(successCb).catch(failureCb);
});
});
});
}
export default authTests;

View File

@@ -0,0 +1,10 @@
import firebase from '../../firebase';
import TestSuite from '../../../lib/TestSuite';
import authTests from './authTests';
const suite = new TestSuite('Auth', 'firebase.auth()', firebase);
suite.addTests(authTests);
export default suite;

View File

@@ -0,0 +1,10 @@
import firebase from '../../firebase';
import TestSuite from '../../../lib/TestSuite';
import logTests from './log';
const suite = new TestSuite('Crash', 'firebase.crash()', firebase);
// bootstrap tests
suite.addTests(logTests);
export default suite;

View File

@@ -0,0 +1,17 @@
export default function addTests({ describe, it, firebase }) {
describe('Log', () => {
it('log: it should log without error', () => {
return new Promise((resolve) => {
firebase.native.crash().log('Test log');
resolve();
});
});
it('logcat: it should log without error', () => {
return new Promise((resolve) => {
firebase.native.crash().logcat(0, 'LogTest', 'Test log');
resolve();
});
});
});
}

View File

@@ -0,0 +1,21 @@
import firebase from '../../firebase';
import TestSuite from '../../../lib/TestSuite';
/*
Test suite files
*/
import snapshotTests from './snapshot';
import refTestGroups from './ref/index';
const suite = new TestSuite('Database', 'firebase.database()', firebase);
/*
Register tests with test suite
*/
suite.addTests(refTestGroups);
suite.addTests(snapshotTests);
export default suite;

View File

@@ -0,0 +1,69 @@
function childTests({ describe, it, context, firebase }) {
describe('ref().child', () => {
context('when passed a shallow path', () => {
it('returns correct child ref', () => {
// Setup
const ref = firebase.native.database().ref('tests');
// Test
const childRef = ref.child('tests');
// Assertion
childRef.key.should.eql('tests');
});
});
context('when passed a nested path', () => {
it('returns correct child ref', () => {
// Setup
const ref = firebase.native.database().ref('tests');
// Test
const grandChildRef = ref.child('tests/number');
// Assertion
grandChildRef.key.should.eql('number');
});
});
context('when passed a path that doesn\'t exist', () => {
it('creates a reference, anyway', () => {
// Setup
const ref = firebase.native.database().ref('tests');
// Test
const grandChildRef = ref.child('doesnt/exist');
// Assertion
grandChildRef.key.should.eql('exist');
});
});
context('when passed an invalid path', () => {
it('creates a reference, anyway', () => {
// Setup
const ref = firebase.native.database().ref('tests');
// Test
const grandChildRef = ref.child('does$&nt/exist');
// Assertion
grandChildRef.key.should.eql('exist');
});
});
});
}
export default childTests;

View File

@@ -0,0 +1,38 @@
import DatabaseContents from '../../support/DatabaseContents';
function factoryTests({ describe, it, firebase }) {
describe('ref()', () => {
it('returns root reference when provided no path', () => {
// Setup
const ref = firebase.native.database().ref();
// Test
// Assertion
(ref.key === null).should.be.true();
(ref.parent === null).should.be.true();
});
it('returns reference to data at path', async () => {
// Setup
const ref = firebase.native.database().ref('tests/types/number');
// Test
let valueAtRef;
await ref.once('value', (snapshot) => {
valueAtRef = snapshot.val();
});
// Assertion
valueAtRef.should.eql(DatabaseContents.DEFAULT.number);
});
});
}
export default factoryTests;

View File

@@ -0,0 +1,43 @@
import onTests from './onTests';
import offTests from './offTests';
import onceTests from './onceTests';
import setTests from './setTests';
import updateTests from './updateTests';
import removeTests from './removeTests';
import pushTests from './pushTests';
import factoryTests from './factoryTests';
import keyTests from './keyTests';
import parentTests from './parentTests';
import childTests from './childTests';
import isEqualTests from './isEqualTests';
import refTests from './refTests';
import rootTests from './rootTests';
import transactionTests from './transactionTests';
import queryTests from './queryTests';
import DatabaseContents from '../../support/DatabaseContents';
const testGroups = [
factoryTests, keyTests, parentTests, childTests, rootTests,
pushTests, onTests, offTests, onceTests, updateTests, removeTests, setTests,
transactionTests, queryTests, refTests, isEqualTests,
];
function registerTestSuite(testSuite) {
testSuite.beforeEach(async function () {
this._databaseRef = testSuite.firebase.native.database().ref('tests/types');
await this._databaseRef.set(DatabaseContents.DEFAULT);
});
testSuite.afterEach(async function () {
await this._databaseRef.set(DatabaseContents.DEFAULT);
});
testGroups.forEach((testGroup) => {
testGroup(testSuite);
});
}
module.exports = registerTestSuite;

View File

@@ -0,0 +1,41 @@
function isEqualTests({ describe, before, it, firebase }) {
describe('ref().isEqual()', () => {
before(() => {
this.ref = firebase.native.database().ref('tests/types');
});
it('returns true when the reference is for the same location', () => {
// Setup
const ref2 = firebase.native.database().ref('tests/types');
// Assertion
this.ref.isEqual(ref2).should.eql(true);
});
it('returns false when the reference is for a different location', () => {
// Setup
const ref2 = firebase.native.database().ref('tests/types/number');
// Assertion
this.ref.isEqual(ref2).should.eql(false);
});
it('returns false when the reference is null', () => {
// Assertion
this.ref.isEqual(null).should.eql(false);
});
it('returns false when the reference is not a Reference', () => {
// Assertion
this.ref.isEqual(1).should.eql(false);
});
});
}
export default isEqualTests;

View File

@@ -0,0 +1,30 @@
function keyTests({ describe, it, firebase }) {
describe('ref().key', () => {
it('returns null for root ref', () => {
// Setup
const ref = firebase.native.database().ref();
// Test
// Assertion
(ref.key === null).should.be.true();
});
it('returns correct key for path', () => {
// Setup
const ref = firebase.native.database().ref('tests/types/number');
const arrayItemRef = firebase.native.database().ref('tests/types/array/1');
// Assertion
ref.key.should.eql('number');
arrayItemRef.key.should.eql('1');
});
});
}
export default keyTests;

View File

@@ -0,0 +1,276 @@
import should from 'should';
import sinon from 'sinon';
import DatabaseContents from '../../support/DatabaseContents';
function offTests({ describe, it, xit, xcontext, context, firebase }) {
describe('ref().off()', () => {
xit('doesn\'t unbind children callbacks', async () => {
// Setup
const parentCallback = sinon.spy();
const childCallback = sinon.spy();
const parentRef = firebase.native.database().ref('tests/types');
const childRef = firebase.native.database().ref('tests/types/string');
await new Promise((resolve) => {
parentRef.on('value', () => {
parentCallback();
resolve();
});
});
await new Promise((resolve) => {
childRef.on('value', () => {
childCallback();
resolve();
});
});
parentCallback.should.be.calledOnce();
childCallback.should.be.calledOnce();
// Returns nothing
should(parentRef.off(), undefined);
// Trigger event parent callback is listening to
await parentRef.set(DatabaseContents.DEFAULT);
// parent and child callbacks should not have been called any more
parentCallback.should.be.calledOnce();
childCallback.should.be.calledOnce();
// Trigger event child callback is listening to
await childRef.set(DatabaseContents.DEFAULT.string);
// child callback should still be listening
childCallback.should.be.calledOnce();
// Teardown
childRef.off();
});
context('when passed no arguments', () => {
context('and there are no callbacks bound', () => {
it('does nothing', () => {
const ref = firebase.native.database().ref('tests/types/array');
should(ref.off(), undefined);
});
});
it('stops all callbacks listening for all changes', async () => {
// Setup
const valueCallback = sinon.spy();
const childAddedCallback = sinon.spy();
const ref = firebase.native.database().ref('tests/types/array');
const arrayLength = DatabaseContents.DEFAULT.array.length;
await new Promise((resolve) => {
ref.on('value', () => {
valueCallback();
resolve();
});
});
await new Promise((resolve) => {
ref.on('child_added', () => {
childAddedCallback();
resolve();
});
});
valueCallback.should.be.calledOnce();
childAddedCallback.should.have.callCount(arrayLength);
// Check childAddedCallback is really attached
await ref.push(DatabaseContents.DEFAULT.number);
childAddedCallback.should.be.callCount(arrayLength + 1);
// Returns nothing
should(ref.off(), undefined);
// Trigger both callbacks
await ref.set(DatabaseContents.DEFAULT.array);
await ref.push(DatabaseContents.DEFAULT.number);
// Callbacks should have been unbound and not called again
valueCallback.should.be.calledOnce();
childAddedCallback.should.be.callCount(arrayLength + 1);
});
});
context('when passed an event type', () => {
context('and there are no callbacks bound', () => {
it('does nothing', () => {
const ref = firebase.native.database().ref('tests/types/array');
should(ref.off('value'), undefined);
});
});
context('that is invalid', () => {
it('does nothing', () => {
const ref = firebase.native.database().ref('tests/types/array');
should(ref.off('invalid'), undefined);
});
});
xit('detaches all callbacks listening for that event', async () => {
// Setup
const callbackA = sinon.spy();
const callbackB = sinon.spy();
const ref = firebase.native.database().ref('tests/types/string');
await new Promise((resolve) => {
ref.on('value', () => {
callbackA();
resolve();
});
});
await new Promise((resolve) => {
ref.on('value', () => {
callbackB();
resolve();
});
});
callbackA.should.be.calledOnce();
callbackB.should.be.calledOnce();
// Returns nothing
should(ref.off('value'), undefined);
// Assertions
await ref.set(DatabaseContents.DEFAULT.string);
// Callbacks should have been unbound and not called again
callbackA.should.be.calledOnce();
callbackB.should.be.calledOnce();
});
});
context('when passed a particular callback', () => {
context('and there are no callbacks bound', () => {
it('does nothing', () => {
const ref = firebase.native.database().ref('tests/types/array');
should(ref.off('value', sinon.spy()), undefined);
});
});
xit('detaches only that callback', async () => {
// Setup
const callbackA = sinon.spy();
const callbackB = sinon.spy();
const ref = firebase.native.database().ref('tests/types/string');
// Attach the callback the first time
await new Promise((resolve) => {
ref.on('value', () => {
callbackA();
resolve();
});
});
// Attach the callback the second time
await new Promise((resolve) => {
ref.on('value', () => {
callbackB();
resolve();
});
});
callbackA.should.be.calledOnce();
callbackB.should.be.calledOnce();
// Detach callbackA, only
should(ref.off('value', callbackA), undefined);
// Trigger the event the callback is listening to
await ref.set(DatabaseContents.DEFAULT.string);
// CallbackB should still be attached
callbackA.should.be.calledOnce();
callbackB.should.be.calledTwice();
// Teardown
should(ref.off('value', callbackB), undefined);
});
context('that has been added multiple times', () => {
xit('must be called as many times completely remove', async () => {
// Setup
const callbackA = sinon.spy();
const ref = firebase.native.database().ref('tests/types/string');
// Attach the callback the first time
await new Promise((resolve) => {
ref.on('value', () => {
callbackA();
resolve();
});
});
// Attach the callback the second time
await new Promise((resolve) => {
ref.on('value', () => {
callbackA();
resolve();
});
});
callbackA.should.be.calledTwice();
// Undo the first time the callback was attached
should(ref.off(), undefined);
// Trigger the event the callback is listening to
await ref.set(DatabaseContents.DEFAULT.number);
// Callback should have been called only once because one of the attachments
// has been removed
callbackA.should.be.calledThrice();
// Undo the second attachment
should(ref.off(), undefined);
// Trigger the event the callback is listening to
await ref.set(DatabaseContents.DEFAULT.number);
// Callback should not have been called any more times
callbackA.should.be.calledThrice();
});
});
});
xcontext('when a context', () => {
/**
* @todo Add tests for when a context is passed. Not sure what the intended
* behaviour is as the documentation is unclear, but assumption is that as the
* context is not required to unbind a listener, it's used as a filter parameter
* so in order for off() to remove a callback, the callback must have been bound
* with the same event type, callback function and context.
*
* Needs to be tested against web implementation, if possible.
*/
});
});
}
export default offTests;

View File

@@ -0,0 +1,125 @@
import sinon from 'sinon';
import 'should-sinon';
import Promise from 'bluebird';
import DatabaseContents from '../../support/DatabaseContents';
function onTests({ describe, it, firebase, tryCatch }) {
describe('ref().on()', () => {
it('calls callback when value changes', () => {
return Promise.each(Object.keys(DatabaseContents.DEFAULT), async (dataRef) => {
// Setup
const ref = firebase.native.database().ref(`tests/types/${dataRef}`);
const currentDataValue = DatabaseContents.DEFAULT[dataRef];
const callback = sinon.spy();
// Test
await new Promise((resolve) => {
ref.on('value', (snapshot) => {
callback(snapshot.val());
resolve();
});
});
callback.should.be.calledWith(currentDataValue);
const newDataValue = DatabaseContents.NEW[dataRef];
await ref.set(newDataValue);
// Assertions
callback.should.be.calledWith(newDataValue);
// Tear down
ref.off();
});
});
it('allows binding multiple callbacks to the same ref', () => {
return Promise.each(Object.keys(DatabaseContents.DEFAULT), async (dataRef) => {
// Setup
const ref = firebase.native.database().ref(`tests/types/${dataRef}`);
const currentDataValue = DatabaseContents.DEFAULT[dataRef];
const callbackA = sinon.spy();
const callbackB = sinon.spy();
// Test
await new Promise((resolve) => {
ref.on('value', (snapshot) => {
callbackA(snapshot.val());
resolve();
});
});
await new Promise((resolve) => {
ref.on('value', (snapshot) => {
callbackB(snapshot.val());
resolve();
});
});
callbackA.should.be.calledWith(currentDataValue);
callbackB.should.be.calledWith(currentDataValue);
// Tear down
ref.off();
});
});
it('calls callback with current values', () => {
return Promise.each(Object.keys(DatabaseContents.DEFAULT), (dataRef) => {
// Setup
const dataTypeValue = DatabaseContents.DEFAULT[dataRef];
const ref = firebase.native.database().ref(`tests/types/${dataRef}`);
// Test
return ref.on('value', (snapshot) => {
// Assertion
snapshot.val().should.eql(dataTypeValue);
// Tear down
ref.off();
});
});
});
it('errors if permission denied', () => {
return new Promise((resolve, reject) => {
const successCb = tryCatch(() => {
// Assertion
reject(new Error('No permission denied error'));
}, reject);
const failureCb = tryCatch((error) => {
// Assertion
error.message.includes('permission_denied').should.be.true();
resolve();
}, reject);
// Setup
const invalidRef = firebase.native.database().ref('nope');
// Test
invalidRef.on('value', successCb, failureCb);
});
});
});
}
export default onTests;

View File

@@ -0,0 +1,81 @@
import sinon from 'sinon';
import 'should-sinon';
import DatabaseContents from '../../support/DatabaseContents';
function onceTests({ describe, firebase, it, tryCatch }) {
describe('ref().once()', () => {
it('returns a promise', () => {
// Setup
const ref = firebase.native.database().ref('tests/types/number');
// Test
const returnValue = ref.once('value');
// Assertion
returnValue.should.be.Promise();
});
it('resolves with the correct value', async () => {
await Promise.map(Object.keys(DatabaseContents.DEFAULT), (dataRef) => {
// Setup
const dataTypeValue = DatabaseContents.DEFAULT[dataRef];
const ref = firebase.native.database().ref(`tests/types/${dataRef}`);
// Test
return ref.once('value').then((snapshot) => {
// Assertion
snapshot.val().should.eql(dataTypeValue);
});
});
});
it('is NOT called when the value is changed', async () => {
// Setup
const callback = sinon.spy();
const ref = firebase.native.database().ref('tests/types/number');
// Test
ref.once('value').then(callback);
await ref.set(DatabaseContents.NEW.number);
// Assertion
callback.should.be.calledOnce();
});
it('errors if permission denied', () => {
return new Promise((resolve, reject) => {
const successCb = tryCatch(() => {
// Assertion
reject(new Error('No permission denied error'));
}, reject);
const failureCb = tryCatch((error) => {
// Assertion
error.message.includes('permission_denied').should.be.true();
resolve();
}, reject);
// Setup
const reference = firebase.native.database().ref('nope');
// Test
reference.once('value', successCb, failureCb);
});
});
});
}
export default onceTests;

View File

@@ -0,0 +1,33 @@
function parentTests({ describe, context, it, firebase }) {
describe('ref().parent', () => {
context('on the root ref', () => {
it('returns null', () => {
// Setup
const ref = firebase.native.database().ref();
// Test
// Assertion
(ref.parent === null).should.be.true();
});
});
context('on a non-root ref', () => {
it('returns correct parent', () => {
// Setup
const ref = firebase.native.database().ref('tests/types/number');
const parentRef = firebase.native.database().ref('tests/types');
// Assertion
ref.parent.key.should.eql(parentRef.key);
});
});
});
}
export default parentTests;

View File

@@ -0,0 +1,113 @@
import sinon from 'sinon';
import 'should-sinon';
import DatabaseContents from '../../support/DatabaseContents';
function pushTests({ describe, it, firebase }) {
describe('ref().push()', () => {
it('returns a ref that can be used to set value later', async () => {
// Setup
const ref = firebase.native.database().ref('tests/types/array');
let originalListValue;
await ref.once('value', (snapshot) => {
originalListValue = snapshot.val();
});
originalListValue.should.eql(DatabaseContents.DEFAULT.array);
// Test
const newItemRef = ref.push();
const valueToAddToList = DatabaseContents.NEW.number;
await newItemRef.set(valueToAddToList);
let newItemValue,
newListValue;
// Assertion
await newItemRef.once('value', (snapshot) => {
newItemValue = snapshot.val();
});
newItemValue.should.eql(valueToAddToList);
await ref.once('value', (snapshot) => {
newListValue = snapshot.val();
});
const originalListAsObject = originalListValue.reduce((memo, value, index) => {
memo[index] = value;
return memo;
}, {});
originalListAsObject[newItemRef.key] = valueToAddToList;
newListValue.should.eql(originalListAsObject);
});
it('allows setting value immediately', async () => {
// Setup
const ref = firebase.native.database().ref('tests/types/array');
let originalListValue;
await ref.once('value', (snapshot) => {
originalListValue = snapshot.val();
});
// Test
const valueToAddToList = DatabaseContents.NEW.number;
const newItemRef = await ref.push(valueToAddToList);
let newItemValue,
newListValue;
// Assertion
await newItemRef.once('value', (snapshot) => {
newItemValue = snapshot.val();
});
newItemValue.should.eql(valueToAddToList);
await ref.once('value', (snapshot) => {
newListValue = snapshot.val();
});
const originalListAsObject = originalListValue.reduce((memo, value, index) => {
memo[index] = value;
return memo;
}, {});
originalListAsObject[newItemRef.key] = valueToAddToList;
newListValue.should.eql(originalListAsObject);
});
it('calls an onComplete callback', async () => {
// Setup
const callback = sinon.spy();
const ref = firebase.native.database().ref('tests/types/array');
// Test
const valueToAddToList = DatabaseContents.NEW.number;
await ref.push(valueToAddToList, callback);
// Assertion
callback.should.be.calledWith(null);
});
})
}
export default pushTests;

View File

@@ -0,0 +1,25 @@
import 'should-sinon';
import Promise from 'bluebird';
function queryTests({ describe, it, firebase, tryCatch }) {
describe('ref query', () => {
it('orderByChild().equalTo()', () => {
return new Promise((resolve, reject) => {
const successCb = tryCatch((snapshot) => {
const webVal = snapshot.val();
const ref = firebase.native.database().ref('tests/query');
ref.orderByChild('search').equalTo('foo').once('value', tryCatch((snapshot) => {
const nativeVal = snapshot.val();
nativeVal.should.eql(webVal);
resolve();
}, reject), reject);
}, reject);
firebase.web.database().ref('tests/query').orderByChild('search').equalTo('foo').once('value', successCb, reject);
});
});
});
}
export default queryTests;

View File

@@ -0,0 +1,15 @@
function refTests({ describe, it, firebase }) {
describe('ref().ref', () => {
it('returns a the reference itself', () => {
// Setup
const ref = firebase.native.database().ref();
// Assertion
ref.ref.should.eql(ref);
});
});
}
export default refTests;

View File

@@ -0,0 +1,44 @@
import DatabaseContents from '../../support/DatabaseContents';
function removeTests({ describe, it, firebase }) {
describe('ref().remove()', () => {
it('returns a promise', () => {
// Setup
const ref = firebase.native.database().ref('tests/types');
// Test
const returnValue = ref.remove({ number: DatabaseContents.DEFAULT.number });
// Assertion
returnValue.should.be.Promise();
});
it('sets value to null', async () => {
await Promise.map(Object.keys(DatabaseContents.DEFAULT), async (dataRef) => {
// Setup
const previousValue = DatabaseContents.DEFAULT[dataRef];
const ref = firebase.native.database().ref(`tests/types/${dataRef}`);
await ref.once('value').then((snapshot) => {
snapshot.val().should.eql(previousValue);
});
// Test
await ref.remove();
// Assertion
await ref.once('value').then((snapshot) => {
(snapshot.val() === null).should.be.true();
});
});
});
});
}
export default removeTests;

View File

@@ -0,0 +1,36 @@
function rootTests({ describe, it, context, firebase }) {
describe('ref().root', () => {
context('when called on a non-root reference', () => {
it('returns root ref', () => {
// Setup
const rootRef = firebase.native.database().ref();
const nonRootRef = firebase.native.database().ref('tests/types/number');
// Test
// Assertion
nonRootRef.root.should.eql(rootRef);
});
});
context('when called on the root reference', () => {
it('returns root ref', () => {
// Setup
const rootRef = firebase.native.database().ref();
// Test
// Assertion
rootRef.root.should.eql(rootRef);
});
});
});
}
export default rootTests;

View File

@@ -0,0 +1,73 @@
import DatabaseContents from '../../support/DatabaseContents';
function setTests({ describe, it, xit, firebase }) {
describe('ref.set()', () => {
xit('returns a promise', async () => {
// Setup
const ref = firebase.native.database().ref('tests/types/number');
// Test
const returnValue = ref.set(DatabaseContents.DEFAULT.number);
// Assertion
returnValue.should.be.Promise();
await returnValue.then((value) => {
(value === undefined).should.be.true();
});
});
it('changes value', async () => {
await Promise.map(Object.keys(DatabaseContents.DEFAULT), async (dataRef) => {
// Setup
const previousValue = DatabaseContents.DEFAULT[dataRef];
const ref = firebase.native.database().ref(`tests/types/${dataRef}`);
await ref.once('value').then((snapshot) => {
snapshot.val().should.eql(previousValue);
});
const newValue = DatabaseContents.NEW[dataRef];
// Test
await ref.set(newValue);
await ref.once('value').then((snapshot) => {
// Assertion
snapshot.val().should.eql(newValue);
});
});
});
it('can unset values', async () => {
await Promise.map(Object.keys(DatabaseContents.DEFAULT), async (dataRef) => {
// Setup
const previousValue = DatabaseContents.DEFAULT[dataRef];
const ref = firebase.native.database().ref(`tests/types/${dataRef}`);
await ref.once('value').then((snapshot) => {
snapshot.val().should.eql(previousValue);
});
// Test
await ref.set(null);
await ref.once('value').then((snapshot) => {
// Assertion
(snapshot.val() === null).should.be.true();
});
});
});
});
}
export default setTests;

View File

@@ -0,0 +1,53 @@
import Promise from 'bluebird';
function onTests({ describe, it, firebase, tryCatch }) {
describe('ref.transaction()', () => {
it('works', () => {
return new Promise((resolve, reject) => {
let valueBefore = 1;
firebase.native.database()
.ref('tests/transaction').transaction((currentData) => {
if (currentData === null) {
return valueBefore + 10;
}
valueBefore = currentData;
return valueBefore + 10;
}, tryCatch((error, committed, snapshot) => {
if (error) {
return reject(error);
}
if (!committed) {
return reject(new Error('Transaction did not commit.'));
}
snapshot.val().should.equal(valueBefore + 10);
return resolve();
}, reject), true);
});
});
it('aborts if undefined returned', () => {
return new Promise((resolve, reject) => {
firebase.native.database()
.ref('tests/transaction').transaction(() => {
return undefined;
}, (error, committed) => {
if (error) {
return reject(error);
}
if (!committed) {
return resolve();
}
return reject(new Error('Transaction did not abort commit.'));
}, true);
});
});
});
}
export default onTests;

View File

@@ -0,0 +1,112 @@
import Promise from 'bluebird';
import DatabaseContents from '../../support/DatabaseContents';
function updateTests({ describe, it, firebase }) {
describe('ref().update()', () => {
it('returns a promise', () => {
// Setup
const ref = firebase.native.database().ref('tests/types');
// Test
const returnValue = ref.update({ number: DatabaseContents.DEFAULT.number });
// Assertion
returnValue.should.be.Promise();
});
it('changes value', () => {
return Promise.each(Object.keys(DatabaseContents.DEFAULT), async (dataRef) => {
// Setup
const previousValue = DatabaseContents.DEFAULT[dataRef];
const ref = firebase.native.database().ref(`tests/types/${dataRef}`);
await ref.once('value').then((snapshot) => {
snapshot.val().should.eql(previousValue);
});
const newValue = DatabaseContents.NEW[dataRef];
const parentRef = firebase.native.database().ref('tests/types');
// Test
await parentRef.update({ [dataRef]: newValue });
// Assertion
await ref.once('value').then((snapshot) => {
snapshot.val().should.eql(newValue);
});
});
});
it('can unset values', () => {
return Promise.each(Object.keys(DatabaseContents.DEFAULT), async (dataRef) => {
// Setup
const previousValue = DatabaseContents.DEFAULT[dataRef];
const ref = firebase.native.database().ref(`tests/types/${dataRef}`);
await ref.once('value').then((snapshot) => {
snapshot.val().should.eql(previousValue);
});
const parentRef = firebase.native.database().ref('tests/types');
// Test
await parentRef.update({ [dataRef]: null });
// Assertion
await ref.once('value').then((snapshot) => {
(snapshot.val() === null).should.be.true();
});
});
});
it('updates multiple values at once', async () => {
// Setup
const numberPreviousValue = DatabaseContents.DEFAULT.number;
const stringPreviousValue = DatabaseContents.DEFAULT.string;
const numberRef = firebase.native.database().ref('tests/types/number');
const stringRef = firebase.native.database().ref('tests/types/string');
await numberRef.once('value').then((snapshot) => {
snapshot.val().should.eql(numberPreviousValue);
});
await stringRef.once('value').then((snapshot) => {
snapshot.val().should.eql(stringPreviousValue);
});
const numberNewValue = DatabaseContents.NEW.number;
const stringNewValue = DatabaseContents.NEW.string;
const parentRef = firebase.native.database().ref('tests/types');
// Test
await parentRef.update({
number: numberNewValue,
string: stringNewValue,
});
// Assertion
await numberRef.once('value').then((snapshot) => {
snapshot.val().should.eql(numberNewValue);
});
await stringRef.once('value').then((snapshot) => {
snapshot.val().should.eql(stringNewValue);
});
});
});
}
export default updateTests;

View File

@@ -0,0 +1,116 @@
export default function addTests({ tryCatch, describe, it, firebase }) {
describe('Snapshot', () => {
it('should provide a functioning val() method', () => {
return new Promise((resolve, reject) => {
const successCb = tryCatch((snapshot) => {
snapshot.val.should.be.a.Function();
snapshot.val().should.eql([
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
]);
resolve();
}, reject);
firebase.native.database().ref('tests/types/array').once('value', successCb, reject);
});
});
it('should provide a functioning child() method', () => {
return new Promise((resolve, reject) => {
const successCb = tryCatch((snapshot) => {
snapshot.child('0').val.should.be.a.Function();
snapshot.child('0').val().should.equal(0);
snapshot.child('0').key.should.be.a.String();
snapshot.child('0').key.should.equal('0');
resolve();
}, reject);
firebase.native.database().ref('tests/types/array').once('value', successCb, reject);
});
});
it('should provide a functioning hasChild() method', () => {
return new Promise((resolve, reject) => {
const successCb = tryCatch((snapshot) => {
snapshot.hasChild.should.be.a.Function();
snapshot.hasChild('foo').should.equal(true);
snapshot.hasChild('baz').should.equal(false);
resolve();
}, reject);
firebase.native.database().ref('tests/types/object').once('value', successCb, reject);
});
});
it('should provide a functioning hasChildren() method', () => {
return new Promise((resolve, reject) => {
const successCb = tryCatch((snapshot) => {
snapshot.hasChildren.should.be.a.Function();
snapshot.hasChildren().should.equal(true);
snapshot.child('foo').hasChildren().should.equal(false);
resolve();
}, reject);
firebase.native.database().ref('tests/types/object').once('value', successCb, reject);
});
});
it('should provide a functioning exists() method', () => {
return new Promise((resolve, reject) => {
const successCb = tryCatch((snapshot) => {
snapshot.exists.should.be.a.Function();
snapshot.exists().should.equal(false);
resolve();
}, reject);
firebase.native.database().ref('tests/types/object/baz/daz').once('value', successCb, reject);
});
});
it('should provide a functioning getPriority() method', () => {
return new Promise((resolve, reject) => {
const successCb = tryCatch((snapshot) => {
snapshot.getPriority.should.be.a.Function();
snapshot.getPriority().should.equal(666);
snapshot.val().should.eql({ foo: 'bar' });
resolve();
}, reject);
const ref = firebase.native.database().ref('tests/priority');
ref.once('value', successCb, reject);
});
});
it('should provide a functioning forEach() method', () => {
// TODO this doesn't really test that the key order returned is in correct order
return new Promise((resolve, reject) => {
const successCb = tryCatch((snapshot) => {
let total = 0;
snapshot.forEach.should.be.a.Function();
snapshot.forEach((childSnapshot) => {
const val = childSnapshot.val();
total = total + val;
return val === 3; // stop iteration after key 3
});
total.should.equal(6); // 0 + 1 + 2 + 3 = 6
resolve();
}, reject);
firebase.native.database().ref('tests/types/array').once('value', successCb, reject);
});
});
it('should provide a key property', () => {
return new Promise((resolve, reject) => {
const successCb = tryCatch((snapshot) => {
snapshot.key.should.be.a.String();
snapshot.key.should.equal('array');
resolve();
}, reject);
firebase.native.database().ref('tests/types/array').once('value', successCb, reject);
});
});
});
}

216
tests/src/tests/index.js Normal file
View File

@@ -0,0 +1,216 @@
import { setSuiteStatus, setTestStatus } from '../actions/TestActions';
import analytics from './analytics/index';
import crash from './crash/index';
import database from './database/index';
import messaging from './messaging/index';
import storage from './storage/index';
import auth from './auth/index';
const testSuiteInstances = [
database,
auth,
analytics,
messaging,
crash,
storage,
];
/*
A map of test suite instances to their ids so they may be retrieved
at run time and called upon to run individual tests
*/
const testSuiteRunners = {};
/*
Attributes to hold initial Redux store state
*/
const testSuites = {};
const tests = {};
const focusedTestIds = {};
const pendingTestIds = {};
const testContexts = {};
/**
* @typedef {number} TestId
* @typedef {number} TestSuiteId
*
* @typedef {Object} Test
* @property {number} id
* @property {number} testSuiteId
*
* @typedef {Object} TestSuite
* @property {number} id
* @property {TestId[]} testIds
*
* @typedef {Object.<TestId,Test>} IndexedTestGroup
* @typedef {Object.<TestSuiteId,TestSuite>} IndexedTestSuiteGroup
* @typedef {Object.<TestId,bool>} IdLookup
*/
/**
* Return initial state for the tests to provide to Redux
* @returns {{suites: {}, descriptions: {}}}
*/
export function initialState() {
testSuiteInstances.forEach((testSuite) => {
const { id, name, description } = testSuite;
// Add to test suite runners for later recall
testSuiteRunners[testSuite.id] = testSuite;
const testDefinitions = testSuite.testDefinitions;
// Add to test suites to place in the redux store
testSuites[testSuite.id] = {
id,
name,
description,
testContextIds: Object.keys(testDefinitions.testContexts),
testIds: Object.keys(testDefinitions.tests),
status: null,
message: null,
time: 0,
progress: 0,
};
Object.assign(tests, testDefinitions.tests);
Object.assign(testContexts, testDefinitions.testContexts);
Object.assign(focusedTestIds, testDefinitions.focusedTestIds);
Object.assign(pendingTestIds, testDefinitions.pendingTestIds);
});
return {
tests,
testSuites,
testContexts,
focusedTestIds,
pendingTestIds,
};
}
/**
* Provide a redux store to the test suites
* @param store
*/
export function setupSuites(store) {
Object.values(testSuiteRunners).forEach((testSuite) => {
// eslint-disable-next-line no-param-reassign
testSuite.setStore(store, (action) => {
store.dispatch(setSuiteStatus(action));
}, (action) => {
store.dispatch(setTestStatus(action));
});
});
}
/**
* Run a single test by id, ignoring whether it's pending or focused.
* @param {number} testId - id of test to run
*/
export function runTest(testId) {
const test = tests[testId];
runTests({ [testId]: test });
}
/**
* Run all tests in all test suites. If testIds is provided, only run the tests
* that match the ids included.
* @params {IndexedTestGroup} testGroup - Group of tests to run
* @param {Object=} options - options limiting which tests should be run
* @param {IdLookup} options.pendingTestIds - map of ids of pending tests
* @param {IdLookup} options.focusedTestIds - map of ids of focused tests
*/
export function runTests(testGroup, options = { pendingTestIds: {}, focusedTestIds: {} }) {
const areFocusedTests = Object.keys(options.focusedTestIds).length > 0;
if (areFocusedTests) {
runOnlyTestsInLookup(testGroup, options.focusedTestIds);
} else {
const arePendingTests = Object.keys(options.pendingTestIds).length > 0;
if (arePendingTests) {
runAllButTestsInLookup(testGroup, options.pendingTestIds);
} else {
const testsBySuiteId = getTestsBySuiteId(testGroup);
runTestsBySuiteId(testsBySuiteId);
}
}
}
/**
* Runs all tests listed in tests, except those with ids matching values in
* testLookup
* @param {IndexedTestGroup} testGroup - complete list of tests
* @param {IdLookup} testLookup - id lookup of pending tests
*/
function runAllButTestsInLookup(testGroup, testLookup) {
const testsToRunBySuiteId = Object.keys(testGroup).reduce((memo, testId) => {
const testIsNotPending = !testLookup[testId];
if (testIsNotPending) {
const test = testGroup[testId];
// eslint-disable-next-line no-param-reassign
memo[test.testSuiteId] = memo[test.testSuiteId] || [];
memo[test.testSuiteId].push(testId);
}
return memo;
}, {});
Promise.each(Object.keys(testsToRunBySuiteId), (testSuiteId) => {
const testIds = testsToRunBySuiteId[testSuiteId];
return runSuite(testSuiteId, testIds);
});
}
/**
* Runs only the tests listed in focused tests
* @param {IndexedTestGroup} testGroup - complete list of tests
* @param {IdLookup} testLookup - id lookup of focused tests
*/
function runOnlyTestsInLookup(testGroup, testLookup) {
const testsInLookupBySuiteId = getTestsBySuiteId(testGroup, testLookup);
runTestsBySuiteId(testsInLookupBySuiteId);
}
function runTestsBySuiteId(suiteIdTests) {
Promise.each(Object.keys(suiteIdTests), (testSuiteId) => {
const testIds = suiteIdTests[testSuiteId];
return runSuite(testSuiteId, testIds);
});
}
/**
* Run tests in a suite. If testIds is provided, only run the tests that match the
* ids included.
* @param {number} testSuiteId - Id of test suite to run
* @param {number[]=} testIds - array of test ids to run from the test suite
*/
function runSuite(testSuiteId, testIds = null) {
const testSuiteRunner = testSuiteRunners[testSuiteId];
if (testSuiteRunner) {
return testSuiteRunner.run(testIds);
}
console.error(`runSuite: Suite with id "${testSuiteId}" not found`);
return Promise.reject();
}
function getTestsBySuiteId(testGroup, testLookup = testGroup) {
return Object.keys(testLookup).reduce((memo, testId) => {
const test = testGroup[testId];
if (test) {
// eslint-disable-next-line no-param-reassign
memo[test.testSuiteId] = memo[test.testSuiteId] || [];
memo[test.testSuiteId].push(testId);
}
return memo;
}, {});
}

View File

@@ -0,0 +1,10 @@
import firebase from '../../firebase';
import TestSuite from '../../../lib/TestSuite';
import messagingTests from './messagingTests';
const suite = new TestSuite('Messaging', 'firebase.messaging()', firebase);
suite.addTests(messagingTests);
export default suite;

View File

@@ -0,0 +1,149 @@
function messagingTests({ describe, it, firebase }) {
describe('FCM', () => {
it('it should build a RemoteMessage', () => {
const remoteMessage = new firebase.native.messaging.RemoteMessage('305229645282');
// all optional
remoteMessage.setId('foobar');
remoteMessage.setTtl(12000);
remoteMessage.setType('something');
remoteMessage.setData({
object: { foo: 'bar ' },
array: [1, 2, 3, 4, 5],
string: 'hello',
boolean: true,
number: 123456,
});
// return json object so we can assert values
const mOutput = remoteMessage.toJSON();
mOutput.id.should.equal('foobar');
mOutput.ttl.should.equal(12000);
mOutput.type.should.equal('something');
mOutput.data.should.be.a.Object();
// all data types should be a string as this is all that native accepts
mOutput.data.object.should.equal('[object Object]');
mOutput.data.array.should.equal('1,2,3,4,5');
mOutput.data.string.should.equal('hello');
mOutput.data.number.should.equal('123456');
return Promise.resolve();
});
it('should send a RemoteMessage', () => {
const remoteMessage = new firebase.native.messaging.RemoteMessage('305229645282');
// all optional
remoteMessage.setId('foobar');
remoteMessage.setTtl(12000);
remoteMessage.setType('something');
remoteMessage.setData({
object: { foo: 'bar ' },
array: [1, 2, 3, 4, 5],
string: 'hello',
number: 123456,
});
firebase.native.messaging().send(remoteMessage);
return Promise.resolve();
});
it('it should return fcm token from getToken', () => {
const successCb = (token) => {
console.log(token);
token.should.be.a.String();
return Promise.resolve();
};
return firebase.native.messaging()
.getToken()
.then(successCb);
});
it('it should build a RemoteMessage', () => {
const remoteMessage = new firebase.native.messaging.RemoteMessage('305229645282');
// all optional
remoteMessage.setId('foobar');
remoteMessage.setTtl(12000);
remoteMessage.setType('something');
remoteMessage.setData({
object: { foo: 'bar ' },
array: [1, 2, 3, 4, 5],
string: 'hello',
boolean: true,
number: 123456,
});
// return json object so we can assert values
const mOutput = remoteMessage.toJSON();
mOutput.id.should.equal('foobar');
mOutput.ttl.should.equal(12000);
mOutput.type.should.equal('something');
mOutput.data.should.be.a.Object();
// all data types should be a string as this is all that native accepts
mOutput.data.object.should.equal('[object Object]');
mOutput.data.array.should.equal('1,2,3,4,5');
mOutput.data.string.should.equal('hello');
mOutput.data.number.should.equal('123456');
return Promise.resolve();
});
it('it should send a RemoteMessage', () => {
const remoteMessage = new firebase.native.messaging.RemoteMessage('305229645282');
// all optional
remoteMessage.setId('foobar');
remoteMessage.setTtl(12000);
remoteMessage.setType('something');
remoteMessage.setData({
object: { foo: 'bar ' },
array: [1, 2, 3, 4, 5],
string: 'hello',
number: 123456,
});
firebase.native.messaging().send(remoteMessage);
return Promise.resolve();
});
it('it should create/remove onTokenRefresh listeners', () => {
const cb = () => {
};
try {
const listener = firebase.native.messaging().onTokenRefresh(cb);
listener.remove();
} catch (e) {
console.error(e);
}
return Promise.resolve();
});
it('it should subscribe/unsubscribe to topics', () => {
firebase.native.messaging().subscribeToTopic('foobar');
firebase.native.messaging().unsubscribeFromTopic('foobar');
return Promise.resolve();
});
it('it should show a notification', () => {
firebase.native.messaging().createLocalNotification({
title: 'Hello',
body: 'My Notification Message',
big_text: "Is it me you're looking for?",
sub_text: 'nope',
show_in_foreground: true,
});
return Promise.resolve();
});
});
}
export default messagingTests;

View File

@@ -0,0 +1,10 @@
import firebase from '../../firebase';
import TestSuite from '../../../lib/TestSuite';
import storageTests from './storageTests';
const suite = new TestSuite('Storage', 'Upload/Download storage tests', firebase);
suite.addTests(storageTests);
export default suite;

View File

@@ -0,0 +1,90 @@
function storageTests({ xdescribe, it, firebase, tryCatch }) {
xdescribe('downloadFile()', () => {
it('it should error on download file if permission denied', () => {
return new Promise((resolve, reject) => {
const successCb = tryCatch(() => {
reject(new Error('No permission denied error'));
}, reject);
const failureCb = tryCatch((error) => {
error.code.should.equal('storage/unauthorized');
error.message.includes('not authorized').should.be.true();
resolve();
}, reject);
firebase.native.storage().ref('/not.jpg').downloadFile(`${firebase.native.storage.Native.DOCUMENT_DIRECTORY_PATH}/not.jpg`).then(successCb).catch(failureCb);
});
});
it('it should download a file', () => {
return new Promise((resolve, reject) => {
const successCb = tryCatch((meta) => {
meta.state.should.eql(firebase.native.storage.TaskState.SUCCESS);
meta.bytesTransferred.should.eql(meta.totalBytes);
resolve();
}, reject);
const failureCb = tryCatch((error) => {
reject(error);
}, reject);
firebase.native.storage().ref('/ok.jpeg').downloadFile(`${firebase.native.storage.Native.DOCUMENT_DIRECTORY_PATH}/ok.jpeg`).then(successCb).catch(failureCb);
});
});
});
xdescribe('putFile()', () => {
it('it should error on upload if permission denied', () => {
return new Promise((resolve, reject) => {
const successCb = tryCatch(() => {
reject(new Error('No permission denied error'));
}, reject);
const failureCb = tryCatch((error) => {
error.code.should.equal('storage/unauthorized');
error.message.includes('not authorized').should.be.true();
resolve();
}, reject);
firebase.native.storage().ref('/uploadNope.jpeg').putFile(`${firebase.native.storage.Native.DOCUMENT_DIRECTORY_PATH}/ok.jpeg`).then(successCb).catch(failureCb);
});
});
it('it should upload a file', () => {
return new Promise((resolve, reject) => {
const successCb = tryCatch((uploadTaskSnapshot) => {
uploadTaskSnapshot.state.should.eql(firebase.native.storage.TaskState.SUCCESS);
uploadTaskSnapshot.bytesTransferred.should.eql(uploadTaskSnapshot.totalBytes);
uploadTaskSnapshot.metadata.should.be.an.Object();
uploadTaskSnapshot.downloadUrl.should.be.a.String();
resolve();
}, reject);
const failureCb = tryCatch((error) => {
reject(error);
}, reject);
firebase.native.storage().ref('/uploadOk.jpeg').putFile(`${firebase.native.storage.Native.DOCUMENT_DIRECTORY_PATH}/ok.jpeg`).then(successCb).catch(failureCb);
});
});
});
xdescribe('on()', () => {
it('should listen to upload state', () => {
return new Promise((resolve, reject) => {
const path = `${firebase.native.storage.Native.DOCUMENT_DIRECTORY_PATH}/ok.jpeg`;
const ref = firebase.native.storage().ref('/uploadOk.jpeg');
const unsubscribe = ref.putFile(path).on(firebase.native.storage.TaskEvent.STATE_CHANGED, tryCatch((snapshot) => {
if (snapshot.state === firebase.native.storage.TaskState.SUCCESS) {
resolve();
}
}, reject), tryCatch((error) => {
unsubscribe();
reject(error);
}, reject));
});
});
});
}
export default storageTests;

View File

@@ -0,0 +1,25 @@
export default {
DEFAULT: {
array: [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
],
boolean: true,
string: 'foobar',
number: 123567890,
object: {
foo: 'bar',
},
},
NEW: {
array: [
9, 8, 7, 6, 5, 4,
],
boolean: false,
string: 'baz',
number: 84564564,
object: {
foo: 'baz',
},
},
};

View File

@@ -0,0 +1,10 @@
import DatabaseContents from './DatabaseContents';
const databaseTypeMap =
Object.keys(DatabaseContents.DEFAULT).reduce((dataTypeMap, dataType) => {
// eslint-disable-next-line no-param-reassign
dataTypeMap[`tests/types/${dataType}`] = DatabaseContents.DEFAULT[dataType];
return dataTypeMap;
}, {});
export default databaseTypeMap;