+
+
+
+ {feedbackTree.children.map(child =>
+ ()
+ )}
+
+
+ );
+};
+
+const mapStateToProps = (state, props) => ({
+ // eslint-disable-next-line no-undefined
+ opened: state.scratchGui.testResults.openedMap[props.id] === undefined ?
+ // open failed groups by default
+ !props.feedbackTree.groupPassed : state.scratchGui.testResults.openedMap[props.id]
+
+});
+
+const mapDispatchToProps = dispatch => ({
+ setOpened: (opened, nodeId) => dispatch(setOpened(opened, nodeId))
+});
+
+FeedbackTreeComponent.propTypes = {
+ opened: PropTypes.bool.isRequired,
+ setOpened: PropTypes.func.isRequired,
+ feedbackTree: PropTypes.object.isRequired,
+ id: PropTypes.string.isRequired
+};
+
+const ConnectedFeedbackTreeComponent = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(FeedbackTreeComponent);
+
+export default TestResultTabComponent;
diff --git a/src/containers/controls.jsx b/src/containers/controls.jsx
index 036050a00ec..9b0493a3ad7 100644
--- a/src/containers/controls.jsx
+++ b/src/containers/controls.jsx
@@ -13,6 +13,7 @@ class Controls extends React.Component {
bindAll(this, [
'handleDebugModeClick',
'handleGreenFlagClick',
+ 'handleTestFlagClick',
'handlePauseClick',
'handleResumeClick',
'handleRewindModeClick',
@@ -49,6 +50,11 @@ class Controls extends React.Component {
}
}
+ handleTestFlagClick (e) {
+ e.preventDefault();
+ this.props.vm.testFlag();
+ }
+
handlePauseClick (e) {
e.preventDefault();
if (this.props.projectRunning && !this.props.paused) {
@@ -109,6 +115,7 @@ class Controls extends React.Component {
turbo={turbo}
onDebugModeClick={this.handleDebugModeClick}
onGreenFlagClick={this.handleGreenFlagClick}
+ onTestFlagClick={this.handleTestFlagClick}
onPauseClick={this.handlePauseClick}
onResumeClick={this.handleResumeClick}
onRewindModeClick={this.handleRewindModeClick}
diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx
index ee41ee80103..5fde6c80618 100644
--- a/src/containers/gui.jsx
+++ b/src/containers/gui.jsx
@@ -16,7 +16,8 @@ import {
BLOCKS_TAB_INDEX,
COSTUMES_TAB_INDEX,
SOUNDS_TAB_INDEX,
- DEBUGGER_TAB_INDEX
+ DEBUGGER_TAB_INDEX,
+ TEST_RESULT_TAB_INDEX
} from '../reducers/editor-tab';
import {
@@ -139,6 +140,7 @@ const mapStateToProps = state => {
costumeLibraryVisible: state.scratchGui.modals.costumeLibrary,
costumesTabVisible: state.scratchGui.editorTab.activeTabIndex === COSTUMES_TAB_INDEX,
debuggerTabVisible: state.scratchGui.editorTab.activeTabIndex === DEBUGGER_TAB_INDEX,
+ testResultTabVisible: state.scratchGui.editorTab.activeTabIndex === TEST_RESULT_TAB_INDEX,
error: state.scratchGui.projectState.error,
isError: getIsError(loadingState),
isFullScreen: state.scratchGui.mode.isFullScreen,
@@ -164,6 +166,7 @@ const mapDispatchToProps = dispatch => ({
onActivateCostumesTab: () => dispatch(activateTab(COSTUMES_TAB_INDEX)),
onActivateSoundsTab: () => dispatch(activateTab(SOUNDS_TAB_INDEX)),
onActivateDebuggerTab: () => dispatch(activateTab(DEBUGGER_TAB_INDEX)),
+ onActivateTestResultTab: () => dispatch(activateTab(TEST_RESULT_TAB_INDEX)),
onRequestCloseBackdropLibrary: () => dispatch(closeBackdropLibrary()),
onRequestCloseCostumeLibrary: () => dispatch(closeCostumeLibrary()),
onRequestCloseTelemetryModal: () => dispatch(closeTelemetryModal())
diff --git a/src/containers/test-result-tab.jsx b/src/containers/test-result-tab.jsx
new file mode 100644
index 00000000000..ecebddc03ae
--- /dev/null
+++ b/src/containers/test-result-tab.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import TestResultTabComponent from '../components/test-result-tab/test-result-tab.jsx';
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+import VM from 'scratch-vm';
+
+class TestResultTab extends React.Component {
+ constructor (props) {
+ super(props);
+
+ bindAll(this, [
+ 'getTestResults'
+ ]);
+ }
+
+ getTestResults () {
+ return this.props.vm.runtime.getTestResults();
+ }
+
+ render () {
+ return (
+
+ );
+ }
+}
+
+
+TestResultTab.propTypes = {
+ vm: PropTypes.instanceOf(VM).isRequired
+};
+
+export default TestResultTab;
diff --git a/src/lib/libraries/extensions/bats/bats.png b/src/lib/libraries/extensions/bats/bats.png
new file mode 100644
index 00000000000..5825b4def66
Binary files /dev/null and b/src/lib/libraries/extensions/bats/bats.png differ
diff --git a/src/lib/libraries/extensions/bats/icon--test-flag.svg b/src/lib/libraries/extensions/bats/icon--test-flag.svg
new file mode 100644
index 00000000000..3cddd707470
--- /dev/null
+++ b/src/lib/libraries/extensions/bats/icon--test-flag.svg
@@ -0,0 +1 @@
+
diff --git a/src/lib/libraries/extensions/index.jsx b/src/lib/libraries/extensions/index.jsx
index 41fa6de5fad..16023759f86 100644
--- a/src/lib/libraries/extensions/index.jsx
+++ b/src/lib/libraries/extensions/index.jsx
@@ -49,6 +49,9 @@ import gdxforConnectionSmallIconURL from './gdxfor/gdxfor-small.svg';
import debuggerIconURL from './debugger/debugger.png';
import debuggerInsetIconURL from './debugger/debugger-small.svg';
+import batsIconURL from './bats/bats.png';
+import batsInsetIconURL from './bats/icon--test-flag.svg';
+
export default [
{
name: (
@@ -340,5 +343,25 @@ export default [
/>
),
featured: true
+ },
+ {
+ name: (
+
+ ),
+ extensionId: 'itch',
+ iconURL: batsIconURL,
+ insetIconURL: batsInsetIconURL,
+ description: (
+
+ ),
+ featured: true
}
];
diff --git a/src/reducers/editor-tab.js b/src/reducers/editor-tab.js
index eb354850c63..6693a1b48eb 100644
--- a/src/reducers/editor-tab.js
+++ b/src/reducers/editor-tab.js
@@ -4,7 +4,8 @@ const ACTIVATE_TAB = 'scratch-gui/navigation/ACTIVATE_TAB';
const BLOCKS_TAB_INDEX = 0;
const COSTUMES_TAB_INDEX = 1;
const SOUNDS_TAB_INDEX = 2;
-const DEBUGGER_TAB_INDEX = 3;
+const TEST_RESULT_TAB_INDEX = 3;
+const DEBUGGER_TAB_INDEX = 4;
const initialState = {
activeTabIndex: BLOCKS_TAB_INDEX
@@ -36,5 +37,6 @@ export {
BLOCKS_TAB_INDEX,
COSTUMES_TAB_INDEX,
SOUNDS_TAB_INDEX,
+ TEST_RESULT_TAB_INDEX,
DEBUGGER_TAB_INDEX
};
diff --git a/src/reducers/gui.js b/src/reducers/gui.js
index 1f1d8e839e4..a46f59694b7 100644
--- a/src/reducers/gui.js
+++ b/src/reducers/gui.js
@@ -22,6 +22,7 @@ import fontsLoadedReducer, {fontsLoadedInitialState} from './fonts-loaded';
import restoreDeletionReducer, {restoreDeletionInitialState} from './restore-deletion';
import stageSizeReducer, {stageSizeInitialState} from './stage-size';
import targetReducer, {targetsInitialState} from './targets';
+import testResultsReducer, {testResultsInitialState} from './test-results';
import timeoutReducer, {timeoutInitialState} from './timeout';
import toolboxReducer, {toolboxInitialState} from './toolbox';
import vmReducer, {vmInitialState} from './vm';
@@ -57,6 +58,7 @@ const guiInitialState = {
fontsLoaded: fontsLoadedInitialState,
restoreDeletion: restoreDeletionInitialState,
targets: targetsInitialState,
+ testResults: testResultsInitialState,
timeout: timeoutInitialState,
toolbox: toolboxInitialState,
vm: vmInitialState,
@@ -158,6 +160,7 @@ const guiReducer = combineReducers({
fontsLoaded: fontsLoadedReducer,
restoreDeletion: restoreDeletionReducer,
targets: targetReducer,
+ testResults: testResultsReducer,
timeout: timeoutReducer,
toolbox: toolboxReducer,
vm: vmReducer,
diff --git a/src/reducers/test-results.js b/src/reducers/test-results.js
new file mode 100644
index 00000000000..7e71c2d4f0b
--- /dev/null
+++ b/src/reducers/test-results.js
@@ -0,0 +1,34 @@
+const SET_OPENED = 'scratch-gui/test-results/SET_OPENED';
+
+const initialState = {
+ openedMap: {} // map of nodeId to boolean
+};
+
+const reducer = function (state, action) {
+ if (typeof state === 'undefined') state = initialState;
+ switch (action.type) {
+ case SET_OPENED:
+ return Object.assign({}, state, {
+ // change the value of the node id to action.opened
+ openedMap: Object.assign({}, state.openedMap, {
+ [action.nodeId]: action.opened
+ })
+ });
+ default:
+ return state;
+ }
+};
+
+const setOpened = function (value, nodeId) {
+ return {
+ type: SET_OPENED,
+ opened: value,
+ nodeId: nodeId
+ };
+};
+
+export {
+ reducer as default,
+ initialState as testResultsInitialState,
+ setOpened
+};