Initial commit of 001code-html Scratch frontend project.

Includes scratch-gui, scratch-vm, scratch-blocks, scratch-render, scratch-l10n, and deployment config.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-16 15:37:45 +08:00
commit 6e0a1fbcbb
11350 changed files with 965674 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ButtonComponent matches snapshot 1`] = `
<span
className=""
onClick={[Function]}
role="button"
>
<div
className={undefined}
/>
</span>
`;

View File

@@ -0,0 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`IconButtonComponent matches snapshot 1`] = `
<div
className="custom-class-name"
onClick={[Function]}
role="button"
>
<img
className={undefined}
draggable={false}
src="imgSrc"
/>
<div
className={undefined}
>
<div>
Text
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,565 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Sound Editor Component matches snapshot 1`] = `
<div
className={undefined}
onMouseDown={undefined}
>
<div
className={undefined}
>
<div
className={undefined}
>
<label
className={undefined}
>
<span
className={undefined}
>
Sound
</span>
<input
className=""
onBlur={[Function]}
onChange={[Function]}
onKeyPress={[Function]}
onSubmit={[Function]}
tabIndex="1"
type="text"
value="sound name"
/>
</label>
<div
className={undefined}
>
<button
className={undefined}
disabled={false}
onClick={[Function]}
title="Undo"
>
<img
className={undefined}
draggable={false}
src="test-file-stub"
/>
</button>
<button
className={undefined}
disabled={true}
onClick={[Function]}
title="Redo"
>
<img
className={undefined}
draggable={false}
src="test-file-stub"
/>
</button>
</div>
</div>
<div
className={undefined}
>
<div
className=""
onClick={undefined}
role="button"
>
<img
className={undefined}
draggable={false}
src="test-file-stub"
/>
<div
className={undefined}
>
Copy
</div>
</div>
<div
className=""
onClick={undefined}
role="button"
>
<img
className={undefined}
draggable={false}
src="test-file-stub"
/>
<div
className={undefined}
>
Paste
</div>
</div>
<div
className=""
onClick={undefined}
role="button"
>
<img
className={undefined}
draggable={false}
src="test-file-stub"
/>
<div
className={undefined}
>
Copy to New
</div>
</div>
</div>
<div
className=""
onClick={[Function]}
role="button"
>
<img
className={undefined}
draggable={false}
src="test-file-stub"
/>
<div
className={undefined}
>
Delete
</div>
</div>
</div>
<div
className={undefined}
>
<div
className={undefined}
>
<svg
className={undefined}
viewBox="0 0 600 160"
>
<g
transform="scale(1, -1) translate(0, -80)"
>
<path
className={undefined}
d="M0 0Q0 80 150 120 Q300 160 450 200 Q600 240 600 0 Q600 -240 450 -200 Q300 -160 150 -120 Q0 -80 0 0Z"
strokeLinejoin="round"
strokeWidth={1}
/>
</g>
</svg>
<div
className=""
onMouseDown={[Function]}
onTouchStart={[Function]}
>
<div
className=""
style={
Object {
"alignContent": undefined,
"alignItems": undefined,
"alignSelf": undefined,
"flexBasis": undefined,
"flexDirection": undefined,
"flexGrow": undefined,
"flexShrink": undefined,
"flexWrap": undefined,
"height": undefined,
"justifyContent": undefined,
"left": "20%",
"width": "60.00000000000001%",
}
}
>
<div
className=""
style={
Object {
"alignContent": undefined,
"alignItems": undefined,
"alignSelf": undefined,
"flexBasis": undefined,
"flexDirection": undefined,
"flexGrow": undefined,
"flexShrink": undefined,
"flexWrap": undefined,
"height": undefined,
"justifyContent": undefined,
"width": undefined,
}
}
/>
<div
className=""
onMouseDown={[Function]}
onTouchStart={[Function]}
style={
Object {
"alignContent": undefined,
"alignItems": undefined,
"alignSelf": undefined,
"flexBasis": undefined,
"flexDirection": undefined,
"flexGrow": undefined,
"flexShrink": undefined,
"flexWrap": undefined,
"height": undefined,
"justifyContent": undefined,
"width": undefined,
}
}
>
<div
className=""
style={
Object {
"alignContent": undefined,
"alignItems": undefined,
"alignSelf": undefined,
"flexBasis": undefined,
"flexDirection": undefined,
"flexGrow": undefined,
"flexShrink": undefined,
"flexWrap": undefined,
"height": undefined,
"justifyContent": undefined,
"width": undefined,
}
}
>
<img
draggable={false}
src="test-file-stub"
/>
</div>
<div
className=""
style={
Object {
"alignContent": undefined,
"alignItems": undefined,
"alignSelf": undefined,
"flexBasis": undefined,
"flexDirection": undefined,
"flexGrow": undefined,
"flexShrink": undefined,
"flexWrap": undefined,
"height": undefined,
"justifyContent": undefined,
"width": undefined,
}
}
>
<img
draggable={false}
src="test-file-stub"
/>
</div>
</div>
<div
className=""
onMouseDown={[Function]}
onTouchStart={[Function]}
style={
Object {
"alignContent": undefined,
"alignItems": undefined,
"alignSelf": undefined,
"flexBasis": undefined,
"flexDirection": undefined,
"flexGrow": undefined,
"flexShrink": undefined,
"flexWrap": undefined,
"height": undefined,
"justifyContent": undefined,
"width": undefined,
}
}
>
<div
className=""
style={
Object {
"alignContent": undefined,
"alignItems": undefined,
"alignSelf": undefined,
"flexBasis": undefined,
"flexDirection": undefined,
"flexGrow": undefined,
"flexShrink": undefined,
"flexWrap": undefined,
"height": undefined,
"justifyContent": undefined,
"width": undefined,
}
}
>
<img
draggable={false}
src="test-file-stub"
/>
</div>
<div
className=""
style={
Object {
"alignContent": undefined,
"alignItems": undefined,
"alignSelf": undefined,
"flexBasis": undefined,
"flexDirection": undefined,
"flexGrow": undefined,
"flexShrink": undefined,
"flexWrap": undefined,
"height": undefined,
"justifyContent": undefined,
"width": undefined,
}
}
>
<img
draggable={false}
src="test-file-stub"
/>
</div>
</div>
</div>
<div
className={undefined}
>
<div
className=""
style={
Object {
"transform": "translateX(50%)",
}
}
/>
</div>
</div>
</div>
</div>
<div
className=""
>
<div
className=""
>
<button
className=""
onClick={[Function]}
title="Stop"
>
<img
draggable={false}
src="test-file-stub"
/>
</button>
</div>
<div
className={undefined}
>
<div
className=""
onClick={[Function]}
role="button"
>
<img
className={undefined}
draggable={false}
src="test-file-stub"
/>
<div
className={undefined}
>
<span>
Faster
</span>
</div>
</div>
<div
className=""
onClick={[Function]}
role="button"
>
<img
className={undefined}
draggable={false}
src="test-file-stub"
/>
<div
className={undefined}
>
<span>
Slower
</span>
</div>
</div>
<div
className=""
onClick={[Function]}
role="button"
>
<img
className={undefined}
draggable={false}
src="test-file-stub"
/>
<div
className={undefined}
>
<span>
Louder
</span>
</div>
</div>
<div
className=""
onClick={[Function]}
role="button"
>
<img
className={undefined}
draggable={false}
src="test-file-stub"
/>
<div
className={undefined}
>
<span>
Softer
</span>
</div>
</div>
<div
className=""
onClick={undefined}
role="button"
>
<img
className={undefined}
draggable={false}
src="test-file-stub"
/>
<div
className={undefined}
>
<span>
Mute
</span>
</div>
</div>
<div
className=""
onClick={undefined}
role="button"
>
<img
className={undefined}
draggable={false}
src="test-file-stub"
/>
<div
className={undefined}
>
<span>
Fade in
</span>
</div>
</div>
<div
className=""
onClick={undefined}
role="button"
>
<img
className={undefined}
draggable={false}
src="test-file-stub"
/>
<div
className={undefined}
>
<span>
Fade out
</span>
</div>
</div>
<div
className=""
onClick={[Function]}
role="button"
>
<img
className={undefined}
draggable={false}
src="test-file-stub"
/>
<div
className={undefined}
>
<span>
Reverse
</span>
</div>
</div>
<div
className=""
onClick={[Function]}
role="button"
>
<img
className={undefined}
draggable={false}
src="test-file-stub"
/>
<div
className={undefined}
>
<span>
Robot
</span>
</div>
</div>
<div
className=""
onClick={[Function]}
role="button"
>
<img
className={undefined}
draggable={false}
src="test-file-stub"
/>
<div
className={undefined}
>
<span>
Echo
</span>
</div>
</div>
</div>
</div>
<div
className={undefined}
>
<div
className={undefined}
>
00:00.30 / 00:00.60
</div>
<div
className={undefined}
>
Hz
<span>
Mono
</span>
(10.51KB)
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,179 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SpriteSelectorItemComponent matches snapshot when given a number and details to show 1`] = `
<div
className="react-contextmenu-wrapper ponies undefined"
onClick={[Function]}
onContextMenu={[Function]}
onMouseDown={[Function]}
onMouseEnter={undefined}
onMouseLeave={undefined}
onMouseOut={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<div
className={undefined}
>
5
</div>
<div
className={undefined}
>
<div
className={undefined}
>
<img
className={undefined}
draggable={false}
src="https://scratch.mit.edu/foo/bar/pony"
/>
</div>
</div>
<div
className={undefined}
>
<div
className={undefined}
>
Pony sprite
</div>
<div
className={undefined}
>
480 x 360
</div>
</div>
<div
aria-label="Delete"
className=""
onClick={[Function]}
role="button"
tabIndex={0}
>
<div
className={undefined}
>
<img
className={undefined}
src="test-file-stub"
/>
</div>
</div>
<nav
className="react-contextmenu"
onContextMenu={[Function]}
onMouseLeave={[Function]}
role="menu"
style={
Object {
"opacity": 0,
"pointerEvents": "none",
"position": "fixed",
}
}
tabIndex="-1"
>
<div
aria-disabled="false"
aria-orientation={null}
className="react-contextmenu-item"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseMove={[Function]}
onTouchEnd={[Function]}
role="menuitem"
tabIndex="-1"
>
<span>
delete
</span>
</div>
</nav>
</div>
`;
exports[`SpriteSelectorItemComponent matches snapshot when selected 1`] = `
<div
className="react-contextmenu-wrapper ponies undefined"
onClick={[Function]}
onContextMenu={[Function]}
onMouseDown={[Function]}
onMouseEnter={undefined}
onMouseLeave={undefined}
onMouseOut={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<div
className={undefined}
>
<div
className={undefined}
>
<img
className={undefined}
draggable={false}
src="https://scratch.mit.edu/foo/bar/pony"
/>
</div>
</div>
<div
className={undefined}
>
<div
className={undefined}
>
Pony sprite
</div>
</div>
<div
aria-label="Delete"
className=""
onClick={[Function]}
role="button"
tabIndex={0}
>
<div
className={undefined}
>
<img
className={undefined}
src="test-file-stub"
/>
</div>
</div>
<nav
className="react-contextmenu"
onContextMenu={[Function]}
onMouseLeave={[Function]}
role="menu"
style={
Object {
"opacity": 0,
"pointerEvents": "none",
"position": "fixed",
}
}
tabIndex="-1"
>
<div
aria-disabled="false"
aria-orientation={null}
className="react-contextmenu-item"
onClick={[Function]}
onMouseLeave={[Function]}
onMouseMove={[Function]}
onTouchEnd={[Function]}
role="menuitem"
tabIndex="-1"
>
<span>
delete
</span>
</div>
</nav>
</div>
`;

View File

@@ -0,0 +1,23 @@
import React from 'react';
import {shallow} from 'enzyme';
import ButtonComponent from '../../../src/components/button/button';
import renderer from 'react-test-renderer';
describe('ButtonComponent', () => {
test('matches snapshot', () => {
const onClick = jest.fn();
const component = renderer.create(
<ButtonComponent onClick={onClick} />
);
expect(component.toJSON()).toMatchSnapshot();
});
test('triggers callback when clicked', () => {
const onClick = jest.fn();
const componentShallowWrapper = shallow(
<ButtonComponent onClick={onClick} />
);
componentShallowWrapper.simulate('click');
expect(onClick).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,62 @@
import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
// Mock this utility because it uses dynamic imports that do not work with jest
jest.mock('../../../src/lib/libraries/decks/translate-image.js', () => {});
import Cards, {ImageStep, VideoStep} from '../../../src/components/cards/cards.jsx';
describe('Cards component', () => {
const defaultProps = () => ({
activeDeckId: 'id1',
content: {
id1: {
name: 'id1 - name',
img: 'id1 - img',
steps: [{video: 'videoUrl'}]
}
},
dragging: false,
expanded: true,
isRtl: false,
locale: 'en',
onActivateDeckFactory: jest.fn(),
onCloseCards: jest.fn(),
onDrag: jest.fn(),
onEndDrag: jest.fn(),
onNextStep: jest.fn(),
onPrevStep: jest.fn(),
onShowAll: jest.fn(),
onShrinkExpandCards: jest.fn(),
onStartDrag: jest.fn(),
showVideos: true,
step: 0,
x: 0,
y: 0
});
test('showVideos=true shows the video step', () => {
const component = mountWithIntl(
<Cards
{...defaultProps()}
showVideos
/>
);
expect(component.find(ImageStep).exists()).toEqual(false);
expect(component.find(VideoStep).exists()).toEqual(true);
});
test('showVideos=false shows the title image/name instead of video step', () => {
const component = mountWithIntl(
<Cards
{...defaultProps()}
showVideos={false}
/>
);
expect(component.find(VideoStep).exists()).toEqual(false);
const imageStep = component.find(ImageStep);
expect(imageStep.props().image).toEqual('id1 - img');
expect(imageStep.props().title).toEqual('id1 - name');
});
});

View File

@@ -0,0 +1,40 @@
import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import Controls from '../../../src/components/controls/controls';
import TurboMode from '../../../src/components/turbo-mode/turbo-mode';
import GreenFlag from '../../../src/components/green-flag/green-flag';
import StopAll from '../../../src/components/stop-all/stop-all';
describe('Controls component', () => {
const defaultProps = () => ({
active: false,
onGreenFlagClick: jest.fn(),
onStopAllClick: jest.fn(),
turbo: false
});
test('shows turbo mode when in turbo mode', () => {
const component = mountWithIntl(
<Controls
{...defaultProps()}
/>
);
expect(component.find(TurboMode).exists()).toEqual(false);
component.setProps({turbo: true});
expect(component.find(TurboMode).exists()).toEqual(true);
});
test('triggers the right callbacks when clicked', () => {
const props = defaultProps();
const component = mountWithIntl(
<Controls
{...props}
/>
);
component.find(GreenFlag).simulate('click');
expect(props.onGreenFlagClick).toHaveBeenCalled();
component.find(StopAll).simulate('click');
expect(props.onStopAllClick).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,43 @@
import React from 'react';
import {Provider} from 'react-redux';
const {mountWithIntl} = require('../../helpers/intl-helpers.jsx');
import configureStore from 'redux-mock-store';
import CrashMessageComponent from '../../../src/components/crash-message/crash-message.jsx';
import ErrorBoundary from '../../../src/containers/error-boundary.jsx';
const ChildComponent = () => <div>hello</div>;
describe('ErrorBoundary', () => {
const mockStore = configureStore();
let store;
beforeEach(() => {
store = mockStore({
locales: {
isRtl: false,
locale: 'en-US'
}
});
});
test('ErrorBoundary shows children before error and CrashMessageComponent after', () => {
const child = <ChildComponent />;
const wrapper = mountWithIntl(
<Provider store={store}><ErrorBoundary action="test">{child}</ErrorBoundary></Provider>
);
const errorSite = wrapper.childAt(0).childAt(0);
// @ts-ignore: 'onReload' prop is absent because this component will only be used for pattern matching
const crashMessagePattern = <CrashMessageComponent />;
expect(wrapper.containsMatchingElement(child)).toBeTruthy();
expect(wrapper.containsMatchingElement(crashMessagePattern)).toBeFalsy();
errorSite.simulateError(new Error('fake error for testing purposes'));
expect(wrapper.containsMatchingElement(child)).toBeFalsy();
expect(wrapper.containsMatchingElement(crashMessagePattern)).toBeTruthy();
});
});

View File

@@ -0,0 +1,37 @@
import React from 'react';
import {shallow} from 'enzyme';
import IconButton from '../../../src/components/icon-button/icon-button';
import renderer from 'react-test-renderer';
describe('IconButtonComponent', () => {
test('matches snapshot', () => {
const onClick = jest.fn();
const title = <div>Text</div>;
const imgSrc = 'imgSrc';
const className = 'custom-class-name';
const component = renderer.create(
<IconButton
className={className}
img={imgSrc}
title={title}
onClick={onClick}
/>
);
expect(component.toJSON()).toMatchSnapshot();
});
test('triggers callback when clicked', () => {
const onClick = jest.fn();
const title = <div>Text</div>;
const imgSrc = 'imgSrc';
const componentShallowWrapper = shallow(
<IconButton
img={imgSrc}
title={title}
onClick={onClick}
/>
);
componentShallowWrapper.simulate('click');
expect(onClick).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,64 @@
describe('no-op', () => {
test('no-op', () => {});
});
// tw: these seem to be hopelessly broken to the increasing scope of changes we make to the menu bar, disable for now...
/*
import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers';
import MenuBar from '../../../src/components/menu-bar/menu-bar';
import {menuInitialState} from '../../../src/reducers/menus';
import {LoadingState} from '../../../src/reducers/project-state';
import {DEFAULT_THEME} from '../../../src/lib/themes';
import configureStore from 'redux-mock-store';
import {Provider} from 'react-redux';
import VM from 'scratch-vm';
describe('MenuBar Component', () => {
const store = configureStore()({
locales: {
isRtl: false,
locale: 'en-US'
},
scratchGui: {
menus: menuInitialState,
projectState: {
loadingState: LoadingState.NOT_LOADED
},
theme: {
theme: DEFAULT_THEME
},
timeTravel: {
year: 'NOW'
},
vm: new VM()
}
});
const getComponent = function (props = {}) {
return <Provider store={store}><MenuBar {...props} /></Provider>;
};
test('menu bar with no About handler has no About button', () => {
const menuBar = mountWithIntl(getComponent());
const button = menuBar.find('AboutButton');
expect(button.exists()).toBe(false);
});
test('menu bar with an About handler has an About button', () => {
const onClickAbout = jest.fn();
const menuBar = mountWithIntl(getComponent({onClickAbout}));
const button = menuBar.find('AboutButton');
expect(button.exists()).toBe(true);
});
test('clicking on About button calls the handler', () => {
const onClickAbout = jest.fn();
const menuBar = mountWithIntl(getComponent({onClickAbout}));
const button = menuBar.find('AboutButton');
expect(onClickAbout).toHaveBeenCalledTimes(0);
button.simulate('click');
expect(onClickAbout).toHaveBeenCalledTimes(1);
});
});
*/

View File

@@ -0,0 +1,78 @@
import React from 'react';
import {OrderedMap} from 'immutable';
import configureStore from 'redux-mock-store';
import {Provider} from 'react-redux';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import MonitorList from '../../../src/components/monitor-list/monitor-list.jsx';
import {DEFAULT_THEME} from '../../../src/lib/themes';
describe('MonitorListComponent', () => {
const store = configureStore()({scratchGui: {
monitorLayout: {
monitors: {},
savedMonitorPositions: {}
},
theme: {
theme: DEFAULT_THEME
},
toolbox: {
toolboxXML: ''
},
vm: {
runtime: {
requestUpdateMonitor: () => {},
getLabelForOpcode: () => ''
}
}
}});
const draggable = false;
const onMonitorChange = jest.fn();
const stageSize = {
width: 100,
height: 100,
widthDefault: 100,
heightDefault: 100
};
let monitors = OrderedMap({});
// Wrap this in a function so it gets test specific states and can be reused.
const getComponent = function () {
return (
<Provider store={store}>
<MonitorList
draggable={draggable}
monitors={monitors}
stageSize={stageSize}
onMonitorChange={onMonitorChange}
/>
</Provider>
);
};
test('it renders the correct step size for discrete sliders', () => {
monitors = OrderedMap({
id1: {
visible: true,
mode: 'slider',
isDiscrete: true
}
});
const wrapper = mountWithIntl(getComponent());
const input = wrapper.find('input');
expect(input.props().step).toBe(1);
});
test('it renders the correct step size for non-discrete sliders', () => {
monitors = OrderedMap({
id1: {
visible: true,
mode: 'slider',
isDiscrete: false
}
});
const wrapper = mountWithIntl(getComponent());
const input = wrapper.find('input');
expect(input.props().step).toBe(0.01);
});
});

View File

@@ -0,0 +1,56 @@
import React from 'react';
import {shallow} from 'enzyme';
import DefaultMonitor from '../../../src/components/monitor/default-monitor';
import Monitor from '../../../src/components/monitor/monitor';
import {DARK_THEME, DEFAULT_THEME} from '../../../src/lib/themes';
jest.mock('../../../src/lib/themes/default');
jest.mock('../../../src/lib/themes/dark');
describe('Monitor Component', () => {
test('it selects the correct colors based on default theme', () => {
const noop = () => {};
const wrapper = shallow(<Monitor
category="motion"
// eslint-disable-next-line react/jsx-no-bind
componentRef={noop}
draggable={false}
label="My label"
mode="default"
// eslint-disable-next-line react/jsx-no-bind
onDragEnd={noop}
// eslint-disable-next-line react/jsx-no-bind
onNextMode={noop}
theme={DEFAULT_THEME}
/>);
const defaultMonitor = wrapper.find(DefaultMonitor);
// selects colors from mock value in src/lib/themes/__mocks__/default-colors.js
expect(defaultMonitor.props().categoryColor).toEqual({background: '#111111', text: '#444444'});
});
test('it selects the correct colors based on dark mode theme', () => {
const noop = () => {};
const wrapper = shallow(<Monitor
category="motion"
// eslint-disable-next-line react/jsx-no-bind
componentRef={noop}
draggable={false}
label="My label"
mode="default"
// eslint-disable-next-line react/jsx-no-bind
onDragEnd={noop}
// eslint-disable-next-line react/jsx-no-bind
onNextMode={noop}
theme={DARK_THEME}
/>);
const defaultMonitor = wrapper.find(DefaultMonitor);
// selects colors from mock value in src/lib/themes/__mocks__/dark-mode.js
expect(defaultMonitor.props().categoryColor).toEqual({background: '#AAAAAA', text: '#BBBBBB'});
});
});

View File

@@ -0,0 +1,146 @@
import React from 'react';
import {mountWithIntl, componentWithIntl} from '../../helpers/intl-helpers.jsx';
import SoundEditor from '../../../src/components/sound-editor/sound-editor';
describe('Sound Editor Component', () => {
let props;
beforeEach(() => {
props = {
isStereo: false,
duration: 1,
size: 10507,
canUndo: true,
canRedo: false,
chunkLevels: [1, 2, 3],
name: 'sound name',
playhead: 0.5,
trimStart: 0.2,
trimEnd: 0.8,
onChangeName: jest.fn(),
onDelete: jest.fn(),
onPlay: jest.fn(),
onRedo: jest.fn(),
onReverse: jest.fn(),
onSofter: jest.fn(),
onLouder: jest.fn(),
onRobot: jest.fn(),
onEcho: jest.fn(),
onFaster: jest.fn(),
onSlower: jest.fn(),
onSetTrimEnd: jest.fn(),
onSetTrimStart: jest.fn(),
onStop: jest.fn(),
onUndo: jest.fn()
};
});
test('matches snapshot', () => {
const component = componentWithIntl(<SoundEditor {...props} />);
expect(component.toJSON()).toMatchSnapshot();
});
test('delete button appears when selection is not null', () => {
const wrapper = mountWithIntl(
<SoundEditor
{...props}
trimEnd={0.75}
trimStart={0.25}
/>
);
wrapper.find('[children="Delete"]').simulate('click');
expect(props.onDelete).toHaveBeenCalled();
});
test('play button appears when playhead is null', () => {
const wrapper = mountWithIntl(
<SoundEditor
{...props}
playhead={null}
/>
);
wrapper.find('button[title="Play"]').simulate('click');
expect(props.onPlay).toHaveBeenCalled();
});
test('stop button appears when playhead is not null', () => {
const wrapper = mountWithIntl(
<SoundEditor
{...props}
playhead={0.5}
/>
);
wrapper.find('button[title="Stop"]').simulate('click');
expect(props.onStop).toHaveBeenCalled();
});
test('submitting name calls the callback', () => {
const wrapper = mountWithIntl(
<SoundEditor {...props} />
);
wrapper.find('input')
.simulate('change', {target: {value: 'hello'}})
.simulate('blur');
expect(props.onChangeName).toHaveBeenCalled();
});
test('effect buttons call the correct callbacks', () => {
const wrapper = mountWithIntl(
<SoundEditor {...props} />
);
wrapper.find('[children="Reverse"]').simulate('click');
expect(props.onReverse).toHaveBeenCalled();
wrapper.find('[children="Robot"]').simulate('click');
expect(props.onRobot).toHaveBeenCalled();
wrapper.find('[children="Faster"]').simulate('click');
expect(props.onFaster).toHaveBeenCalled();
wrapper.find('[children="Slower"]').simulate('click');
expect(props.onSlower).toHaveBeenCalled();
wrapper.find('[children="Louder"]').simulate('click');
expect(props.onLouder).toHaveBeenCalled();
wrapper.find('[children="Softer"]').simulate('click');
expect(props.onSofter).toHaveBeenCalled();
});
test('undo and redo buttons can be disabled by canUndo/canRedo', () => {
let wrapper = mountWithIntl(
<SoundEditor
{...props}
canUndo
canRedo={false}
/>
);
expect(wrapper.find('button[title="Undo"]').prop('disabled')).toBe(false);
expect(wrapper.find('button[title="Redo"]').prop('disabled')).toBe(true);
wrapper = mountWithIntl(
<SoundEditor
{...props}
canRedo
canUndo={false}
/>
);
expect(wrapper.find('button[title="Undo"]').prop('disabled')).toBe(true);
expect(wrapper.find('button[title="Redo"]').prop('disabled')).toBe(false);
});
test.skip('undo/redo buttons call the correct callback', () => {
const wrapper = mountWithIntl(
<SoundEditor
{...props}
canRedo
canUndo
/>
);
wrapper.find('button[title="Undo"]').simulate('click');
expect(props.onUndo).toHaveBeenCalled();
wrapper.find('button[title="Redo"]').simulate('click');
expect(props.onRedo).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,83 @@
import React from 'react';
import {mountWithIntl, shallowWithIntl, componentWithIntl} from '../../helpers/intl-helpers.jsx';
import SpriteSelectorItemComponent from '../../../src/components/sprite-selector-item/sprite-selector-item';
import DeleteButton from '../../../src/components/delete-button/delete-button';
describe('SpriteSelectorItemComponent', () => {
let className;
let costumeURL;
let name;
let onClick;
let onDeleteButtonClick;
let selected;
let number;
let details;
// Wrap this in a function so it gets test specific states and can be reused.
const getComponent = function () {
return (
<SpriteSelectorItemComponent
className={className}
costumeURL={costumeURL}
details={details}
name={name}
number={number}
selected={selected}
onClick={onClick}
onDeleteButtonClick={onDeleteButtonClick}
/>
);
};
beforeEach(() => {
className = 'ponies';
costumeURL = 'https://scratch.mit.edu/foo/bar/pony';
name = 'Pony sprite';
onClick = jest.fn();
onDeleteButtonClick = jest.fn();
selected = true;
// Reset to undefined since they are optional props
number = undefined; // eslint-disable-line no-undefined
details = undefined; // eslint-disable-line no-undefined
});
test('matches snapshot when selected', () => {
const component = componentWithIntl(getComponent());
expect(component.toJSON()).toMatchSnapshot();
});
test('matches snapshot when given a number and details to show', () => {
number = 5;
details = '480 x 360';
const component = componentWithIntl(getComponent());
expect(component.toJSON()).toMatchSnapshot();
});
test('does not have a close box when not selected', () => {
selected = false;
const wrapper = shallowWithIntl(getComponent());
expect(wrapper.find(DeleteButton).exists()).toBe(false);
});
test('triggers callback when Box component is clicked', () => {
// Use `mount` here because of the way ContextMenuTrigger consumes onClick
const wrapper = mountWithIntl(getComponent());
wrapper.simulate('click');
expect(onClick).toHaveBeenCalled();
});
test('triggers callback when CloseButton component is clicked', () => {
const wrapper = shallowWithIntl(getComponent());
wrapper.find(DeleteButton).simulate('click');
expect(onDeleteButtonClick).toHaveBeenCalled();
});
test('it has a context menu with delete menu item and callback', () => {
const wrapper = mountWithIntl(getComponent());
const contextMenu = wrapper.find('ContextMenu');
expect(contextMenu.exists()).toBe(true);
contextMenu.find('[children="delete"]').simulate('click');
expect(onDeleteButtonClick).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,52 @@
import React from 'react';
import {shallow} from 'enzyme';
import ToggleButtons from '../../../src/components/toggle-buttons/toggle-buttons';
describe('ToggleButtons', () => {
test('renders multiple buttons', () => {
const component = shallow(<ToggleButtons
buttons={[
{
title: 'Button 1',
handleClick: () => {},
icon: 'Button 1 icon'
},
{
title: 'Button 2',
handleClick: () => {},
icon: 'Button 2 icon'
}
]}
/>);
const buttons = component.find('button');
expect(buttons).toHaveLength(2);
expect(buttons.get(0).props.title).toBe('Button 1');
expect(buttons.get(1).props.title).toBe('Button 2');
});
test('calls correct click handler', () => {
const onClick1 = jest.fn();
const onClick2 = jest.fn();
const component = shallow(<ToggleButtons
buttons={[
{
title: 'Button 1',
handleClick: onClick1,
icon: 'Button 1 icon'
},
{
title: 'Button 2',
handleClick: onClick2,
icon: 'Button 2 icon'
}
]}
/>);
const button2 = component.find('button[title="Button 2"]');
button2.simulate('click');
expect(onClick2).toHaveBeenCalled();
expect(onClick1).not.toHaveBeenCalled();
});
});