Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 10 additions & 41 deletions packages/scratch-gui/src/lib/vm-listener-hoc.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import VM from '@scratch/scratch-vm';
import VM from 'scratch-vm';

import {connect} from 'react-redux';

Expand All @@ -10,7 +10,7 @@ import {updateBlockDrag} from '../reducers/block-drag';
import {updateMonitors} from '../reducers/monitors';
import {setProjectChanged, setProjectUnchanged} from '../reducers/project-changed';
import {setRunningState, setTurboState, setStartedState} from '../reducers/vm-status';
import {showExtensionAlert, showStandardAlert, closeAlertWithId} from '../reducers/alerts';
import {showExtensionAlert} from '../reducers/alerts';
import {updateMicIndicator} from '../reducers/mic-indicator';

/*
Expand Down Expand Up @@ -46,7 +46,6 @@ const vmListenerHOC = function (WrappedComponent) {
this.props.vm.on('PROJECT_START', this.props.onGreenFlag);
this.props.vm.on('PERIPHERAL_CONNECTION_LOST_ERROR', this.props.onShowExtensionAlert);
this.props.vm.on('MIC_LISTENING', this.props.onMicListeningUpdate);
this.props.vm.on('EXTENSION_DATA_LOADING', this.props.onExtensionDataLoading);

}
componentDidMount () {
Expand All @@ -68,20 +67,7 @@ const vmListenerHOC = function (WrappedComponent) {
}
}
componentWillUnmount () {
this.props.vm.removeListener('targetsUpdate', this.handleTargetsUpdate);
this.props.vm.removeListener('MONITORS_UPDATE', this.props.onMonitorsUpdate);
this.props.vm.removeListener('BLOCK_DRAG_UPDATE', this.props.onBlockDragUpdate);
this.props.vm.removeListener('TURBO_MODE_ON', this.props.onTurboModeOn);
this.props.vm.removeListener('TURBO_MODE_OFF', this.props.onTurboModeOff);
this.props.vm.removeListener('PROJECT_RUN_START', this.props.onProjectRunStart);
this.props.vm.removeListener('PROJECT_RUN_STOP', this.props.onProjectRunStop);
this.props.vm.removeListener('PROJECT_CHANGED', this.handleProjectChanged);
this.props.vm.removeListener('RUNTIME_STARTED', this.props.onRuntimeStarted);
this.props.vm.removeListener('PROJECT_START', this.props.onGreenFlag);
this.props.vm.removeListener('PERIPHERAL_CONNECTION_LOST_ERROR', this.props.onShowExtensionAlert);
this.props.vm.removeListener('MIC_LISTENING', this.props.onMicListeningUpdate);
this.props.vm.removeListener('EXTENSION_DATA_LOADING', this.props.onExtensionDataLoading);

if (this.props.attachKeyboardEvents) {
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
Expand All @@ -98,14 +84,9 @@ const vmListenerHOC = function (WrappedComponent) {
}
}
handleKeyDown (e) {
// Don't capture keys intended for HTML inputs (e.g. project title).
// The Blockly workspace is rendered as SVG, so SVG-targeted events
// should always reach the VM for key-sensing — even when a block has
// Blockly focus — so that game controls are never silently dropped
// while the user is on the Code tab.
if (e.target !== document && e.target !== document.body) {
if (!(e.target instanceof SVGElement)) return;
}
// Don't capture keys intended for Blockly inputs.
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement ||
e.target.isContentEditable) return;

const key = (!e.key || e.key === 'Dead') ? e.keyCode : e.key;
this.props.vm.postIOData('keyboard', {
Expand Down Expand Up @@ -135,7 +116,7 @@ const vmListenerHOC = function (WrappedComponent) {
}
render () {
const {

/* eslint-disable no-unused-vars */
attachKeyboardEvents,
projectChanged,
shouldUpdateTargets,
Expand All @@ -145,7 +126,6 @@ const vmListenerHOC = function (WrappedComponent) {
onKeyDown,
onKeyUp,
onMicListeningUpdate,
onExtensionDataLoading,
onMonitorsUpdate,
onTargetsUpdate,
onProjectChanged,
Expand All @@ -156,7 +136,7 @@ const vmListenerHOC = function (WrappedComponent) {
onTurboModeOff,
onTurboModeOn,
onShowExtensionAlert,

/* eslint-enable no-unused-vars */
...props
} = this.props;
return <WrappedComponent {...props} />;
Expand All @@ -165,7 +145,6 @@ const vmListenerHOC = function (WrappedComponent) {
VMListener.propTypes = {
attachKeyboardEvents: PropTypes.bool,
onBlockDragUpdate: PropTypes.func.isRequired,
onExtensionDataLoading: PropTypes.func.isRequired,
onGreenFlag: PropTypes.func,
onKeyDown: PropTypes.func,
onKeyUp: PropTypes.func,
Expand All @@ -190,7 +169,7 @@ const vmListenerHOC = function (WrappedComponent) {
attachKeyboardEvents: true,
onGreenFlag: () => ({})
};
const mapStateToProps = (state, ownProps) => ({
const mapStateToProps = state => ({
projectChanged: state.scratchGui.projectChanged,
// Do not emit target or project updates in fullscreen or player only mode
// or when recording sounds (it leads to garbled recordings on low-power machines)
Expand All @@ -199,11 +178,8 @@ const vmListenerHOC = function (WrappedComponent) {
// Do not update the projectChanged state in fullscreen or player only mode
shouldUpdateProjectChanged: !state.scratchGui.mode.isFullScreen && !state.scratchGui.mode.isPlayerOnly,
vm: state.scratchGui.vm,
username: ownProps.username ?? (
state.session && state.session.session && state.session.session.user ?
state.session.session.user.username :
''
)
username: state.session && state.session.session && state.session.session.user ?
state.session.session.user.username : ''
});
const mapDispatchToProps = dispatch => ({
onTargetsUpdate: data => {
Expand All @@ -227,13 +203,6 @@ const vmListenerHOC = function (WrappedComponent) {
},
onMicListeningUpdate: listening => {
dispatch(updateMicIndicator(listening));
},
onExtensionDataLoading: loading => {
if (loading) {
dispatch(showStandardAlert('loadingExtensionData'));
} else {
dispatch(closeAlertWithId('loadingExtensionData'));
}
}
});
return connect(
Expand Down
41 changes: 16 additions & 25 deletions packages/scratch-gui/test/unit/util/vm-listener-hoc.test.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React from 'react';
import configureStore from 'redux-mock-store';
import {render} from '@testing-library/react';
import VM from '@scratch/scratch-vm';
import {mount} from 'enzyme';
import VM from 'scratch-vm';

import vmListenerHOC from '../../../src/lib/vm-listener-hoc.jsx';
import '@testing-library/jest-dom';

describe('VMListenerHOC', () => {
const mockStore = configureStore();
Expand All @@ -26,7 +25,7 @@ describe('VMListenerHOC', () => {
const Component = () => (<div />);
const WrappedComponent = vmListenerHOC(Component);
const onGreenFlag = jest.fn();
render(
mount(
<WrappedComponent
store={store}
vm={vm}
Expand All @@ -39,28 +38,23 @@ describe('VMListenerHOC', () => {
});

test('onGreenFlag is not passed to the children', () => {
const Component = ({onGreenFlag}) => (
<div id="onGreenFlag">{`${onGreenFlag ?
onGreenFlag() :
onGreenFlag
}`}</div>
);
const Component = () => (<div />);
const WrappedComponent = vmListenerHOC(Component);
const {container} = render(
const wrapper = mount(
<WrappedComponent
store={store}
vm={vm}
onGreenFlag={jest.fn()}
/>
);
const element = container.querySelector('#onGreenFlag');
expect(element).toHaveTextContent(/undefined/i);
const child = wrapper.find(Component);
expect(child.props().onGreenFlag).toBeUndefined();
});

test('targetsUpdate event from vm triggers targets update action', () => {
const Component = () => (<div />);
const WrappedComponent = vmListenerHOC(Component);
render(
mount(
<WrappedComponent
store={store}
vm={vm}
Expand All @@ -85,7 +79,7 @@ describe('VMListenerHOC', () => {
vm: vm
}
});
render(
mount(
<WrappedComponent
store={store}
vm={vm}
Expand All @@ -108,7 +102,7 @@ describe('VMListenerHOC', () => {
vm: vm
}
});
render(
mount(
<WrappedComponent
store={store}
vm={vm}
Expand All @@ -129,7 +123,7 @@ describe('VMListenerHOC', () => {
vm: vm
}
});
render(
mount(
<WrappedComponent
store={store}
vm={vm}
Expand Down Expand Up @@ -160,23 +154,20 @@ describe('VMListenerHOC', () => {
vm: vm
}
});
render(
mount(
<WrappedComponent
attachKeyboardEvents
store={store}
vm={vm}
/>
);

// keydown with an HTML target (e.g. project title input) should not be forwarded to VM
const inputEl = document.createElement('input');
eventTriggers.keydown({key: 'A', target: inputEl});
// keyboard events in text inputs are ignored
eventTriggers.keydown({key: 'A', target: document.createElement('input')});
expect(vm.postIOData).not.toHaveBeenLastCalledWith('keyboard', {key: 'A', isDown: true});

// keydown with an SVG target (Blockly workspace) should always be forwarded to VM
// even when a block has Blockly focus, so game controls work from the Code tab
const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
eventTriggers.keydown({key: 'A', target: svgEl});
// keydown with other non-input targets are sent to the vm via postIOData
eventTriggers.keydown({key: 'A', target: document.createElement('div')});
expect(vm.postIOData).toHaveBeenLastCalledWith('keyboard', {key: 'A', isDown: true});

// keydown/up with target as the document are sent to the vm via postIOData
Expand Down
Loading