From b2d4021455dedc85eb222f323c086f293fd6998b Mon Sep 17 00:00:00 2001 From: Andy O'Neill Date: Thu, 9 Feb 2023 11:49:11 -0500 Subject: [PATCH] feat: store color theme in state --- src/components/gui/gui.jsx | 7 +- src/containers/blocks.jsx | 34 ++------ src/lib/make-toolbox-xml.js | 84 ++++++++++++-------- src/lib/themes/__mocks__/dark-mode.js | 8 ++ src/lib/themes/__mocks__/default-colors.js | 11 +++ src/lib/themes/dark-mode.js | 66 ++++++++++++++++ src/lib/themes/default-colors.js | 90 ++++++++++++++++++++++ src/lib/themes/high-contrast.js | 87 +++++++++++++++++++++ src/lib/themes/index.js | 31 ++++++++ src/reducers/gui.js | 3 + src/reducers/theme.js | 25 ++++++ test/unit/util/themes.test.js | 22 ++++++ 12 files changed, 410 insertions(+), 58 deletions(-) create mode 100644 src/lib/themes/__mocks__/dark-mode.js create mode 100644 src/lib/themes/__mocks__/default-colors.js create mode 100644 src/lib/themes/dark-mode.js create mode 100644 src/lib/themes/default-colors.js create mode 100644 src/lib/themes/high-contrast.js create mode 100644 src/lib/themes/index.js create mode 100644 src/reducers/theme.js create mode 100644 test/unit/util/themes.test.js diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index a44604898..53330d3b7 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -117,6 +117,7 @@ const GUIComponent = props => { stageSizeMode, targetIsStage, telemetryModalVisible, + theme, tipsLibraryVisible, vm, ...componentProps @@ -303,10 +304,12 @@ const GUIComponent = props => { canUseCloud={canUseCloud} grow={1} isVisible={blocksTabVisible} + key={theme} options={{ media: `${basePath}static/blocks-media/` }} stageSize={stageSize} + theme={theme} vm={vm} /> @@ -422,6 +425,7 @@ GUIComponent.propTypes = { stageSizeMode: PropTypes.oneOf(Object.keys(STAGE_SIZE_MODES)), targetIsStage: PropTypes.bool, telemetryModalVisible: PropTypes.bool, + theme: PropTypes.string, tipsLibraryVisible: PropTypes.bool, vm: PropTypes.instanceOf(VM).isRequired }; @@ -448,7 +452,8 @@ GUIComponent.defaultProps = { const mapStateToProps = state => ({ // This is the button's mode, as opposed to the actual current state - stageSizeMode: state.scratchGui.stageSize.stageSize + stageSizeMode: state.scratchGui.stageSize.stageSize, + theme: state.scratchGui.theme.theme }); export default injectIntl(connect( diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 5756249f7..cb998b929 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -18,6 +18,7 @@ import {BLOCKS_DEFAULT_SCALE, STAGE_DISPLAY_SIZES} from '../lib/layout-constants import DropAreaHOC from '../lib/drop-area-hoc.jsx'; import DragConstants from '../lib/drag-constants'; import defineDynamicBlock from '../lib/define-dynamic-block'; +import {getColorsForTheme, themes} from '../lib/themes'; import {connect} from 'react-redux'; import {updateToolbox} from '../reducers/toolbox'; @@ -94,7 +95,7 @@ class Blocks extends React.Component { const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options, - {rtl: this.props.isRtl, toolbox: this.props.toolboxXML} + {rtl: this.props.isRtl, toolbox: this.props.toolboxXML, colours: getColorsForTheme(this.props.theme)} ); this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig); @@ -351,7 +352,8 @@ class Blocks extends React.Component { return makeToolboxXML(false, target.isStage, target.id, dynamicBlocksXML, targetCostumes[targetCostumes.length - 1].name, stageCostumes[stageCostumes.length - 1].name, - targetSounds.length > 0 ? targetSounds[targetSounds.length - 1].name : '' + targetSounds.length > 0 ? targetSounds[targetSounds.length - 1].name : '', + getColorsForTheme(this.props.theme) ); } catch { return null; @@ -620,22 +622,11 @@ Blocks.propTypes = { wheel: PropTypes.bool, startScale: PropTypes.number }), - colours: PropTypes.shape({ - workspace: PropTypes.string, - flyout: PropTypes.string, - toolbox: PropTypes.string, - toolboxSelected: PropTypes.string, - scrollbar: PropTypes.string, - scrollbarHover: PropTypes.string, - insertionMarker: PropTypes.string, - insertionMarkerOpacity: PropTypes.number, - fieldShadow: PropTypes.string, - dragShadowOpacity: PropTypes.number - }), comments: PropTypes.bool, collapse: PropTypes.bool }), stageSize: PropTypes.oneOf(Object.keys(STAGE_DISPLAY_SIZES)).isRequired, + theme: PropTypes.oneOf(themes), toolboxXML: PropTypes.string, updateMetrics: PropTypes.func, updateToolboxState: PropTypes.func, @@ -656,18 +647,6 @@ Blocks.defaultOptions = { length: 2, colour: '#ddd' }, - colours: { - workspace: '#F9F9F9', - flyout: '#F9F9F9', - toolbox: '#FFFFFF', - toolboxSelected: '#E9EEF2', - scrollbar: '#CECDCE', - scrollbarHover: '#CECDCE', - insertionMarker: '#000000', - insertionMarkerOpacity: 0.2, - fieldShadow: 'rgba(255, 255, 255, 0.3)', - dragShadowOpacity: 0.6 - }, comments: true, collapse: false, sounds: false @@ -675,7 +654,8 @@ Blocks.defaultOptions = { Blocks.defaultProps = { isVisible: true, - options: Blocks.defaultOptions + options: Blocks.defaultOptions, + theme: 'standard' }; const mapStateToProps = state => ({ diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js index 0b618979d..8d356e442 100644 --- a/src/lib/make-toolbox-xml.js +++ b/src/lib/make-toolbox-xml.js @@ -1,17 +1,19 @@ import ScratchBlocks from 'scratch-blocks'; +import {defaultColors} from './themes'; const categorySeparator = ''; const blockSeparator = ''; // At default scale, about 28px /* eslint-disable no-unused-vars */ -const motion = function (isInitialSetup, isStage, targetId) { +const motion = function (isInitialSetup, isStage, targetId, colors) { const stageSelected = ScratchBlocks.ScratchMsgs.translate( 'MOTION_STAGE_SELECTED', 'Stage selected: no motion blocks' ); + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + ${isStage ? ` ` : ` @@ -151,11 +153,12 @@ const xmlEscape = function (unsafe) { }); }; -const looks = function (isInitialSetup, isStage, targetId, costumeName, backdropName) { +const looks = function (isInitialSetup, isStage, targetId, costumeName, backdropName, colors) { const hello = ScratchBlocks.ScratchMsgs.translate('LOOKS_HELLO', 'Hello!'); const hmm = ScratchBlocks.ScratchMsgs.translate('LOOKS_HMM', 'Hmm...'); + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + ${isStage ? '' : ` @@ -288,9 +291,10 @@ const looks = function (isInitialSetup, isStage, targetId, costumeName, backdrop `; }; -const sound = function (isInitialSetup, isStage, targetId, soundName) { +const sound = function (isInitialSetup, isStage, targetId, soundName, colors) { + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + @@ -343,9 +347,10 @@ const sound = function (isInitialSetup, isStage, targetId, soundName) { `; }; -const events = function (isInitialSetup, isStage) { +const events = function (isInitialSetup, isStage, targetId, colors) { + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + @@ -382,9 +387,14 @@ const events = function (isInitialSetup, isStage) { `; }; -const control = function (isInitialSetup, isStage) { +const control = function (isInitialSetup, isStage, targetId, colors) { + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + @@ -429,10 +439,15 @@ const control = function (isInitialSetup, isStage) { `; }; -const sensing = function (isInitialSetup, isStage) { +const sensing = function (isInitialSetup, isStage, targetId, colors) { const name = ScratchBlocks.ScratchMsgs.translate('SENSING_ASK_TEXT', 'What\'s your name?'); + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + ${isStage ? '' : ` @@ -504,12 +519,17 @@ const sensing = function (isInitialSetup, isStage) { `; }; -const operators = function (isInitialSetup) { +const operators = function (isInitialSetup, isStage, targetId, colors) { const apple = ScratchBlocks.ScratchMsgs.translate('OPERATORS_JOIN_APPLE', 'apple'); const banana = ScratchBlocks.ScratchMsgs.translate('OPERATORS_JOIN_BANANA', 'banana'); const letter = ScratchBlocks.ScratchMsgs.translate('OPERATORS_LETTEROF_APPLE', 'a'); + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` - + @@ -691,25 +711,27 @@ const operators = function (isInitialSetup) { `; }; -const variables = function () { +const variables = function (isInitialSetup, isStage, targetId, colors) { + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` `; }; -const myBlocks = function () { +const myBlocks = function (isInitialSetup, isStage, targetId, colors) { + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. return ` `; @@ -732,10 +754,11 @@ const xmlClose = ''; * @param {?string} costumeName - The name of the default selected costume dropdown. * @param {?string} backdropName - The name of the default selected backdrop dropdown. * @param {?string} soundName - The name of the default selected sound dropdown. + * @param {?object} colors - The colors for the theme. * @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox. */ const makeToolboxXML = function (isInitialSetup, isStage = true, targetId, categoriesXML = [], - costumeName = '', backdropName = '', soundName = '') { + costumeName = '', backdropName = '', soundName = '', colors = defaultColors) { isStage = isInitialSetup || isStage; const gap = [categorySeparator]; @@ -753,15 +776,16 @@ const makeToolboxXML = function (isInitialSetup, isStage = true, targetId, categ } // return `undefined` }; - const motionXML = moveCategory('motion') || motion(isInitialSetup, isStage, targetId); - const looksXML = moveCategory('looks') || looks(isInitialSetup, isStage, targetId, costumeName, backdropName); - const soundXML = moveCategory('sound') || sound(isInitialSetup, isStage, targetId, soundName); - const eventsXML = moveCategory('event') || events(isInitialSetup, isStage, targetId); - const controlXML = moveCategory('control') || control(isInitialSetup, isStage, targetId); - const sensingXML = moveCategory('sensing') || sensing(isInitialSetup, isStage, targetId); - const operatorsXML = moveCategory('operators') || operators(isInitialSetup, isStage, targetId); - const variablesXML = moveCategory('data') || variables(isInitialSetup, isStage, targetId); - const myBlocksXML = moveCategory('procedures') || myBlocks(isInitialSetup, isStage, targetId); + const motionXML = moveCategory('motion') || motion(isInitialSetup, isStage, targetId, colors.motion); + const looksXML = moveCategory('looks') || + looks(isInitialSetup, isStage, targetId, costumeName, backdropName, colors.looks); + const soundXML = moveCategory('sound') || sound(isInitialSetup, isStage, targetId, soundName, colors.sounds); + const eventsXML = moveCategory('event') || events(isInitialSetup, isStage, targetId, colors.event); + const controlXML = moveCategory('control') || control(isInitialSetup, isStage, targetId, colors.control); + const sensingXML = moveCategory('sensing') || sensing(isInitialSetup, isStage, targetId, colors.sensing); + const operatorsXML = moveCategory('operators') || operators(isInitialSetup, isStage, targetId, colors.operators); + const variablesXML = moveCategory('data') || variables(isInitialSetup, isStage, targetId, colors.data); + const myBlocksXML = moveCategory('procedures') || myBlocks(isInitialSetup, isStage, targetId, colors.more); const everything = [ xmlOpen, diff --git a/src/lib/themes/__mocks__/dark-mode.js b/src/lib/themes/__mocks__/dark-mode.js new file mode 100644 index 000000000..c3faf8d54 --- /dev/null +++ b/src/lib/themes/__mocks__/dark-mode.js @@ -0,0 +1,8 @@ +const blockColors = { + motion: { + primary: '#AAAAAA' + }, + text: '#BBBBBB' +}; + +export default blockColors; diff --git a/src/lib/themes/__mocks__/default-colors.js b/src/lib/themes/__mocks__/default-colors.js new file mode 100644 index 000000000..f382f38aa --- /dev/null +++ b/src/lib/themes/__mocks__/default-colors.js @@ -0,0 +1,11 @@ +const blockColors = { + motion: { + primary: '#111111', + secondary: '#222222', + tertiary: '#333333' + }, + text: '#444444', + workspace: '#555555' +}; + +export default blockColors; diff --git a/src/lib/themes/dark-mode.js b/src/lib/themes/dark-mode.js new file mode 100644 index 000000000..d9fc37a57 --- /dev/null +++ b/src/lib/themes/dark-mode.js @@ -0,0 +1,66 @@ +const blockColors = { + motion: { + primary: '#0F1E33', + secondary: '#4C4C4C', + tertiary: '#4C97FF' + }, + looks: { + primary: '#1E1433', + secondary: '#4C4C4C', + tertiary: '#9966FF' + }, + sounds: { + primary: '#291329', + secondary: '#4C4C4C', + tertiary: '#CF63CF' + }, + control: { + primary: '#332205', + secondary: '#4C4C4C', + tertiary: '#FFAB19' + }, + event: { + primary: '#332600', + secondary: '#4C4C4C', + tertiary: '#FFBF00' + }, + sensing: { + primary: '#12232A', + secondary: '#4C4C4C', + tertiary: '#5CB1D6' + }, + pen: { + primary: '#03251C', + secondary: '#4C4C4C', + tertiary: '#0fBD8C' + }, + operators: { + primary: '#112611', + secondary: '#4C4C4C', + tertiary: '#59C059' + }, + data: { + primary: '#331C05', + secondary: '#4C4C4C', + tertiary: '#FF8C1A' + }, + data_lists: { + primary: '#331405', + secondary: '#4C4C4C', + tertiary: '#FF661A' + }, + more: { + primary: '#331419', + secondary: '#4C4C4C', + tertiary: '#FF6680' + }, + text: '#E5E5E5', + workspace: '#121212', + toolboxSelected: '#4C4C4C', + toolboxText: '#E5E5E5', + toolbox: '#121212', + flyout: '#121212', + textField: '#4C4C4C' +}; + +export default blockColors; diff --git a/src/lib/themes/default-colors.js b/src/lib/themes/default-colors.js new file mode 100644 index 000000000..e9f9cde3c --- /dev/null +++ b/src/lib/themes/default-colors.js @@ -0,0 +1,90 @@ +const blockColors = { + motion: { + primary: '#4C97FF', + secondary: '#4280D7', + tertiary: '#3373CC' + }, + looks: { + primary: '#9966FF', + secondary: '#855CD6', + tertiary: '#774DCB' + }, + sounds: { + primary: '#CF63CF', + secondary: '#C94FC9', + tertiary: '#BD42BD' + }, + control: { + primary: '#FFAB19', + secondary: '#EC9C13', + tertiary: '#CF8B17' + }, + event: { + primary: '#FFBF00', + secondary: '#E6AC00', + tertiary: '#CC9900' + }, + sensing: { + primary: '#5CB1D6', + secondary: '#47A8D1', + tertiary: '#2E8EB8' + }, + pen: { + primary: '#0fBD8C', + secondary: '#0DA57A', + tertiary: '#0B8E69' + }, + operators: { + primary: '#59C059', + secondary: '#46B946', + tertiary: '#389438' + }, + data: { + primary: '#FF8C1A', + secondary: '#FF8000', + tertiary: '#DB6E00' + }, + // This is not a new category, but rather for differentiation + // between lists and scalar variables. + data_lists: { + primary: '#FF661A', + secondary: '#FF5500', + tertiary: '#E64D00' + }, + more: { + primary: '#FF6680', + secondary: '#FF4D6A', + tertiary: '#FF3355' + }, + text: '#575E75', + workspace: '#F9F9F9', + toolboxHover: '#4C97FF', + toolboxSelected: '#E9EEF2', + toolboxText: '#575E75', + toolbox: '#FFFFFF', + flyout: '#F9F9F9', + scrollbar: '#CECDCE', + scrollbarHover: '#CECDCE', + textField: '#FFFFFF', + insertionMarker: '#000000', + insertionMarkerOpacity: 0.2, + dragShadowOpacity: 0.6, + stackGlow: '#FFF200', + stackGlowSize: 4, + stackGlowOpacity: 1, + replacementGlow: '#FFFFFF', + replacementGlowSize: 2, + replacementGlowOpacity: 1, + colourPickerStroke: '#FFFFFF', + // CSS colours: support RGBA + fieldShadow: 'rgba(255, 255, 255, 0.3)', + dropDownShadow: 'rgba(0, 0, 0, .3)', + numPadBackground: '#547AB2', + numPadBorder: '#435F91', + numPadActiveBackground: '#435F91', + numPadText: 'white', // Do not use hex here, it cannot be inlined with data-uri SVG + valueReportBackground: '#FFFFFF', + valueReportBorder: '#AAAAAA' +}; + +export default blockColors; diff --git a/src/lib/themes/high-contrast.js b/src/lib/themes/high-contrast.js new file mode 100644 index 000000000..1c185556f --- /dev/null +++ b/src/lib/themes/high-contrast.js @@ -0,0 +1,87 @@ +const blockColors = { + motion: { + primary: '#80B5FF', + secondary: '#B3D2FF', + tertiary: '#3373CC', + // this field is not used in the "classic" Scratch 3 color scheme + // If we add "modes" we'd have to handle what happens when this field is not defined. + quaternary: '#CCE1FF' + }, + looks: { + primary: '#CCB3FF', + secondary: '#DDCCFF', + tertiary: '#774DCB', + quaternary: '#EEE5FF' + }, + sounds: { + primary: '#E19DE1', + secondary: '#FFB3FF', + tertiary: '#BD42BD', + quaternary: '#FFCCFF' + + }, + control: { + primary: '#FFBE4C', + secondary: '#FFDA99', + tertiary: '#CF8B17', + quaternary: '#FFE3B3' + }, + event: { + primary: '#FFD966', + secondary: '#FFECB3', + tertiary: '#CC9900', + quaternary: '#FFF2CC' + }, + sensing: { + primary: '#85C4E0', + secondary: '#AED8EA', + tertiary: '#2E8EB8', + quaternary: '#C2E2F0' + }, + pen: { + primary: '#13ECAF', + secondary: '#75F0CD', + tertiary: '#0B8E69', + quaternary: '#A3F5DE' + }, + operators: { + primary: '#7ECE7E', + secondary: '#B5E3B5', + tertiary: '#389438', + quaternary: '#DAF1DA' + }, + data: { + primary: '#FFA54C', + secondary: '#FFCC99', + tertiary: '#DB6E00', + quaternary: '#FFE5CC' + }, + // This is not a new category, but rather for differentiation + // between lists and scalar variables. + data_lists: { + primary: '#FF9966', + secondary: '#FFCAB0', // I don't think this is used, b/c we don't have any droppable fields in list blocks + tertiary: '#E64D00', + quaternary: '#FFDDCC' + }, + more: { + primary: '#FF99AA', + secondary: '#FFCCD5', + tertiary: '#FF3355', + quaternary: '#FFE5EA' + }, + // For productionizing: we should look into making this field be the color of the text on the blocks + // e.g. move and steps in move (10) steps, and rename this property below for controlling the text inside + // of inputs e.g. 10 + text: '#000000', // Text inside of inputs e.g. 90 in [point in direction (90)] + + toolboxText: '#000000', // Toolbox text, color picker text (used to be #575E75) + // The color that the category menu label (e.g. 'motion', 'looks', etc.) changes to on hover + toolboxHover: '#3373CC', + insertionMarker: '#000000', + insertionMarkerOpacity: 0.2, + fieldShadow: 'rgba(255, 255, 255, 0.3)', + dragShadowOpacity: 0.6 +}; + +export default blockColors; diff --git a/src/lib/themes/index.js b/src/lib/themes/index.js new file mode 100644 index 000000000..e1fa87f71 --- /dev/null +++ b/src/lib/themes/index.js @@ -0,0 +1,31 @@ +import defaultsDeep from 'lodash.defaultsdeep'; + +import darkMode from './dark-mode'; +import highContrast from './high-contrast'; +import defaultColors from './default-colors'; + +const mergeWithDefaults = colors => defaultsDeep({}, colors, defaultColors); + +const themeMap = { + 'dark-mode': mergeWithDefaults(darkMode), + 'high-contrast': mergeWithDefaults(highContrast), + 'standard': defaultColors +}; + +const getColorsForTheme = theme => { + const colors = themeMap[theme]; + + if (!colors) { + throw new Error(`Undefined theme ${theme}`); + } + + return colors; +}; + +const themes = Object.keys(themeMap); + +export { + defaultColors, + getColorsForTheme, + themes +}; diff --git a/src/reducers/gui.js b/src/reducers/gui.js index 378a3203c..2e70c40e1 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -21,6 +21,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 themeReducer, {themeInitialState} from './theme'; import timeoutReducer, {timeoutInitialState} from './timeout'; import toolboxReducer, {toolboxInitialState} from './toolbox'; import vmReducer, {vmInitialState} from './vm'; @@ -55,6 +56,7 @@ const guiInitialState = { fontsLoaded: fontsLoadedInitialState, restoreDeletion: restoreDeletionInitialState, targets: targetsInitialState, + theme: themeInitialState, timeout: timeoutInitialState, toolbox: toolboxInitialState, vm: vmInitialState, @@ -154,6 +156,7 @@ const guiReducer = combineReducers({ fontsLoaded: fontsLoadedReducer, restoreDeletion: restoreDeletionReducer, targets: targetReducer, + theme: themeReducer, timeout: timeoutReducer, toolbox: toolboxReducer, vm: vmReducer, diff --git a/src/reducers/theme.js b/src/reducers/theme.js new file mode 100644 index 000000000..5d05eff00 --- /dev/null +++ b/src/reducers/theme.js @@ -0,0 +1,25 @@ +const SET_THEME = 'scratch-gui/theme/SET_THEME'; + +const initialState = { + theme: 'standard' +}; + +const reducer = (state = initialState, action) => { + switch (action.type) { + case SET_THEME: + return {...state, theme: action.theme}; + default: + return state; + } +}; + +const setTheme = theme => ({ + type: SET_THEME, + theme +}); + +export { + reducer as default, + initialState as themeInitialState, + setTheme +}; diff --git a/test/unit/util/themes.test.js b/test/unit/util/themes.test.js new file mode 100644 index 000000000..d281add0c --- /dev/null +++ b/test/unit/util/themes.test.js @@ -0,0 +1,22 @@ +import {defaultColors, getColorsForTheme} from '../../../src/lib/themes'; + +jest.mock('../../../src/lib/themes/default-colors'); +jest.mock('../../../src/lib/themes/dark-mode'); + +describe('themes', () => { + test('provides the default theme colors', () => { + expect(defaultColors.motion.primary).toEqual('#111111'); + }); + + test('returns the dark mode', () => { + const colors = getColorsForTheme('dark-mode'); + + expect(colors.motion.primary).toEqual('#AAAAAA'); + }); + + test('uses default theme colors when not specified', () => { + const colors = getColorsForTheme('dark-mode'); + + expect(colors.motion.secondary).toEqual('#222222'); + }); +});