feat: [UEPR-57] update tests to use react testing library and remove use of default props

This commit is contained in:
MiroslavDionisiev 2025-04-22 09:32:15 +03:00
parent a739682d91
commit dce8c99c1b
65 changed files with 1721 additions and 1463 deletions
.gitignore
bin
package.json
src
test

1
.gitignore vendored
View file

@ -21,6 +21,7 @@ ENV
# Test # Test
/test/results/* /test/results/*
/test/generated/generated-locales.js
/.nyc_output /.nyc_output
/coverage /coverage
/bin/lib/localized-urls.json /bin/lib/localized-urls.json

48
bin/build-translations.js Normal file
View file

@ -0,0 +1,48 @@
const routes = require('../src/routes.json');
const path = require('path');
const fs = require('fs');
const merge = require('lodash.merge');
const globalTemplateFile = path.resolve(__dirname, '../src/l10n.json');
const generatedLocales = {
en: JSON.parse(fs.readFileSync(globalTemplateFile, 'utf8'))
};
const defaultLocales = {};
const views = [];
for (const route in routes) {
if (typeof routes[route].redirect !== 'undefined') {
continue;
}
views.push(routes[route].name);
try {
const subdir = routes[route].view.split('/');
subdir.pop();
const l10n = path.resolve(
__dirname,
`../src/views/${subdir.join('/')}/l10n.json`
);
const viewIds = JSON.parse(fs.readFileSync(l10n, 'utf8'));
defaultLocales[routes[route].name] = viewIds;
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
}
}
views
.map(view => defaultLocales[view])
.reduce((acc, curr) => merge(acc, curr), generatedLocales.en);
const dirPath = './test/generated';
const filePath = './test/generated/generated-locales.js';
const variableName = 'generatedLocales';
const variableValue = JSON.stringify(generatedLocales, null, 2);
const content = `const ${variableName} = ${variableValue};
export {${variableName}};
`;
fs.mkdirSync(dirPath, {recursive: true});
fs.writeFileSync(filePath, content, 'utf8');

View file

@ -9,13 +9,14 @@
"test:lint:ci": "eslint . --ext .js,.jsx,.json --format junit -o ./test/results/lint-results.xml", "test:lint:ci": "eslint . --ext .js,.jsx,.json --format junit -o ./test/results/lint-results.xml",
"test:health": "jest ./test/health/*.test.js", "test:health": "jest ./test/health/*.test.js",
"test:integration": "jest ./test/integration/*.test.js --reporters=default --maxWorkers=5", "test:integration": "jest ./test/integration/*.test.js --reporters=default --maxWorkers=5",
"test:unit": "npm run test:unit:jest && npm run test:unit:tap", "test:unit": "npm run test:build-translations && npm run test:unit:jest && npm run test:unit:tap",
"test:unit:jest": "npm run test:unit:jest:unit && npm run test:unit:jest:localization", "test:unit:jest": "npm run test:unit:jest:unit && npm run test:unit:jest:localization",
"test:unit:jest:unit": "jest ./test/unit/ --env=jsdom --reporters=default", "test:unit:jest:unit": "jest ./test/unit/ --env=jsdom --reporters=default",
"test:unit:jest:localization": "jest ./test/localization/*.test.js --reporters=default", "test:unit:jest:localization": "jest ./test/localization/*.test.js --reporters=default",
"test:unit:tap": "tap ./test/{unit-legacy,localization-legacy}/ --no-coverage -R classic", "test:unit:tap": "tap ./test/{unit-legacy,localization-legacy}/ --no-coverage -R classic",
"test:unit:convertReportToXunit": "tap ./test/results/unit-raw.tap --no-coverage -R xunit > ./test/results/unit-tap-results.xml", "test:unit:convertReportToXunit": "tap ./test/results/unit-raw.tap --no-coverage -R xunit > ./test/results/unit-tap-results.xml",
"test:coverage": "tap ./test/{unit-legacy,localization-legacy}/ --coverage --coverage-report=lcov", "test:coverage": "tap ./test/{unit-legacy,localization-legacy}/ --coverage --coverage-report=lcov",
"test:build-translations": "node ./bin/build-translations.js",
"build": "npm run clean && npm run translate && NODE_OPTIONS=--max_old_space_size=8000 webpack --bail", "build": "npm run clean && npm run translate && NODE_OPTIONS=--max_old_space_size=8000 webpack --bail",
"build:analyze": "ANALYZE_BUNDLE=true npm run build", "build:analyze": "ANALYZE_BUNDLE=true npm run build",
"clean": "rm -rf ./build && rm -rf ./intl && mkdir -p build && mkdir -p intl", "clean": "rm -rf ./build && rm -rf ./intl && mkdir -p build && mkdir -p intl",

View file

@ -1,12 +1,16 @@
const classNames = require('classnames'); const classNames = require('classnames');
const omit = require('lodash.omit');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const React = require('react'); const React = require('react');
const Avatar = props => ( const Avatar = ({
className,
src = '//uploads.scratch.mit.edu/get_image/user/2584924_24x24.png?v=1438702210.96',
...rest
}) => (
<img <img
className={classNames('avatar', props.className)} className={classNames('avatar', className)}
{...omit(props, ['className'])} src={src}
{...rest}
/> />
); );
@ -15,8 +19,4 @@ Avatar.propTypes = {
src: PropTypes.string src: PropTypes.string
}; };
Avatar.defaultProps = {
src: '//uploads.scratch.mit.edu/get_image/user/2584924_24x24.png?v=1438702210.96'
};
module.exports = Avatar; module.exports = Avatar;

View file

@ -2,19 +2,21 @@ const PropTypes = require('prop-types');
const React = require('react'); const React = require('react');
const FormattedNumber = require('react-intl').FormattedNumber; const FormattedNumber = require('react-intl').FormattedNumber;
const CappedNumber = props => ( const CappedNumber = ({
<props.as className={props.className}> as: Component = 'span',
<FormattedNumber value={Math.min(props.value, 100)} /> className,
{props.value > 100 ? '+' : ''} value
</props.as> }) => (
<Component className={className}>
<FormattedNumber value={Math.min(value, 100)} />
{value > 100 ? '+' : ''}
</Component>
); );
CappedNumber.propTypes = { CappedNumber.propTypes = {
className: PropTypes.string, className: PropTypes.string,
value: PropTypes.number.isRequired value: PropTypes.number.isRequired,
as: PropTypes.elementType
}; };
CappedNumber.defaultProps = {
as: 'span'
};
module.exports = CappedNumber; module.exports = CappedNumber;

View file

@ -11,8 +11,15 @@ require('slick-carousel/slick/slick.scss');
require('slick-carousel/slick/slick-theme.scss'); require('slick-carousel/slick/slick-theme.scss');
require('./carousel.scss'); require('./carousel.scss');
const Carousel = props => { const Carousel = ({
defaults(props.settings, { className,
items = require('./carousel.json'),
settings = {},
showRemixes = false,
showLoves = false,
type = 'project'
}) => {
defaults(settings, {
centerMode: false, centerMode: false,
dots: false, dots: false,
infinite: false, infinite: false,
@ -49,12 +56,12 @@ const Carousel = props => {
return ( return (
<Slider <Slider
className={classNames('carousel', props.className)} className={classNames('carousel', className)}
{... props.settings} {...settings}
> >
{props.items.map(item => { {items.map(item => {
let href = ''; let href = '';
switch (props.type) { switch (type) {
case 'gallery': case 'gallery':
href = `/studios/${item.id}/`; href = `/studios/${item.id}/`;
break; break;
@ -69,14 +76,14 @@ const Carousel = props => {
<Thumbnail <Thumbnail
creator={item.author.username} creator={item.author.username}
href={href} href={href}
key={[props.type, item.id].join('.')} key={`${type}.${item.id}`}
loves={item.stats.loves} loves={item.stats.loves}
remixes={item.stats.remixes} remixes={item.stats.remixes}
showLoves={props.showLoves} showLoves={showLoves}
showRemixes={props.showRemixes} showRemixes={showRemixes}
src={item.image} src={item.image}
title={item.title} title={item.title}
type={props.type} type={type}
/> />
); );
})} })}
@ -102,12 +109,4 @@ Carousel.propTypes = {
type: PropTypes.string type: PropTypes.string
}; };
Carousel.defaultProps = {
items: require('./carousel.json'),
settings: {},
showRemixes: false,
showLoves: false,
type: 'project'
};
module.exports = Carousel; module.exports = Carousel;

View file

@ -15,8 +15,15 @@ require('slick-carousel/slick/slick.scss');
require('slick-carousel/slick/slick-theme.scss'); require('slick-carousel/slick/slick-theme.scss');
require('./carousel.scss'); require('./carousel.scss');
const Carousel = props => { const Carousel = ({
defaults(props.settings, { className,
items = require('./carousel.json'),
settings = {},
showRemixes = false,
showLoves = false,
type = 'project'
}) => {
defaults(settings, {
centerMode: false, centerMode: false,
dots: false, dots: false,
infinite: false, infinite: false,
@ -50,14 +57,16 @@ const Carousel = props => {
} }
] ]
}); });
const arrows = props.items.length > props.settings.slidesToShow;
const arrows = items.length > settings.slidesToShow;
return ( return (
<Slider <Slider
arrows={arrows} arrows={arrows}
className={classNames('carousel', props.className)} className={classNames('carousel', className)}
{... props.settings} {...settings}
> >
{props.items.map(item => { {items.map(item => {
let href = ''; let href = '';
switch (item.type) { switch (item.type) {
case 'gallery': case 'gallery':
@ -74,11 +83,11 @@ const Carousel = props => {
<Thumbnail <Thumbnail
creator={item.creator} creator={item.creator}
href={href} href={href}
key={[props.type, item.id].join('.')} key={[type, item.id].join('.')}
loves={item.love_count} loves={item.love_count}
remixes={item.remixers_count} remixes={item.remixers_count}
showLoves={props.showLoves} showLoves={showLoves}
showRemixes={props.showRemixes} showRemixes={showRemixes}
src={item.thumbnail_url} src={item.thumbnail_url}
title={item.title} title={item.title}
type={item.type} type={item.type}
@ -107,12 +116,4 @@ Carousel.propTypes = {
type: PropTypes.string type: PropTypes.string
}; };
Carousel.defaultProps = {
items: require('./carousel.json'),
settings: {},
showRemixes: false,
showLoves: false,
type: 'project'
};
module.exports = Carousel; module.exports = Carousel;

View file

@ -4,22 +4,23 @@ const React = require('react');
require('./emoji-text.scss'); require('./emoji-text.scss');
const EmojiText = props => ( const EmojiText = ({
<props.as as: Component = 'p',
className={classNames('emoji-text', props.className)} className,
text
}) => (
<Component
className={classNames('emoji-text', className)}
dangerouslySetInnerHTML={{ // eslint-disable-line react/no-danger dangerouslySetInnerHTML={{ // eslint-disable-line react/no-danger
__html: props.text __html: text
}} }}
/> />
); );
EmojiText.propTypes = { EmojiText.propTypes = {
className: PropTypes.string, className: PropTypes.string,
text: PropTypes.string.isRequired text: PropTypes.string.isRequired,
}; as: PropTypes.elementType
EmojiText.defaultProps = {
as: 'p'
}; };
module.exports = EmojiText; module.exports = EmojiText;

View file

@ -6,13 +6,20 @@ const FlexRow = require('../../components/flex-row/flex-row.jsx');
require('./extension-landing.scss'); require('./extension-landing.scss');
const ExtensionRequirements = props => ( const ExtensionRequirements = ({
hideAndroid = false,
hideBluetooth = false,
hideChromeOS = false,
hideMac = false,
hideScratchLink = false,
hideWindows = false
}) => (
<FlexRow className="column extension-requirements-container"> <FlexRow className="column extension-requirements-container">
<span className="requirements-header"> <span className="requirements-header">
<FormattedMessage id="extensionHeader.requirements" /> <FormattedMessage id="extensionHeader.requirements" />
</span> </span>
<FlexRow className="extension-requirements"> <FlexRow className="extension-requirements">
{!props.hideWindows && ( {!hideWindows && (
<span> <span>
<img <img
alt="" alt=""
@ -21,7 +28,7 @@ const ExtensionRequirements = props => (
Windows 10 version 1709+ Windows 10 version 1709+
</span> </span>
)} )}
{!props.hideMac && ( {!hideMac && (
<span> <span>
<img <img
alt="" alt=""
@ -30,7 +37,7 @@ const ExtensionRequirements = props => (
macOS 10.15+ macOS 10.15+
</span> </span>
)} )}
{!props.hideChromeOS && ( {!hideChromeOS && (
<span> <span>
<img <img
alt="" alt=""
@ -39,7 +46,7 @@ const ExtensionRequirements = props => (
ChromeOS ChromeOS
</span> </span>
)} )}
{!props.hideAndroid && ( {!hideAndroid && (
<span> <span>
<img <img
alt="" alt=""
@ -48,13 +55,13 @@ const ExtensionRequirements = props => (
Android 6.0+ Android 6.0+
</span> </span>
)} )}
{!props.hideBluetooth && ( {!hideBluetooth && (
<span> <span>
<img src="/svgs/extensions/bluetooth.svg" /> <img src="/svgs/extensions/bluetooth.svg" />
Bluetooth Bluetooth
</span> </span>
)} )}
{!props.hideScratchLink && ( {!hideScratchLink && (
<span> <span>
<img <img
alt="" alt=""
@ -76,13 +83,4 @@ ExtensionRequirements.propTypes = {
hideWindows: PropTypes.bool hideWindows: PropTypes.bool
}; };
ExtensionRequirements.defaultProps = {
hideAndroid: false,
hideBluetooth: false,
hideChromeOS: false,
hideMac: false,
hideScratchLink: false,
hideWindows: false
};
module.exports = ExtensionRequirements; module.exports = ExtensionRequirements;

View file

@ -4,19 +4,20 @@ const React = require('react');
require('./flex-row.scss'); require('./flex-row.scss');
const FlexRow = props => ( const FlexRow = ({
<props.as className={classNames('flex-row', props.className)}> as: Component = 'div',
{props.children} className,
</props.as> children
}) => (
<Component className={classNames('flex-row', className)}>
{children}
</Component>
); );
FlexRow.propTypes = { FlexRow.propTypes = {
children: PropTypes.node, children: PropTypes.node,
className: PropTypes.string className: PropTypes.string,
}; as: PropTypes.elementType
FlexRow.defaultProps = {
as: 'div'
}; };
module.exports = FlexRow; module.exports = FlexRow;

View file

@ -1,30 +1,32 @@
const classNames = require('classnames'); const classNames = require('classnames');
const omit = require('lodash.omit');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const React = require('react'); const React = require('react');
require('./button.scss'); require('./button.scss');
const Button = props => { const Button = ({
const classes = classNames('button', props.className, {'forms-close-button': props.isCloseType}); children,
className = '',
isCloseType = false,
...restProps
}) => {
const classes = classNames('button', className, {'forms-close-button': isCloseType});
return ( return (
<button <button
className={classes} className={classes}
{...omit(props, ['className', 'children', 'isCloseType'])} {...restProps}
> >
{ {isCloseType ? (
props.isCloseType ? ( <img
<img alt="close-icon"
alt="close-icon" className="modal-content-close-img"
className="modal-content-close-img" draggable="false"
draggable="false" src="/svgs/modal/close-x.svg"
src="/svgs/modal/close-x.svg" />
/> ) : (
) : [ children
props.children )}
]
}
</button> </button>
); );
}; };
@ -35,9 +37,4 @@ Button.propTypes = {
isCloseType: PropTypes.bool isCloseType: PropTypes.bool
}; };
Button.defaultProps = {
className: '',
isCloseType: false
};
module.exports = Button; module.exports = Button;

View file

@ -4,13 +4,17 @@ const React = require('react');
require('./charcount.scss'); require('./charcount.scss');
const CharCount = props => ( const CharCount = ({
className = '',
currentCharacters = 0,
maxCharacters = 0
}) => (
<p <p
className={classNames('char-count', props.className, { className={classNames('char-count', className, {
overmax: (props.currentCharacters > props.maxCharacters) overmax: (currentCharacters > maxCharacters)
})} })}
> >
{props.currentCharacters}/{props.maxCharacters} {currentCharacters}/{maxCharacters}
</p> </p>
); );
@ -20,9 +24,4 @@ CharCount.propTypes = {
maxCharacters: PropTypes.number maxCharacters: PropTypes.number
}; };
CharCount.defaultProps = {
currentCharacters: 0,
maxCharacters: 0
};
module.exports = CharCount; module.exports = CharCount;

View file

@ -9,9 +9,16 @@ const inputHOC = require('./input-hoc.jsx');
require('./row.scss'); require('./row.scss');
require('./checkbox.scss'); require('./checkbox.scss');
const Checkbox = props => ( const Checkbox = ({
className = '',
value = false,
valueLabel = '',
...props
}) => (
<FRCCheckbox <FRCCheckbox
rowClassName={classNames('checkbox-row', props.className)} rowClassName={classNames('checkbox-row', className)}
value={value}
valueLabel={valueLabel}
{...props} {...props}
/> />
); );
@ -22,9 +29,4 @@ Checkbox.propTypes = {
valueLabel: PropTypes.string valueLabel: PropTypes.string
}; };
Checkbox.defaultProps = {
value: false,
valueLabel: ''
};
module.exports = inputHOC(defaultValidationHOC(Checkbox)); module.exports = inputHOC(defaultValidationHOC(Checkbox));

View file

@ -1,4 +1,3 @@
const omit = require('lodash.omit');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const React = require('react'); const React = require('react');
@ -8,10 +7,15 @@ const React = require('react');
* @return {React.Component} a wrapped input component * @return {React.Component} a wrapped input component
*/ */
module.exports = Component => { module.exports = Component => {
const InputComponent = props => ( const InputComponent = ({
messages = {'general.notRequired': 'Not Required'},
required,
...props
}) => (
<Component <Component
help={props.required ? null : props.messages['general.notRequired']} help={required ? null : messages['general.notRequired']}
{...omit(props, ['messages'])} required={required}
{...props}
/> />
); );
@ -22,11 +26,5 @@ module.exports = Component => {
required: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]) required: PropTypes.oneOfType([PropTypes.bool, PropTypes.string])
}; };
InputComponent.defaultProps = {
messages: {
'general.notRequired': 'Not Required'
}
};
return InputComponent; return InputComponent;
}; };

View file

@ -8,12 +8,21 @@ const thumbnailUrl = require('../../lib/user-thumbnail');
require('./grid.scss'); require('./grid.scss');
const Grid = props => ( const Grid = ({
<div className={classNames('grid', props.className)}> className = '',
itemType = 'projects',
items = require('./grid.json'),
showAvatar = false,
showFavorites = false,
showLoves = false,
showRemixes = false,
showViews = false
}) => (
<div className={classNames('grid', className)}>
<FlexRow> <FlexRow>
{props.items.map((item, key) => { {items.map((item, key) => {
const href = `/${props.itemType}/${item.id}/`; const href = `/${itemType}/${item.id}/`;
if (props.itemType === 'projects') { if (itemType === 'projects') {
return ( return (
<Thumbnail <Thumbnail
avatar={thumbnailUrl(item.author.id)} avatar={thumbnailUrl(item.author.id)}
@ -23,11 +32,11 @@ const Grid = props => (
key={key} key={key}
loves={item.stats.loves} loves={item.stats.loves}
remixes={item.stats.remixes} remixes={item.stats.remixes}
showAvatar={props.showAvatar} showAvatar={showAvatar}
showFavorites={props.showFavorites} showFavorites={showFavorites}
showLoves={props.showLoves} showLoves={showLoves}
showRemixes={props.showRemixes} showRemixes={showRemixes}
showViews={props.showViews} showViews={showViews}
src={item.image} src={item.image}
title={item.title} title={item.title}
type={'project'} type={'project'}
@ -63,14 +72,4 @@ Grid.propTypes = {
showViews: PropTypes.bool showViews: PropTypes.bool
}; };
Grid.defaultProps = {
items: require('./grid.json'),
itemType: 'projects',
showLoves: false,
showFavorites: false,
showRemixes: false,
showViews: false,
showAvatar: false
};
module.exports = Grid; module.exports = Grid;

View file

@ -3,12 +3,18 @@ const PropTypes = require('prop-types');
const React = require('react'); const React = require('react');
const FormattedMessage = require('react-intl').FormattedMessage; const FormattedMessage = require('react-intl').FormattedMessage;
const HelpForm = props => { const HelpForm = ({
body = '',
subject = '',
title = '',
user = {username: ''}
}) => {
const prefix = 'https://mitscratch.freshdesk.com/widgets/feedback_widget/new?&widgetType=embedded&widgetView=yes&screenshot=No&searchArea=No'; const prefix = 'https://mitscratch.freshdesk.com/widgets/feedback_widget/new?&widgetType=embedded&widgetView=yes&screenshot=No&searchArea=No';
const title = `formTitle=${props.title}`; const formTitle = `formTitle=${title}`;
const username = `helpdesk_ticket[custom_field][cf_scratch_name_40167]=${props.user.username || ''}`; const username = `helpdesk_ticket[custom_field][cf_scratch_name_40167]=${user.username || ''}`;
const formSubject = `helpdesk_ticket[subject]=${props.subject}`; const formSubject = `helpdesk_ticket[subject]=${subject}`;
const reportLink = `helpdesk_ticket[custom_field][cf_inappropriate_report_link_40167]=${props.body || ''}`; const reportLink = `helpdesk_ticket[custom_field][cf_inappropriate_report_link_40167]=${body || ''}`;
return ( return (
<div> <div>
<script <script
@ -29,7 +35,7 @@ const HelpForm = props => {
height="644px" height="644px"
id="freshwidget-embedded-form" id="freshwidget-embedded-form"
scrolling="no" scrolling="no"
src={`${prefix}&${title}&${username}&${formSubject}&${reportLink}`} src={`${prefix}&${formTitle}&${username}&${formSubject}&${reportLink}`}
title={<FormattedMessage id="contactUs.questionsForum" />} title={<FormattedMessage id="contactUs.questionsForum" />}
width="100%" width="100%"
/> />
@ -49,13 +55,6 @@ HelpForm.propTypes = {
}) })
}; };
HelpForm.defaultProps = {
body: '',
subject: '',
title: '',
user: {username: ''}
};
const mapStateToProps = state => ({ const mapStateToProps = state => ({
user: state.session.session.user user: state.session.session.user
}); });

View file

@ -20,7 +20,7 @@ const JoinFlowStep = ({
onSubmit, onSubmit,
title, title,
titleClassName, titleClassName,
waiting waiting = false
}) => ( }) => (
<form <form
autoComplete="off" autoComplete="off"

View file

@ -1,4 +1,3 @@
const omit = require('lodash.omit');
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const injectIntl = require('react-intl').injectIntl; const injectIntl = require('react-intl').injectIntl;
@ -9,19 +8,24 @@ const ModalTitle = require('../modal/base/modal-title.jsx');
require('./next-step-button.scss'); require('./next-step-button.scss');
const NextStepButton = props => ( const NextStepButton = ({
content,
intl,
waiting = false,
...restProps
} = {}) => (
<button <button
className="modal-flush-bottom-button" className="modal-flush-bottom-button"
disabled={props.waiting} disabled={waiting}
type="submit" type="submit"
{...omit(props, ['intl', 'text', 'waiting'])} {...restProps}
> >
{props.waiting ? ( {waiting ? (
<Spinner className="next-step-spinner" /> <Spinner className="next-step-spinner" />
) : ( ) : (
<ModalTitle <ModalTitle
className="next-step-title" className="next-step-title"
title={props.content ? props.content : props.intl.formatMessage({id: 'general.next'})} title={content ? content : intl.formatMessage({id: 'general.next'})}
/> />
)} )}
</button> </button>
@ -33,8 +37,4 @@ NextStepButton.propTypes = {
waiting: PropTypes.bool waiting: PropTypes.bool
}; };
NextStepButton.defaultProps = {
waiting: false
};
module.exports = injectIntl(NextStepButton); module.exports = injectIntl(NextStepButton);

View file

@ -12,47 +12,48 @@ const ModalNavigation = ({
onBackPage, onBackPage,
nextButtonText, nextButtonText,
prevButtonText, prevButtonText,
nextButtonImageSrc, nextButtonImageSrc = '/images/onboarding/right-arrow.svg',
prevButtonImageSrc, prevButtonImageSrc = '/images/onboarding/left-arrow.svg',
className className
}) => { }) => {
useEffect(() => { useEffect(() => {
new Image().src = nextButtonImageSrc; new Image().src = nextButtonImageSrc;
new Image().src = prevButtonImageSrc; new Image().src = prevButtonImageSrc;
}, []); }, [nextButtonImageSrc, prevButtonImageSrc]);
const dots = useMemo(() => { const dots = useMemo(() => {
const dotsComponents = []; const dotsComponents = [];
if (currentPage >= 0 && totalDots){ if (currentPage >= 0 && totalDots) {
for (let i = 0; i < totalDots; i++){ for (let i = 0; i < totalDots; i++) {
dotsComponents.push(<div dotsComponents.push(
key={`dot page-${currentPage} ${i}`} <div
className={`dot ${currentPage === i && 'active'}`} key={`dot page-${currentPage} ${i}`}
/>); className={`dot ${currentPage === i && 'active'}`}
/>
);
} }
} }
return dotsComponents; return dotsComponents;
}, [currentPage, totalDots]); }, [currentPage, totalDots]);
return ( return (
<div className={classNames('navigation', className)}> <div className={classNames('navigation', className)}>
{ <Button
<Button onClick={onBackPage}
onClick={onBackPage} className={classNames('navigation-button', {
className={classNames('navigation-button', { hidden: !onBackPage,
hidden: !onBackPage, transparent: !prevButtonText
transparent: !prevButtonText })}
})} >
> <img
<img className="left-arrow"
className="left-arrow" alt=""
alt="" src={prevButtonImageSrc}
src={prevButtonImageSrc} />
/> <span className="navText">
<span className="navText"> {prevButtonText}
{prevButtonText} </span>
</span> </Button>
</Button> }
{(currentPage >= 0 && totalDots) && {(currentPage >= 0 && totalDots) &&
<div className="dotRow"> <div className="dotRow">
{dots} {dots}
@ -76,6 +77,7 @@ const ModalNavigation = ({
); );
}; };
ModalNavigation.propTypes = { ModalNavigation.propTypes = {
currentPage: PropTypes.number, currentPage: PropTypes.number,
totalDots: PropTypes.number, totalDots: PropTypes.number,
@ -88,9 +90,4 @@ ModalNavigation.propTypes = {
className: PropTypes.string className: PropTypes.string
}; };
ModalNavigation.defaultProps = {
nextButtonImageSrc: '/images/onboarding/right-arrow.svg',
prevButtonImageSrc: '/images/onboarding/left-arrow.svg'
};
export default ModalNavigation; export default ModalNavigation;

View file

@ -10,6 +10,8 @@ require('slick-carousel/slick/slick.scss');
require('slick-carousel/slick/slick-theme.scss'); require('slick-carousel/slick/slick-theme.scss');
require('./nestedcarousel.scss'); require('./nestedcarousel.scss');
const defaultItems = require('./nestedcarousel.json');
/* /*
NestedCarousel is used to show a carousel, where each slide is composed of a few NestedCarousel is used to show a carousel, where each slide is composed of a few
@ -18,8 +20,12 @@ require('./nestedcarousel.scss');
Each slide has a title, and then a list of thumbnails, that will be shown together. Each slide has a title, and then a list of thumbnails, that will be shown together.
*/ */
const NestedCarousel = props => { const NestedCarousel = ({
defaults(props.settings, { className,
items = defaultItems,
settings = {}
}) => {
defaults(settings, {
dots: true, dots: true,
infinite: false, infinite: false,
lazyLoad: true, lazyLoad: true,
@ -27,36 +33,32 @@ const NestedCarousel = props => {
slidesToScroll: 1, slidesToScroll: 1,
variableWidth: false variableWidth: false
}); });
const arrows = items.length > settings.slidesToShow;
const arrows = props.items.length > props.settings.slidesToShow;
const stages = []; const stages = [];
for (let i = 0; i < props.items.length; i++) { for (let i = 0; i < items.length; i++) {
const items = props.items[i].thumbnails; const thumbnails = items[i].thumbnails.map((item, j) => (
const thumbnails = []; <Thumbnail
for (let j = 0; j < items.length; j++) { key={`inner_${i}_${j}`}
const item = items[j]; linkTitle={false}
thumbnails.push( src={item.thumbnailUrl}
<Thumbnail title={item.title}
key={`inner_${i}_${j}`} />
linkTitle={false} ));
src={item.thumbnailUrl}
title={item.title}
/>
);
}
stages.push( stages.push(
<div key={`outer_${i}`}> <div key={`outer_${i}`}>
<h3>{props.items[i].title}</h3> <h3>{items[i].title}</h3>
{thumbnails} {thumbnails}
</div> </div>
); );
} }
return ( return (
<Slider <Slider
arrows={arrows} arrows={arrows}
className={classNames('nestedcarousel', 'carousel', props.className)} className={classNames('nestedcarousel', 'carousel', className)}
{...props.settings} {...settings}
> >
{stages} {stages}
</Slider> </Slider>
@ -76,9 +78,4 @@ NestedCarousel.propTypes = {
}) })
}; };
NestedCarousel.defaultProps = {
settings: {},
items: require('./nestedcarousel.json')
};
module.exports = NestedCarousel; module.exports = NestedCarousel;

View file

@ -5,15 +5,23 @@ const Box = require('../box/box.jsx');
require('./news.scss'); require('./news.scss');
const News = props => ( const defaultItems = require('./news.json');
const News = ({
items = defaultItems,
messages = {
'general.viewAll': 'View All',
'news.scratchNews': 'Scratch News'
}
}) => (
<Box <Box
className="news" className="news"
moreHref="/discuss/5/" moreHref="/discuss/5/"
moreTitle={props.messages['general.viewAll']} moreTitle={messages['general.viewAll']}
title={props.messages['news.scratchNews']} title={messages['news.scratchNews']}
> >
<ul> <ul>
{props.items.map(item => ( {items.map(item => (
<li key={item.id}> <li key={item.id}>
<a href={item.url}> <a href={item.url}>
<img <img
@ -42,12 +50,4 @@ News.propTypes = {
}) })
}; };
News.defaultProps = {
items: require('./news.json'),
messages: {
'general.viewAll': 'View All',
'news.scratchNews': 'Scratch News'
}
};
module.exports = News; module.exports = News;

View file

@ -11,51 +11,62 @@ const OS_ENUM = require('../../lib/os-enum.js');
require('./os-chooser.scss'); require('./os-chooser.scss');
const OSChooser = props => ( const OSChooser = ({
currentOS,
handleSetOS,
hideAndroid = false,
hideChromeOS = false,
hideMac = false,
hideWindows = false
}) => (
<div className="os-chooser"> <div className="os-chooser">
<FlexRow className="inner"> <FlexRow className="inner">
<FormattedMessage id="oschooser.choose" /> <FormattedMessage id="oschooser.choose" />
{!props.hideWindows && ( {!hideWindows && (
<Button <Button
className={classNames({active: props.currentOS === OS_ENUM.WINDOWS})} className={classNames({active: currentOS === OS_ENUM.WINDOWS})}
onClick={() => // eslint-disable-line react/jsx-no-bind onClick={() => handleSetOS(OS_ENUM.WINDOWS)} // eslint-disable-line react/jsx-no-bind
props.handleSetOS(OS_ENUM.WINDOWS)
}
> >
<img src="/svgs/extensions/windows.svg" /> <img
src="/svgs/extensions/windows.svg"
alt=""
/>
Windows Windows
</Button> </Button>
)} )}
{!props.hideMac && ( {!hideMac && (
<Button <Button
className={classNames({active: props.currentOS === OS_ENUM.MACOS})} className={classNames({active: currentOS === OS_ENUM.MACOS})}
onClick={() => // eslint-disable-line react/jsx-no-bind onClick={() => handleSetOS(OS_ENUM.MACOS)} // eslint-disable-line react/jsx-no-bind
props.handleSetOS(OS_ENUM.MACOS)
}
> >
<img src="/svgs/extensions/mac.svg" /> <img
src="/svgs/extensions/mac.svg"
alt=""
/>
macOS macOS
</Button> </Button>
)} )}
{!props.hideChromeOS && ( {!hideChromeOS && (
<Button <Button
className={classNames({active: props.currentOS === OS_ENUM.CHROMEOS})} className={classNames({active: currentOS === OS_ENUM.CHROMEOS})}
onClick={() => // eslint-disable-line react/jsx-no-bind onClick={() => handleSetOS(OS_ENUM.CHROMEOS)} // eslint-disable-line react/jsx-no-bind
props.handleSetOS(OS_ENUM.CHROMEOS)
}
> >
<img src="/svgs/extensions/chromeos.svg" /> <img
src="/svgs/extensions/chromeos.svg"
alt=""
/>
ChromeOS ChromeOS
</Button> </Button>
)} )}
{!props.hideAndroid && ( {!hideAndroid && (
<Button <Button
className={classNames({active: props.currentOS === OS_ENUM.ANDROID})} className={classNames({active: currentOS === OS_ENUM.ANDROID})}
onClick={() => // eslint-disable-line react/jsx-no-bind onClick={() => handleSetOS(OS_ENUM.ANDROID)} // eslint-disable-line react/jsx-no-bind
props.handleSetOS(OS_ENUM.ANDROID)
}
> >
<img src="/svgs/extensions/android.svg" /> <img
src="/svgs/extensions/android.svg"
alt=""
/>
Android Android
</Button> </Button>
)} )}
@ -72,13 +83,6 @@ OSChooser.propTypes = {
hideWindows: PropTypes.bool hideWindows: PropTypes.bool
}; };
OSChooser.defaultProps = {
hideAndroid: false,
hideChromeOS: false,
hideMac: false,
hideWindows: false
};
const wrappedOSChooser = injectIntl(OSChooser); const wrappedOSChooser = injectIntl(OSChooser);
module.exports = wrappedOSChooser; module.exports = wrappedOSChooser;

View file

@ -8,7 +8,11 @@ import overflowIcon from './overflow-icon.svg';
import './overflow-menu.scss'; import './overflow-menu.scss';
const OverflowMenu = ({children, dropdownAs, className}) => { const OverflowMenu = ({
children,
dropdownAs = 'ul',
className
}) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
<div className={classNames('overflow-menu-container', className)}> <div className={classNames('overflow-menu-container', className)}>
@ -38,8 +42,4 @@ OverflowMenu.propTypes = {
className: PropTypes.string className: PropTypes.string
}; };
OverflowMenu.defaultProps = {
dropdownAs: 'ul'
};
export default OverflowMenu; export default OverflowMenu;

View file

@ -4,9 +4,12 @@ const Avatar = require('../../components/avatar/avatar.jsx');
require('./people-grid.scss'); require('./people-grid.scss');
const PeopleGrid = props => ( const PeopleGrid = ({
linkToNewTab = false,
people
}) => (
<ul className="avatar-grid"> <ul className="avatar-grid">
{props.people.map((person, index) => ( {people.map((person, index) => (
<li <li
className="avatar-item" className="avatar-item"
key={`person-${index}`} key={`person-${index}`}
@ -16,7 +19,7 @@ const PeopleGrid = props => (
<a <a
href={`https://scratch.mit.edu/users/${person.userName}/`} href={`https://scratch.mit.edu/users/${person.userName}/`}
rel="noreferrer noopener" rel="noreferrer noopener"
target={props.linkToNewTab ? '_blank' : '_self'} target={linkToNewTab ? '_blank' : '_self'}
> >
<Avatar <Avatar
alt="" alt=""
@ -27,7 +30,7 @@ const PeopleGrid = props => (
/* if userName is not given, there's no chance userId is given */ /* if userName is not given, there's no chance userId is given */
<Avatar <Avatar
alt="" alt=""
src={`https://uploads.scratch.mit.edu/get_image/user/default_80x80.png`} src="https://uploads.scratch.mit.edu/get_image/user/default_80x80.png"
/> />
)} )}
</div> </div>
@ -48,8 +51,4 @@ PeopleGrid.propTypes = {
})) }))
}; };
PeopleGrid.defaultProps = {
linkToNewTab: false
};
module.exports = PeopleGrid; module.exports = PeopleGrid;

View file

@ -2,18 +2,24 @@ const classNames = require('classnames');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const React = require('react'); const React = require('react');
const Progression = props => { const Progression = ({
children,
className,
step = 0,
...restProps
}) => {
const childProps = { const childProps = {
activeStep: props.step, activeStep: step,
totalSteps: React.Children.count(props.children) totalSteps: React.Children.count(children)
}; };
return ( return (
<div <div
className={classNames('progression', props.className)} className={classNames('progression', className)}
{...props} {...restProps}
> >
{React.Children.map(props.children, (child, id) => { {React.Children.map(children, (child, id) => {
if (id === props.step) { if (id === step) {
return React.cloneElement(child, childProps); return React.cloneElement(child, childProps);
} }
})} })}
@ -38,8 +44,4 @@ Progression.propTypes = {
} }
}; };
Progression.defaultProps = {
step: 0
};
module.exports = Progression; module.exports = Progression;

View file

@ -7,8 +7,18 @@ import {framelessDetailed} from '../../lib/frameless.js';
import Carousel from '../carousel/carousel.jsx'; import Carousel from '../carousel/carousel.jsx';
import './projects-carousel.scss'; import './projects-carousel.scss';
export const ProjectsCarousel = props => { export const ProjectsCarousel = ({
defaults(props.settings, { className,
items,
settings = {},
showLoves,
showRemixes,
title,
type,
useDetailedBreakpoints,
...restProps
}) => {
defaults(settings, {
slidesToShow: 5, slidesToShow: 5,
arrows: true, arrows: true,
slidesToScroll: 1, slidesToScroll: 1,
@ -48,15 +58,25 @@ export const ProjectsCarousel = props => {
} }
] ]
}); });
return ( return (
<div className={classNames('projects-carousel', props.className)}> <div className={classNames('projects-carousel', className)}>
<div className="header"> <div className="header">
{props.title} {title}
</div> </div>
<div className="content"> <div className="content">
<Carousel {...props} /> <Carousel
className={className}
items={items}
settings={settings}
showLoves={showLoves}
showRemixes={showRemixes}
title={title}
type={type}
useDetailedBreakpoints={useDetailedBreakpoints}
{...restProps}
/>
</div> </div>
</div> </div>
); );
@ -81,7 +101,3 @@ ProjectsCarousel.propTypes = {
type: PropTypes.string, type: PropTypes.string,
useDetailedBreakpoints: PropTypes.bool useDetailedBreakpoints: PropTypes.bool
}; };
ProjectsCarousel.defaultProps = {
settings: {}
};

View file

@ -1,7 +1,6 @@
/* eslint-disable react/no-multi-comp */ /* eslint-disable react/no-multi-comp */
const bindAll = require('lodash.bindall'); const bindAll = require('lodash.bindall');
const {injectIntl, FormattedMessage} = require('react-intl'); const {injectIntl, FormattedMessage} = require('react-intl');
const omit = require('lodash.omit');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const React = require('react'); const React = require('react');
@ -48,16 +47,20 @@ const getCountryOptions = reactIntl => (
] ]
); );
const NextStepButton = props => ( const NextStepButton = ({
waiting = false,
text = 'Next Step',
...restProps
}) => (
<Button <Button
className="card-button" className="card-button"
disabled={props.waiting} disabled={waiting}
type="submit" type="submit"
{...omit(props, ['text', 'waiting'])} {...restProps}
> >
{props.waiting ? {waiting ?
<Spinner /> : <Spinner /> :
props.text text
} }
</Button> </Button>
); );
@ -67,11 +70,6 @@ NextStepButton.propTypes = {
waiting: PropTypes.bool waiting: PropTypes.bool
}; };
NextStepButton.defaultProps = {
waiting: false,
text: 'Next Step'
};
/* /*
* USERNAME STEP * USERNAME STEP
@ -626,7 +624,13 @@ const IntlDemographicsStep = injectIntl(DemographicsStep);
/* /*
* NAME STEP * NAME STEP
*/ */
const NameStep = props => ( const NameStep = ({
activeStep,
intl,
onNextStep,
totalSteps,
waiting = false
}) => (
<Slide className="registration-step name-step"> <Slide className="registration-step name-step">
<h2> <h2>
<FormattedMessage id="teacherRegistration.nameStepTitleNew" /> <FormattedMessage id="teacherRegistration.nameStepTitleNew" />
@ -634,25 +638,19 @@ const NameStep = props => (
<p className="description"> <p className="description">
<FormattedMessage id="teacherRegistration.nameStepDescription" /> <FormattedMessage id="teacherRegistration.nameStepDescription" />
<Tooltip <Tooltip
tipContent={ tipContent={intl.formatMessage({id: 'registration.nameStepTooltip'})}
props.intl.formatMessage({id: 'registration.nameStepTooltip'})
}
title={'?'} title={'?'}
/> />
</p> </p>
<Card> <Card>
<Form onValidSubmit={props.onNextStep}> <Form onValidSubmit={onNextStep}>
<Input <Input
required required
label={ label={intl.formatMessage({id: 'teacherRegistration.firstName'})}
props.intl.formatMessage({id: 'teacherRegistration.firstName'})
}
name="user.name.first" name="user.name.first"
type="text" type="text"
validationErrors={{ validationErrors={{
maxLength: props.intl.formatMessage({ maxLength: intl.formatMessage({id: 'registration.validationMaxLength'})
id: 'registration.validationMaxLength'
})
}} }}
validations={{ validations={{
maxLength: 50 maxLength: 50
@ -660,15 +658,11 @@ const NameStep = props => (
/> />
<Input <Input
required required
label={ label={intl.formatMessage({id: 'teacherRegistration.lastName'})}
props.intl.formatMessage({id: 'teacherRegistration.lastName'})
}
name="user.name.last" name="user.name.last"
type="text" type="text"
validationErrors={{ validationErrors={{
maxLength: props.intl.formatMessage({ maxLength: intl.formatMessage({id: 'registration.validationMaxLength'})
id: 'registration.validationMaxLength'
})
}} }}
validations={{ validations={{
maxLength: 50 maxLength: 50
@ -676,13 +670,13 @@ const NameStep = props => (
/> />
<NextStepButton <NextStepButton
text={<FormattedMessage id="registration.nextStep" />} text={<FormattedMessage id="registration.nextStep" />}
waiting={props.waiting} waiting={waiting}
/> />
</Form> </Form>
</Card> </Card>
<StepNavigation <StepNavigation
active={props.activeStep} active={activeStep}
steps={props.totalSteps - 1} steps={totalSteps - 1}
/> />
</Slide> </Slide>
); );
@ -695,10 +689,6 @@ NameStep.propTypes = {
waiting: PropTypes.bool waiting: PropTypes.bool
}; };
NameStep.defaultProps = {
waiting: false
};
const IntlNameStep = injectIntl(NameStep); const IntlNameStep = injectIntl(NameStep);
@ -1297,7 +1287,11 @@ const Link = chunks => <a href={chunks}>{chunks}</a>;
/* /*
* TEACHER APPROVAL STEP * TEACHER APPROVAL STEP
*/ */
const TeacherApprovalStep = props => ( const TeacherApprovalStep = ({
confirmed = false,
email = null,
invited = false
}) => (
<Slide className="registration-step last-step"> <Slide className="registration-step last-step">
<h2> <h2>
<FormattedMessage id="registration.lastStepTitle" /> <FormattedMessage id="registration.lastStepTitle" />
@ -1305,7 +1299,7 @@ const TeacherApprovalStep = props => (
<p className="description"> <p className="description">
<FormattedMessage id="registration.lastStepDescription" /> <FormattedMessage id="registration.lastStepDescription" />
</p> </p>
{props.confirmed || !props.email ? {confirmed || !email ?
[] : ( [] : (
<Card className="confirm"> <Card className="confirm">
<h4><FormattedMessage id="registration.confirmYourEmail" /></h4> <h4><FormattedMessage id="registration.confirmYourEmail" /></h4>
@ -1327,11 +1321,11 @@ const TeacherApprovalStep = props => (
<FormattedMessage <FormattedMessage
id="teacherRegistration.confirmationEmail" id="teacherRegistration.confirmationEmail"
/> />
<strong>{props.email}</strong></p> <strong>{email}</strong></p>
</Card> </Card>
) )
} }
{props.invited ? {invited ?
<Card className="wait"> <Card className="wait">
<h4><FormattedMessage id="registration.waitForApproval" /></h4> <h4><FormattedMessage id="registration.waitForApproval" /></h4>
<p> <p>
@ -1366,12 +1360,6 @@ TeacherApprovalStep.propTypes = {
invited: PropTypes.bool invited: PropTypes.bool
}; };
TeacherApprovalStep.defaultProps = {
confirmed: false,
email: null,
invited: false
};
const IntlTeacherApprovalStep = injectIntl(TeacherApprovalStep); const IntlTeacherApprovalStep = injectIntl(TeacherApprovalStep);

View file

@ -7,27 +7,35 @@ const RelativeTime = require('../relative-time/relative-time.jsx');
require('./social-message.scss'); require('./social-message.scss');
const SocialMessage = props => ( const SocialMessage = ({
<props.as className={classNames('social-message', props.className)}> as: Tag = 'li',
children,
className,
datetime,
iconAlt,
iconSrc,
imgClassName
}) => (
<Tag className={classNames('social-message', className)}>
<FlexRow className="mod-social-message"> <FlexRow className="mod-social-message">
<div className="social-message-content"> <div className="social-message-content">
{typeof props.iconSrc === 'undefined' ? [] : [ {typeof iconSrc === 'undefined' ? [] : [
<img <img
alt={props.iconAlt} alt={iconAlt}
className={classNames('social-message-icon', props.imgClassName)} className={classNames('social-message-icon', imgClassName)}
key="social-message-icon" key="social-message-icon"
src={props.iconSrc} src={iconSrc}
/> />
]} ]}
<div> <div>
{props.children} {children}
</div> </div>
</div> </div>
<span className="social-message-date"> <span className="social-message-date">
<RelativeTime value={new Date(props.datetime)} /> <RelativeTime value={new Date(datetime)} />
</span> </span>
</FlexRow> </FlexRow>
</props.as> </Tag>
); );
SocialMessage.propTypes = { SocialMessage.propTypes = {
@ -40,8 +48,4 @@ SocialMessage.propTypes = {
imgClassName: PropTypes.string imgClassName: PropTypes.string
}; };
SocialMessage.defaultProps = {
as: 'li'
};
module.exports = SocialMessage; module.exports = SocialMessage;

View file

@ -5,10 +5,7 @@ const classNames = require('classnames');
require('./spinner.scss'); require('./spinner.scss');
// Adapted from http://tobiasahlin.com/spinkit/ // Adapted from http://tobiasahlin.com/spinkit/
const Spinner = ({ const Spinner = ({className, color = 'white'}) => (
className,
color
}) => (
<img <img
alt="loading animation" alt="loading animation"
className={classNames('studio-status-icon-spinner', className)} className={classNames('studio-status-icon-spinner', className)}
@ -16,10 +13,6 @@ const Spinner = ({
/> />
); );
Spinner.defaultProps = {
color: 'white'
};
Spinner.propTypes = { Spinner.propTypes = {
className: PropTypes.string, className: PropTypes.string,
color: PropTypes.oneOf(['white', 'blue', 'transparent-gray']) color: PropTypes.oneOf(['white', 'blue', 'transparent-gray'])

View file

@ -8,21 +8,21 @@ require('./subnavigation.scss');
* Container for a custom, horizontal list of navigation elements * Container for a custom, horizontal list of navigation elements
* that can be displayed within a view or component. * that can be displayed within a view or component.
*/ */
const SubNavigation = props => ( const SubNavigation = ({align = 'middle', children, className, role}) => (
<div <div
className={classNames( className={classNames(
[ [
'sub-nav', 'sub-nav',
props.className className
], ],
{ {
'sub-nav-align-left': props.align === 'left', 'sub-nav-align-left': align === 'left',
'sub-nav-align-right': props.align === 'right' 'sub-nav-align-right': align === 'right'
} }
)} )}
role={props.role} role={role}
> >
{props.children} {children}
</div> </div>
); );
@ -33,8 +33,4 @@ SubNavigation.propTypes = {
className: PropTypes.string className: PropTypes.string
}; };
SubNavigation.defaultProps = {
align: 'middle'
};
module.exports = SubNavigation; module.exports = SubNavigation;

View file

@ -11,34 +11,45 @@ const FlexRow = require('../flex-row/flex-row.jsx');
require('./teacher-banner.scss'); require('./teacher-banner.scss');
const TeacherBanner = props => ( const TeacherBanner = ({
<TitleBanner className={classNames('teacher-banner', props.className)}> className,
messages = {
'teacherbanner.greeting': 'Hi',
'teacherbanner.subgreeting': 'Teacher Account',
'teacherbanner.classesButton': 'My Classes',
'teacherbanner.resourcesButton': 'Educator Resources',
'teacherbanner.faqButton': 'Teacher Account FAQ'
},
sessionStatus,
user = {}
}) => (
<TitleBanner className={classNames('teacher-banner', className)}>
<FlexRow className="inner"> <FlexRow className="inner">
<div className="welcome"> <div className="welcome">
{props.sessionStatus === sessionActions.Status.FETCHED ? ( {sessionStatus === sessionActions.Status.FETCHED ? (
props.user ? [ user ? [
<h3 key="greeting"> <h3 key="greeting">
{props.messages['teacherbanner.greeting']},{' '} {messages['teacherbanner.greeting']},{' '}
{props.user.username} {user.username}
</h3>, </h3>,
<p <p
className="title-banner-p" className="title-banner-p"
key="subgreeting" key="subgreeting"
> >
{props.messages['teacherbanner.subgreeting']} {messages['teacherbanner.subgreeting']}
</p> </p>
] : [] ] : []
) : []} ) : []}
</div> </div>
<FlexRow className="quick-links"> <FlexRow className="quick-links">
{props.sessionStatus === sessionActions.Status.FETCHED ? ( {sessionStatus === sessionActions.Status.FETCHED ? (
props.user ? [ user ? [
<a <a
href="/educators/classes" href="/educators/classes"
key="classes-button" key="classes-button"
> >
<Button> <Button>
{props.messages['teacherbanner.classesButton']} {messages['teacherbanner.classesButton']}
</Button> </Button>
</a>, </a>,
<a <a
@ -46,7 +57,7 @@ const TeacherBanner = props => (
key="resources-button" key="resources-button"
> >
<Button> <Button>
{props.messages['teacherbanner.resourcesButton']} {messages['teacherbanner.resourcesButton']}
</Button> </Button>
</a>, </a>,
<a <a
@ -54,7 +65,7 @@ const TeacherBanner = props => (
key="faq-button" key="faq-button"
> >
<Button> <Button>
{props.messages['teacherbanner.faqButton']} {messages['teacherbanner.faqButton']}
</Button> </Button>
</a> </a>
] : [] ] : []
@ -79,20 +90,9 @@ TeacherBanner.propTypes = {
}) })
}; };
TeacherBanner.defaultProps = {
messages: {
'teacherbanner.greeting': 'Hi',
'teacherbanner.subgreeting': 'Teacher Account',
'teacherbanner.classesButton': 'My Classes',
'teacherbanner.resourcesButton': 'Educator Resources',
'teacherbanner.faqButton': 'Teacher Account FAQ'
},
user: {}
};
const mapStateToProps = state => ({ const mapStateToProps = state => ({
sessionStatus: state.session.status, sessionStatus: state.session.status,
user: state.session.session.user user: state.session.session.user || {}
}); });
const ConnectedTeacherBanner = connect(mapStateToProps)(TeacherBanner); const ConnectedTeacherBanner = connect(mapStateToProps)(TeacherBanner);

View file

@ -4,51 +4,70 @@ const React = require('react');
require('./thumbnail.scss'); require('./thumbnail.scss');
const Thumbnail = props => { const Thumbnail = ({
alt = '',
avatar = '',
className,
creator,
favorites,
href = '#',
linkTitle = true,
loves,
remixes,
showAvatar = false,
showFavorites = false,
showLoves = false,
showRemixes = false,
showViews = false,
src = '',
title = 'Project',
type = 'project',
views
}) => {
const extra = []; const extra = [];
const info = []; const info = [];
if (props.loves && props.showLoves) { if (loves && showLoves) {
extra.push( extra.push(
<div <div
className="thumbnail-loves" className="thumbnail-loves"
key="loves" key="loves"
title={`${props.loves} loves`} title={`${loves} loves`}
> >
{props.loves} {loves}
</div> </div>
); );
} }
if (props.favorites && props.showFavorites) { if (favorites && showFavorites) {
extra.push( extra.push(
<div <div
className="thumbnail-favorites" className="thumbnail-favorites"
key="favorites" key="favorites"
title={`${props.favorites} favorites`} title={`${favorites} favorites`}
> >
{props.favorites} {favorites}
</div> </div>
); );
} }
if (props.remixes && props.showRemixes) { if (remixes && showRemixes) {
extra.push( extra.push(
<div <div
className="thumbnail-remixes" className="thumbnail-remixes"
key="remixes" key="remixes"
title={`${props.remixes} remixes`} title={`${remixes} remixes`}
> >
{props.remixes} {remixes}
</div> </div>
); );
} }
if (props.views && props.showViews) { if (views && showViews) {
extra.push( extra.push(
<div <div
className="thumbnail-views" className="thumbnail-views"
key="views" key="views"
title={`${props.views} views`} title={`${views} views`}
> >
{props.views} {views}
</div> </div>
); );
} }
@ -57,65 +76,66 @@ const Thumbnail = props => {
let titleElement; let titleElement;
let avatarElement; let avatarElement;
if (props.linkTitle) { if (linkTitle) {
imgElement = ( imgElement = (
<a <a
className="thumbnail-image" className="thumbnail-image"
href={props.href} href={href}
key="imgElement" key="imgElement"
> >
<img <img
alt={props.alt} alt={alt}
src={props.src} src={src}
/> />
</a> </a>
); );
titleElement = ( titleElement = (
<a <a
href={props.href} href={href}
key="titleElement" key="titleElement"
title={props.title} title={title}
> >
{props.title} {title}
</a> </a>
); );
} else { } else {
imgElement = <img src={props.src} />; imgElement = <img src={src} />;
titleElement = props.title; titleElement = title;
} }
info.push(titleElement); info.push(titleElement);
if (props.creator) { if (creator) {
info.push( info.push(
<div <div
className="thumbnail-creator" className="thumbnail-creator"
key="creator" key="creator"
> >
<a href={`/users/${props.creator}/`}>{props.creator}</a> <a href={`/users/${creator}/`}>{creator}</a>
</div> </div>
); );
} }
if (props.avatar && props.showAvatar) { if (avatar && showAvatar) {
avatarElement = ( avatarElement = (
<a <a
className="creator-image" className="creator-image"
href={`/users/${props.creator}/`} href={`/users/${creator}/`}
> >
<img <img
alt={props.creator} alt={creator}
src={props.avatar} src={avatar}
/> />
</a> </a>
); );
} }
return ( return (
<div <div
className={classNames( className={classNames(
'thumbnail', 'thumbnail',
props.type, type,
props.className className
)} )}
> >
{imgElement} {imgElement}
@ -151,19 +171,4 @@ Thumbnail.propTypes = {
views: PropTypes.oneOfType([PropTypes.number, PropTypes.string]) views: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
}; };
Thumbnail.defaultProps = {
alt: '',
avatar: '',
href: '#',
linkTitle: true,
showAvatar: false,
showFavorites: false,
showLoves: false,
showRemixes: false,
showViews: false,
src: '',
title: 'Project',
type: 'project'
};
module.exports = Thumbnail; module.exports = Thumbnail;

View file

@ -8,11 +8,20 @@ const thumbnailUrl = require('../../lib/user-thumbnail');
require('./thumbnailcolumn.scss'); require('./thumbnailcolumn.scss');
const ThumbnailColumn = props => ( const ThumbnailColumn = ({
<FlexRow className={classNames('thumbnail-column', props.className)}> className,
{props.items.map((item, key) => { itemType = 'projects',
const href = `/${props.itemType}/${item.id}/`; items,
if (props.itemType === 'projects') { showAvatar = false,
showFavorites = false,
showLoves = false,
showRemixes = false,
showViews = false
}) => (
<FlexRow className={classNames('thumbnail-column', className)}>
{items.map((item, key) => {
const href = `/${itemType}/${item.id}/`;
if (itemType === 'projects') {
return ( return (
<Thumbnail <Thumbnail
avatar={thumbnailUrl(item.author.id)} avatar={thumbnailUrl(item.author.id)}
@ -22,11 +31,11 @@ const ThumbnailColumn = props => (
key={key} key={key}
loves={item.stats.loves} loves={item.stats.loves}
remixes={item.stats.remixes} remixes={item.stats.remixes}
showAvatar={props.showAvatar} showAvatar={showAvatar}
showFavorites={props.showFavorites} showFavorites={showFavorites}
showLoves={props.showLoves} showLoves={showLoves}
showRemixes={props.showRemixes} showRemixes={showRemixes}
showViews={props.showViews} showViews={showViews}
src={item.image} src={item.image}
title={item.title} title={item.title}
type={'project'} type={'project'}
@ -59,13 +68,4 @@ ThumbnailColumn.propTypes = {
showViews: PropTypes.bool showViews: PropTypes.bool
}; };
ThumbnailColumn.defaultProps = {
itemType: 'projects',
showLoves: false,
showFavorites: false,
showRemixes: false,
showViews: false,
showAvatar: false
};
module.exports = ThumbnailColumn; module.exports = ThumbnailColumn;

View file

@ -4,12 +4,17 @@ const React = require('react');
require('./tooltip.scss'); require('./tooltip.scss');
const Tooltip = props => ( const Tooltip = ({
className,
currentCharacters,
maxCharacters,
tipContent = ''
}) => (
<span <span
className={classNames( className={classNames(
'tooltip', 'tooltip',
props.className, className,
{overmax: (props.currentCharacters > props.maxCharacters)} {overmax: currentCharacters > maxCharacters}
)} )}
> >
<span className="tip"> <span className="tip">
@ -19,7 +24,7 @@ const Tooltip = props => (
/> />
</span> </span>
<span className="expand"> <span className="expand">
{props.tipContent} {tipContent}
</span> </span>
</span> </span>
); );
@ -31,9 +36,4 @@ Tooltip.propTypes = {
tipContent: PropTypes.node tipContent: PropTypes.node
}; };
Tooltip.defaultProps = {
title: '',
tipContent: ''
};
module.exports = Tooltip; module.exports = Tooltip;

View file

@ -6,17 +6,30 @@ const Box = require('../box/box.jsx');
require('./welcome.scss'); require('./welcome.scss');
const Welcome = props => ( const Welcome = ({
messages = {
'welcome.welcomeToScratch': 'Welcome to Scratch!',
'welcome.explore': 'Explore Starter Projects',
'welcome.exploreAlt': 'Starter Projects',
'welcome.community': 'Learn about the community',
'welcome.communityAlt': 'Community Guidelines',
'welcome.create': 'Create a Project',
'welcome.createAlt': 'Get Started'
},
onDismiss,
permissions,
user
}) => (
<Box <Box
className="welcome" className="welcome"
moreHref="#" moreHref="#"
moreProps={{ moreProps={{
className: 'close', className: 'close',
title: 'Dismiss', title: 'Dismiss',
onClick: props.onDismiss onClick: onDismiss
}} }}
moreTitle="x" moreTitle="x"
title={props.messages['welcome.welcomeToScratch']} title={messages['welcome.welcomeToScratch']}
> >
<div className="welcome-options"> <div className="welcome-options">
<a <a
@ -24,9 +37,9 @@ const Welcome = props => (
className="welcome-option-button" className="welcome-option-button"
href="/starter-projects" href="/starter-projects"
> >
{props.messages['welcome.explore']} {messages['welcome.explore']}
<img <img
alt={props.messages['welcome.exploreAlt']} alt={messages['welcome.exploreAlt']}
src="/images/explore_starter_projects.svg" src="/images/explore_starter_projects.svg"
/> />
</a> </a>
@ -35,9 +48,9 @@ const Welcome = props => (
className="welcome-option-button" className="welcome-option-button"
href="/community_guidelines" href="/community_guidelines"
> >
{props.messages['welcome.community']} {messages['welcome.community']}
<img <img
alt={props.messages['welcome.communityAlt']} alt={messages['welcome.communityAlt']}
src="/images/learn_about_the_community.svg" src="/images/learn_about_the_community.svg"
/> />
</a> </a>
@ -45,14 +58,14 @@ const Welcome = props => (
id="welcome.create" id="welcome.create"
className="welcome-option-button" className="welcome-option-button"
href={ href={
shouldDisplayOnboarding(props.user, props.permissions) ? shouldDisplayOnboarding(user, permissions) ?
'/projects/editor/' : '/projects/editor/' :
'/projects/editor/?tutorial=getStarted' '/projects/editor/?tutorial=getStarted'
} }
> >
{props.messages['welcome.create']} {messages['welcome.create']}
<img <img
alt={props.messages['welcome.createAlt']} alt={messages['welcome.createAlt']}
src="/images/create_a_project.svg" src="/images/create_a_project.svg"
/> />
</a> </a>
@ -75,16 +88,4 @@ Welcome.propTypes = {
user: PropTypes.object user: PropTypes.object
}; };
Welcome.defaultProps = {
messages: {
'welcome.welcomeToScratch': 'Welcome to Scratch!',
'welcome.explore': 'Explore Starter Projects',
'welcome.exploreAlt': 'Starter Projects',
'welcome.community': 'Learn about the community',
'welcome.communityAlt': 'Community Guidelines',
'welcome.create': 'Create a Project',
'welcome.createAlt': 'Get Started'
}
};
module.exports = Welcome; module.exports = Welcome;

View file

@ -6,7 +6,7 @@ const classNames = require('classnames');
import './youtube-video-modal.scss'; import './youtube-video-modal.scss';
export const YoutubeVideoModal = ({videoId, onClose = () => {}, className}) => { export const YoutubeVideoModal = ({videoId, onClose = () => {}, className = 'mint-green'}) => {
if (!videoId) return null; if (!videoId) return null;
return ( return (
<ReactModal <ReactModal
@ -37,12 +37,10 @@ export const YoutubeVideoModal = ({videoId, onClose = () => {}, className}) => {
); );
}; };
YoutubeVideoModal.defaultProps = {
className: 'mint-green'
};
YoutubeVideoModal.propTypes = { YoutubeVideoModal.propTypes = {
videoId: PropTypes.string, videoId: PropTypes.string,
onClose: PropTypes.func, onClose: PropTypes.func,
className: PropTypes.string className: PropTypes.string
}; };
export default YoutubeVideoModal;

View file

@ -251,12 +251,24 @@ SocialMessagesList.defaultProps = {
numNewMessages: 0 numNewMessages: 0
}; };
const MessagesPresentation = props => { export const MessagesPresentation = ({
let adminMessageLength = props.adminMessages.length; adminMessages,
if (Object.keys(props.scratcherInvite).length > 0) { filter = '',
intl,
loadMore,
messages,
numNewMessages = 0,
onAdminDismiss,
onFilterClick,
onLoadMoreMethod,
requestStatus,
scratcherInvite
}) => {
let adminMessageLength = adminMessages.length;
if (Object.keys(scratcherInvite).length > 0) {
adminMessageLength = adminMessageLength + 1; adminMessageLength = adminMessageLength + 1;
} }
let numNewSocialMessages = props.numNewMessages - adminMessageLength; let numNewSocialMessages = numNewMessages - adminMessageLength;
if (numNewSocialMessages < 0) { if (numNewSocialMessages < 0) {
numNewSocialMessages = 0; numNewSocialMessages = 0;
} }
@ -271,39 +283,24 @@ const MessagesPresentation = props => {
<div className="messages-title-filter"> <div className="messages-title-filter">
<Form> <Form>
<Select <Select
label={props.intl.formatMessage({id: 'messages.filterBy'})} label={intl.formatMessage({id: 'messages.filterBy'})}
name="messages.filter" name="messages.filter"
options={[ options={[
{ {label: intl.formatMessage({id: 'messages.activityAll'}), value: ''},
label: props.intl.formatMessage({id: 'messages.activityAll'}), {label: intl.formatMessage({id: 'messages.activityComments'}), value: 'comments'},
value: '' {label: intl.formatMessage({id: 'messages.activityProjects'}), value: 'projects'},
}, {label: intl.formatMessage({id: 'messages.activityStudios'}), value: 'studios'},
{ {label: intl.formatMessage({id: 'messages.activityForums'}), value: 'forums'}
label: props.intl.formatMessage({id: 'messages.activityComments'}),
value: 'comments'
},
{
label: props.intl.formatMessage({id: 'messages.activityProjects'}),
value: 'projects'
},
{
label: props.intl.formatMessage({id: 'messages.activityStudios'}),
value: 'studios'
},
{
label: props.intl.formatMessage({id: 'messages.activityForums'}),
value: 'forums'
}
]} ]}
value={props.filter} value={filter}
onChange={props.onFilterClick} onChange={onFilterClick}
/> />
</Form> </Form>
</div> </div>
</FlexRow> </FlexRow>
</TitleBanner> </TitleBanner>
<div className="messages-details inner"> <div className="messages-details inner">
{props.adminMessages.length > 0 || Object.keys(props.scratcherInvite).length > 0 ? [ {adminMessages.length > 0 || Object.keys(scratcherInvite).length > 0 ? [
<section <section
className="messages-admin" className="messages-admin"
key="messages-admin" key="messages-admin"
@ -317,31 +314,31 @@ const MessagesPresentation = props => {
</h4> </h4>
</div> </div>
<ul className="messages-admin-list"> <ul className="messages-admin-list">
{Object.keys(props.scratcherInvite).length > 0 ? [ {Object.keys(scratcherInvite).length > 0 ? [
<ScratcherInvite <ScratcherInvite
datetimeCreated={props.scratcherInvite.datetime_created} datetimeCreated={scratcherInvite.datetime_created}
id={props.scratcherInvite.id} id={scratcherInvite.id}
key={`invite${props.scratcherInvite.id}`} key={`invite${scratcherInvite.id}`}
onDismiss={() => { // eslint-disable-line react/jsx-no-bind onDismiss={() => { // eslint-disable-line react/jsx-no-bind
props.onAdminDismiss('invite', props.scratcherInvite.id); onAdminDismiss('invite', scratcherInvite.id);
}} }}
/> />
] : []} ] : []}
{props.adminMessages.map(item => ( {adminMessages.map(item => (
<AdminMessage <AdminMessage
datetimeCreated={item.datetime_created} datetimeCreated={item.datetime_created}
id={item.id} id={item.id}
key={`adminmessage${item.id}`} key={`adminmessage${item.id}`}
message={item.message} message={item.message}
onDismiss={() => { // eslint-disable-line react/jsx-no-bind onDismiss={() => { // eslint-disable-line react/jsx-no-bind
props.onAdminDismiss('notification', item.id); onAdminDismiss('notification', item.id);
}} }}
/> />
))} ))}
</ul> </ul>
</section> </section>
] : []} ] : []}
{props.requestStatus.admin === messageStatuses.ADMIN_ERROR ? [ {requestStatus.admin === messageStatuses.ADMIN_ERROR ? [
<section <section
className="messages-admin" className="messages-admin"
key="messages-admin-error" key="messages-admin-error"
@ -353,11 +350,11 @@ const MessagesPresentation = props => {
</section> </section>
] : []} ] : []}
<SocialMessagesList <SocialMessagesList
loadMore={props.loadMore} loadMore={loadMore}
loadStatus={props.requestStatus.message} loadStatus={requestStatus.message}
messages={props.messages} messages={messages}
numNewMessages={numNewSocialMessages} numNewMessages={numNewSocialMessages}
onLoadMoreMethod={props.onLoadMoreMethod} onLoadMoreMethod={onLoadMoreMethod}
/> />
</div> </div>
</div> </div>
@ -380,23 +377,7 @@ MessagesPresentation.propTypes = {
message: PropTypes.string, message: PropTypes.string,
delete: PropTypes.string delete: PropTypes.string
}).isRequired, }).isRequired,
scratcherInvite: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types scratcherInvite: PropTypes.object.isRequired // eslint-disable-line react/forbid-prop-types
user: PropTypes.shape({
id: PropTypes.number,
banned: PropTypes.bool,
vpn_required: PropTypes.bool,
token: PropTypes.string,
thumbnailUrl: PropTypes.string,
dateJoined: PropTypes.string,
email: PropTypes.string,
classroomId: PropTypes.string
}).isRequired
};
MessagesPresentation.defaultProps = {
filter: '',
filterOpen: false,
numNewMessages: 0
}; };
module.exports = injectIntl(MessagesPresentation); module.exports = injectIntl(MessagesPresentation);

View file

@ -10,7 +10,7 @@ import {formatRelativeTime} from '../../lib/format-time.js';
const StudioMuteEditMessage = ({ const StudioMuteEditMessage = ({
className, className,
messageId, messageId = 'studio.mutedEdit',
muteExpiresAtMs muteExpiresAtMs
}) => ( }) => (
<ValidationMessage <ValidationMessage
@ -32,10 +32,6 @@ StudioMuteEditMessage.propTypes = {
muteExpiresAtMs: PropTypes.number muteExpiresAtMs: PropTypes.number
}; };
StudioMuteEditMessage.defaultProps = {
messageId: 'studio.mutedEdit'
};
export default connect( export default connect(
state => ({ state => ({
muteExpiresAtMs: (selectMuteStatus(state).muteExpiresAt * 1000 || 0) muteExpiresAtMs: (selectMuteStatus(state).muteExpiresAt * 1000 || 0)

View file

@ -1,45 +1,13 @@
import React from 'react'; import React from 'react';
import {render} from '@testing-library/react'; import {render} from '@testing-library/react';
import {IntlProvider} from 'react-intl'; import {IntlProvider} from 'react-intl';
import routes from '../../src/routes.json'; import {generatedLocales} from '../generated/generated-locales';
import path from 'path';
import fs from 'fs';
import merge from 'lodash.merge';
// TBD: Move code to script that executes before running all tests,
// fix issue where texts for views don't load
const globalTemplateFile = path.resolve(__dirname, '../../src/l10n.json');
const generalLocales = {en: JSON.parse(fs.readFileSync(globalTemplateFile, 'utf8'))};
const defaultLocales = {};
const views = [];
for (const route in routes) {
if (typeof routes[route].redirect !== 'undefined') {
continue;
}
views.push(routes[route].name);
try {
const subdir = routes[route].view.split('/');
subdir.pop();
const l10n = path.resolve(__dirname, `../../src/views/${subdir.join('/')}/l10n.json`);
const viewIds = JSON.parse(fs.readFileSync(l10n, 'utf8'));
defaultLocales[routes[route].name] = viewIds;
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
}
}
views.map(view => defaultLocales[view]).reduce((acc, curr) => merge(acc, curr), generalLocales);
const renderWithIntl = ui => ({ const renderWithIntl = ui => ({
...render( ...render(
<IntlProvider <IntlProvider
locale="en" locale="en"
messages={generalLocales.en} messages={generatedLocales.en}
> >
{ui} {ui}
</IntlProvider> </IntlProvider>

View file

@ -1,68 +0,0 @@
import {render} from '@testing-library/react';
import {renderWithIntl} from './intl-helpers';
const findNode = (fiberNode, selector, comparator) => {
if (fiberNode &&
fiberNode.stateNode &&
fiberNode.stateNode.state &&
comparator(selector, fiberNode)) {
return fiberNode;
}
let currentNode;
if (!currentNode && fiberNode && fiberNode.child) {
currentNode = findNode(fiberNode.child, selector, comparator);
}
if (!currentNode && fiberNode && fiberNode.sibling) {
currentNode = findNode(fiberNode.sibling, selector, comparator);
}
return currentNode;
};
const getInstance = (container, selector, comparator) => {
const rootFiberKey = Object.keys(container).find(key =>
key.startsWith('__reactContainer')
);
const rootFiber = container[rootFiberKey];
return findNode(rootFiber.stateNode.current, selector, comparator) || null;
};
const compareComponentName = (componentName, fiberNode) => fiberNode.elementType?.name.startsWith(componentName);
const renderWithInstance = (ux, componentName) => {
const component = render(ux);
return {
instance: () => getInstance(
component.container,
componentName,
compareComponentName)?.stateNode,
findByComponentName: selector => getInstance(
component.container,
selector,
compareComponentName)?.stateNode,
...component
};
};
const renderWithInstanceAndIntl = (ux, componentName) => {
const component = renderWithIntl(ux);
return {
instance: () => getInstance(
component.container,
componentName,
compareComponentName)?.stateNode,
findByComponentName: selector => getInstance(
component.container,
selector,
compareComponentName)?.stateNode,
...component
};
};
export {renderWithInstance as render, renderWithInstanceAndIntl as renderWithIntl};

View file

@ -0,0 +1,146 @@
import React from 'react';
import {render} from '@testing-library/react';
import {renderWithIntl} from './intl-helpers';
import {IntlProvider} from 'react-intl';
import {generatedLocales} from '../generated/generated-locales';
const findNode = (fiberNode, selector, comparator) => {
if (fiberNode &&
comparator(selector, fiberNode)) {
return fiberNode;
}
let currentNode;
if (!currentNode && fiberNode && fiberNode.child) {
currentNode = findNode(fiberNode.child, selector, comparator);
}
if (!currentNode && fiberNode && fiberNode.sibling) {
currentNode = findNode(fiberNode.sibling, selector, comparator);
}
return currentNode;
};
const findAllNodes = (fiberNode, selector, comparator) => {
let currentNodes = [];
if (fiberNode &&
comparator(selector, fiberNode)) {
currentNodes.push(fiberNode);
}
if (fiberNode && fiberNode.child) {
currentNodes = [...currentNodes, ...findAllNodes(fiberNode.child, selector, comparator)];
}
if (fiberNode && fiberNode.sibling) {
currentNodes = [...currentNodes, ...findAllNodes(fiberNode.sibling, selector, comparator)];
}
if (fiberNode && fiberNode._reactInternals) {
currentNodes = [...currentNodes, ...findAllNodes(fiberNode._reactInternals, selector, comparator)];
}
return currentNodes;
};
const getInstance = (container, selector, comparator) => {
const rootFiberKey = Object.keys(container).find(key =>
key.startsWith('__reactContainer')
);
const rootFiber = container[rootFiberKey];
return findNode(rootFiber.stateNode.current, selector, comparator) || null;
};
const compareComponentName = (componentName, fiberNode) => fiberNode.elementType?.name?.startsWith(componentName);
const renderWithInstance = (ux, componentName) => {
const component = render(ux);
const instance = getInstance(
component.container,
componentName,
compareComponentName);
return {
instance: () => {
const node = getInstance(
component.container,
componentName,
compareComponentName);
return node?.stateNode || node;
},
findByComponentName: selector => {
const node = findNode(
instance,
selector,
compareComponentName);
return node?.stateNode || node;
},
findAllByComponentName: selector => {
const nodes = findAllNodes(
instance,
selector,
compareComponentName);
return nodes;
},
...component
};
};
const renderWithInstanceAndIntl = (ux, componentName) => {
const component = renderWithIntl(ux);
return {
instance: () => {
const node = getInstance(
component.container,
componentName,
compareComponentName);
return node?.stateNode || node;
},
findByComponentName: selector => {
const node = findNode(
getInstance(
component.container,
componentName,
compareComponentName),
selector,
compareComponentName);
return node?.stateNode || node;
},
findAllByComponentName: selector => {
const nodes = findAllNodes(
getInstance(
component.container,
componentName,
compareComponentName),
selector,
compareComponentName);
return nodes;
},
rerenderWithIntl: newUx => {
component.rerender(
<IntlProvider
locale="en"
messages={generatedLocales.en}
>
{newUx ?? ux}
</IntlProvider>
);
},
...component
};
};
export {renderWithInstance as render, renderWithInstanceAndIntl as renderWithIntl};

View file

@ -1,14 +1,14 @@
/* eslint-disable max-len */ /* eslint-disable max-len */
const React = require('react'); const React = require('react');
const {renderWithIntl} = require('../../helpers/intl-helpers.jsx'); import {renderWithIntl} from '../../helpers/react-testing-library-wrapper.jsx';
import {ConnectedBecomeAScratcher as BecomeAScratcherPage} from '../../../src/views/become-a-scratcher/become-a-scratcher.jsx'; import {ConnectedBecomeAScratcher as BecomeAScratcherPage} from '../../../src/views/become-a-scratcher/become-a-scratcher.jsx';
import sessionActions from '../../../src/redux/session.js'; import sessionActions from '../../../src/redux/session.js';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import {Provider} from 'react-redux';
jest.mock('react-dom', () => ({
render: jest.fn() jest.mock('../../../src/lib/render.jsx', () => jest.fn());
}));
jest.mock('../../../src/components/modal/base/modal.jsx', () => () => 'MockModal'); jest.mock('../../../src/components/modal/base/modal.jsx', () => () => 'MockModal');
@ -26,7 +26,9 @@ describe('BecomeAScratcherPage', () => {
} }
}); });
const {container} = renderWithIntl( const {container} = renderWithIntl(
<BecomeAScratcherPage />, {context: {store: NotLoggedInUserStore}} <Provider store={NotLoggedInUserStore}>
<BecomeAScratcherPage />
</Provider>
); );
expect(container.querySelector('div.not-available-outer')).toBeInTheDocument(); expect(container.querySelector('div.not-available-outer')).toBeInTheDocument();
}); });
@ -47,9 +49,11 @@ describe('BecomeAScratcherPage', () => {
} }
}); });
const {container} = renderWithIntl( const {container} = renderWithIntl(
<BecomeAScratcherPage />, {context: {store: NotInvitedUserStore}} <Provider store={NotInvitedUserStore}>
<BecomeAScratcherPage />
</Provider>
); );
expect(container.querySelector('div.no-invitation').exists()).toBeInTheDocument(); expect(container.querySelector('div.no-invitation')).toBeInTheDocument();
}); });
test('Display Onboarding when user is invited', () => { test('Display Onboarding when user is invited', () => {
@ -69,9 +73,11 @@ describe('BecomeAScratcherPage', () => {
} }
}); });
const {container} = renderWithIntl( const {container} = renderWithIntl(
<BecomeAScratcherPage />, {context: {store: InvitedUserStore}} <Provider store={InvitedUserStore}>
<BecomeAScratcherPage />
</Provider>
); );
expect(container.querySelector('div.congratulations-page').exists()).toBeInTheDocument(); expect(container.querySelector('div.congratulations-page')).toBeInTheDocument();
}); });
test('Display celebration page when user is already a scratcher', () => { test('Display celebration page when user is already a scratcher', () => {
@ -91,8 +97,10 @@ describe('BecomeAScratcherPage', () => {
} }
}); });
const {container} = renderWithIntl( const {container} = renderWithIntl(
<BecomeAScratcherPage />, {context: {store: AlreadyScratcherStore}} <Provider store={AlreadyScratcherStore}>
<BecomeAScratcherPage />
</Provider>
); );
expect(container.querySelector('div.hooray-screen').exists()).toBeInTheDocument(); expect(container.querySelector('div.hooray-screen')).toBeInTheDocument();
}); });
}); });

View file

@ -1,7 +1,7 @@
const React = require('react'); const React = require('react');
const Captcha = require('../../../src/components/captcha/captcha.jsx'); const Captcha = require('../../../src/components/captcha/captcha.jsx');
const {render} = require('@testing-library/react'); import {render} from '../../helpers/react-testing-library-wrapper.jsx';
describe('Captcha test', () => { describe('Captcha test', () => {
global.grecaptcha = { global.grecaptcha = {
@ -25,10 +25,8 @@ describe('Captcha test', () => {
const props = { const props = {
onCaptchaLoad: jest.fn() onCaptchaLoad: jest.fn()
}; };
const {container} = (<Captcha const captchaInstance = render(<Captcha {...props} />, 'Captcha').instance();
{...props} captchaInstance.executeCaptcha();
/>);
container.executeCaptcha();
expect(global.grecaptcha.execute).toHaveBeenCalled(); expect(global.grecaptcha.execute).toHaveBeenCalled();
}); });

View file

@ -3,7 +3,7 @@ import {Provider} from 'react-redux';
const ComposeComment = require('../../../src/views/preview/comment/compose-comment.jsx'); const ComposeComment = require('../../../src/views/preview/comment/compose-comment.jsx');
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import {renderWithIntl} from '../../helpers/react-testing-library-wrapper.js'; import {renderWithIntl} from '../../helpers/react-testing-library-wrapper.jsx';
import {screen} from '@testing-library/react'; import {screen} from '@testing-library/react';
describe('Compose Comment test', () => { describe('Compose Comment test', () => {

View file

@ -1,3 +1,4 @@
/* eslint-disable max-len */
import React from 'react'; import React from 'react';
import {renderWithIntl} from '../../helpers/intl-helpers.jsx'; import {renderWithIntl} from '../../helpers/intl-helpers.jsx';
import DonateTopBanner from '../../../src/views/splash/donate/donate-banner'; import DonateTopBanner from '../../../src/views/splash/donate/donate-banner';
@ -14,27 +15,26 @@ describe('DonateBannerTest', () => {
}); });
test('Testing 2024 EOY campaign message', () => { test('Testing 2024 EOY campaign message', () => {
global.Date.now = () => new Date(2024, 11, 16).getTime(); global.Date.now = () => new Date(2024, 11, 16).getTime();
const {container} = renderWithIntl( const {container} = renderWithIntl(<DonateTopBanner />);
<DonateTopBanner />
);
expect(container.querySelector('div.donate-banner')).toBeInTheDocument(); expect(container.querySelector('div.donate-banner')).toBeInTheDocument();
expect(container.querySelector('p.donate-text')).toBeInTheDocument(); expect(container.querySelector('p.donate-text')).toBeInTheDocument();
const donateText = container.querySelector('p.donate-text'); const donateText = container.querySelector('p.donate-text');
expect(donateText.innerHTML).toEqual('donatebanner.eoyCampaign'); expect(donateText.textContent).toEqual(
'Scratch is a nonprofit that relies on donations to keep our platform free for all kids. Your gift of $5 will make a difference.'
);
}); });
test('testing default message comes back after January 9, 2025', () => { test('testing default message comes back after January 9, 2025', () => {
// Date after Scratch week // Date after Scratch week
global.Date.now = () => new Date(2025, 0, 10).getTime(); global.Date.now = () => new Date(2025, 0, 10).getTime();
const {container} = renderWithIntl( const {container} = renderWithIntl(<DonateTopBanner />);
<DonateTopBanner />
);
expect(container.querySelector('div.donate-banner')).toBeInTheDocument(); expect(container.querySelector('div.donate-banner')).toBeInTheDocument();
expect(container.querySelector('p.donate-text')).toBeInTheDocument(); expect(container.querySelector('p.donate-text')).toBeInTheDocument();
const donateText = container.querySelector('p.donate-text'); const donateText = container.querySelector('p.donate-text');
expect(donateText.innerHTML).toEqual('donatebanner.askSupport'); expect(donateText.textContent).toEqual(
"Scratch is the world's largest free coding community for kids. Your support makes a difference."
);
}); });
}); });

View file

@ -1,8 +1,5 @@
const React = require('react'); const React = require('react');
const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx'); import {renderWithIntl} from '../../helpers/react-testing-library-wrapper.jsx';
const {mountWithIntl} = require('../../helpers/intl-helpers.jsx');
const JoinFlowStep = require('../../../src/components/join-flow/join-flow-step.jsx');
const FormikInput = require('../../../src/components/formik-forms/formik-input.jsx');
const requestSuccessResponse = { const requestSuccessResponse = {
requestSucceeded: true, requestSucceeded: true,
@ -44,52 +41,55 @@ describe('EmailStep test', () => {
}); });
test('send correct props to formik', () => { test('send correct props to formik', () => {
const intlWrapper = shallowWithIntl(<EmailStep const {findByComponentName, instance} = renderWithIntl(<EmailStep
{...defaultProps()} {...defaultProps()}
/>); />, 'EmailStep');
const emailStepWrapper = intlWrapper.dive();
expect(emailStepWrapper.props().initialValues.email).toBe(''); const formikComponent = findByComponentName('Formik');
expect(emailStepWrapper.props().validateOnBlur).toBe(false); const emailStepInstance = instance();
expect(emailStepWrapper.props().validateOnChange).toBe(false);
expect(emailStepWrapper.props().validate).toBe(emailStepWrapper.instance().validateForm); expect(formikComponent.memoizedProps.initialValues.email).toBe('');
expect(emailStepWrapper.props().onSubmit).toBe(emailStepWrapper.instance().handleValidSubmit); expect(formikComponent.memoizedProps.validateOnBlur).toBe(false);
expect(formikComponent.memoizedProps.validateOnChange).toBe(false);
expect(formikComponent.memoizedProps.validate).toBe(emailStepInstance.validateForm);
expect(formikComponent.memoizedProps.onSubmit).toBe(emailStepInstance.handleValidSubmit);
}); });
test('props sent to JoinFlowStep', () => { test('props sent to JoinFlowStep', () => {
const intlWrapper = shallowWithIntl(<EmailStep const {findAllByComponentName} = renderWithIntl(<EmailStep
{...defaultProps()} {...defaultProps()}
/>); />, 'EmailStep');
// Dive to get past the intl wrapper
const emailStepWrapper = intlWrapper.dive(); const joinFlowSteps = findAllByComponentName('JoinFlowStep');
// Dive to get past the anonymous component. expect(joinFlowSteps).toHaveLength(1);
const joinFlowWrapper = emailStepWrapper.dive().find(JoinFlowStep); expect(joinFlowSteps[0].memoizedProps.footerContent.props.id).toBe('registration.acceptTermsOfUse');
expect(joinFlowWrapper).toHaveLength(1); expect(joinFlowSteps[0].memoizedProps.headerImgSrc).toBe('/images/join-flow/email-header.png');
expect(joinFlowWrapper.props().footerContent.props.id).toBe('registration.acceptTermsOfUse'); expect(joinFlowSteps[0].memoizedProps.innerClassName).toBe('join-flow-inner-email-step');
expect(joinFlowWrapper.props().headerImgSrc).toBe('/images/join-flow/email-header.png'); expect(joinFlowSteps[0].memoizedProps.nextButton).toBe('Create Your Account');
expect(joinFlowWrapper.props().innerClassName).toBe('join-flow-inner-email-step'); expect(joinFlowSteps[0].memoizedProps.title).toBe('What\'s your email?');
expect(joinFlowWrapper.props().nextButton).toBe('registration.createAccount'); expect(joinFlowSteps[0].memoizedProps.titleClassName).toBe('join-flow-email-title');
expect(joinFlowWrapper.props().title).toBe('registration.emailStepTitle'); expect(joinFlowSteps[0].memoizedProps.waiting).toBe(true);
expect(joinFlowWrapper.props().titleClassName).toBe('join-flow-email-title');
expect(joinFlowWrapper.props().waiting).toBe(true);
}); });
test('props sent to FormikInput for email', () => { test('props sent to FormikInput for email', () => {
const intlWrapper = shallowWithIntl(<EmailStep const {findByComponentName, findAllByComponentName, instance} = renderWithIntl(<EmailStep
{...defaultProps()} {...defaultProps()}
/>); />, 'EmailStep');
// Dive to get past the intl wrapper
const emailStepWrapper = intlWrapper.dive(); const emailStepInstance = instance();
// Dive to get past the anonymous component.
const joinFlowWrapper = emailStepWrapper.dive().find(JoinFlowStep); const joinFlowSteps = findAllByComponentName('JoinFlowStep');
expect(joinFlowWrapper).toHaveLength(1); expect(joinFlowSteps).toHaveLength(1);
const emailInputWrapper = joinFlowWrapper.find(FormikInput).first();
expect(emailInputWrapper.props().id).toEqual('email'); const emailInput = findByComponentName('FormikInput');
expect(emailInputWrapper.props().error).toBeUndefined();
expect(emailInputWrapper.props().name).toEqual('email'); expect(emailInput.memoizedProps.id).toEqual('email');
expect(emailInputWrapper.props().placeholder).toEqual('general.emailAddress'); expect(emailInput.memoizedProps.error).toBeUndefined();
expect(emailInputWrapper.props().validationClassName).toEqual('validation-full-width-input'); expect(emailInput.memoizedProps.name).toEqual('email');
expect(emailInputWrapper.props().onSetRef).toEqual(emailStepWrapper.instance().handleSetEmailRef); expect(emailInput.memoizedProps.placeholder).toEqual('Email address');
expect(emailInputWrapper.props().validate).toEqual(emailStepWrapper.instance().validateEmail); expect(emailInput.memoizedProps.validationClassName).toEqual('validation-full-width-input');
expect(emailInput.memoizedProps.onSetRef).toEqual(emailStepInstance.handleSetEmailRef);
expect(emailInput.memoizedProps.validate).toEqual(emailStepInstance.validateEmail);
}); });
test('handleValidSubmit passes formData to next step', () => { test('handleValidSubmit passes formData to next step', () => {
@ -101,14 +101,11 @@ describe('EmailStep test', () => {
executeCaptcha: jest.fn() executeCaptcha: jest.fn()
}; };
const formData = {item1: 'thing', item2: 'otherthing'}; const formData = {item1: 'thing', item2: 'otherthing'};
const intlWrapper = shallowWithIntl( const emailStepInstance = renderWithIntl(<EmailStep
<EmailStep {...defaultProps()}
{...defaultProps()} />, 'EmailStep').instance();
/>); emailStepInstance.setCaptchaRef(captchaRef);
emailStepInstance.handleValidSubmit(formData, formikBag);
const emailStepWrapper = intlWrapper.dive();
emailStepWrapper.instance().setCaptchaRef(captchaRef);
emailStepWrapper.instance().handleValidSubmit(formData, formikBag);
expect(formikBag.setSubmitting).toHaveBeenCalledWith(false); expect(formikBag.setSubmitting).toHaveBeenCalledWith(false);
expect(captchaRef.executeCaptcha).toHaveBeenCalled(); expect(captchaRef.executeCaptcha).toHaveBeenCalled();
@ -125,19 +122,16 @@ describe('EmailStep test', () => {
executeCaptcha: jest.fn() executeCaptcha: jest.fn()
}; };
const formData = {item1: 'thing', item2: 'otherthing'}; const formData = {item1: 'thing', item2: 'otherthing'};
const intlWrapper = shallowWithIntl( const emailStepInstance = renderWithIntl(<EmailStep
<EmailStep {...defaultProps()}
{...defaultProps()} {...props}
{...props} />, 'EmailStep').instance();
/>);
const emailStepWrapper = intlWrapper.dive(); emailStepInstance.setCaptchaRef(captchaRef); // to setup catpcha state
// Call these to setup captcha. emailStepInstance.handleValidSubmit(formData, formikBag);
emailStepWrapper.instance().setCaptchaRef(captchaRef); // to setup catpcha state
emailStepWrapper.instance().handleValidSubmit(formData, formikBag);
const captchaToken = 'abcd'; const captchaToken = 'abcd';
emailStepWrapper.instance().handleCaptchaSolved(captchaToken); emailStepInstance.handleCaptchaSolved(captchaToken);
// Make sure captchaSolved calls onNextStep with formData that has // Make sure captchaSolved calls onNextStep with formData that has
// a captcha token and left everything else in the object in place. // a captcha token and left everything else in the object in place.
expect(props.onNextStep).toHaveBeenCalledWith( expect(props.onNextStep).toHaveBeenCalledWith(
@ -152,7 +146,7 @@ describe('EmailStep test', () => {
test('Component logs analytics', () => { test('Component logs analytics', () => {
const sendAnalyticsFn = jest.fn(); const sendAnalyticsFn = jest.fn();
const onCaptchaError = jest.fn(); const onCaptchaError = jest.fn();
mountWithIntl( renderWithIntl(
<EmailStep <EmailStep
sendAnalytics={sendAnalyticsFn} sendAnalytics={sendAnalyticsFn}
onCaptchaError={onCaptchaError} onCaptchaError={onCaptchaError}
@ -161,33 +155,30 @@ describe('EmailStep test', () => {
}); });
test('validateEmail test email empty', () => { test('validateEmail test email empty', () => {
const intlWrapper = shallowWithIntl( const emailStepInstance = renderWithIntl(<EmailStep
<EmailStep {...defaultProps()}
{...defaultProps()} />, 'EmailStep').instance();
/>);
const emailStepWrapper = intlWrapper.dive(); const val = emailStepInstance.validateEmail('');
const val = emailStepWrapper.instance().validateEmail(''); expect(val).toBe('Required');
expect(val).toBe('general.required');
}); });
test('validateEmail test email null', () => { test('validateEmail test email null', () => {
const intlWrapper = shallowWithIntl( const emailStepInstance = renderWithIntl(<EmailStep
<EmailStep {...defaultProps()}
{...defaultProps()} />, 'EmailStep').instance();
/>);
const emailStepWrapper = intlWrapper.dive(); const val = emailStepInstance.validateEmail(null);
const val = emailStepWrapper.instance().validateEmail(null); expect(val).toBe('Required');
expect(val).toBe('general.required');
}); });
test('validateEmail test email undefined', () => { test('validateEmail test email undefined', () => {
const intlWrapper = shallowWithIntl( const emailStepInstance = renderWithIntl(<EmailStep
<EmailStep {...defaultProps()}
{...defaultProps()} />, 'EmailStep').instance();
/>);
const emailStepWrapper = intlWrapper.dive(); const val = emailStepInstance.validateEmail();
const val = emailStepWrapper.instance().validateEmail(); expect(val).toBe('Required');
expect(val).toBe('general.required');
}); });
}); });
@ -198,12 +189,9 @@ describe('validateEmailRemotelyWithCache test with successful requests', () => {
}); });
test('validateEmailRemotelyWithCache calls validate.validateEmailRemotely', done => { test('validateEmailRemotelyWithCache calls validate.validateEmailRemotely', done => {
const intlWrapper = shallowWithIntl( const emailStepInstance = renderWithIntl(<EmailStep />, 'EmailStep').instance();
<EmailStep />
);
const instance = intlWrapper.dive().instance();
instance.validateEmailRemotelyWithCache('some-email@some-domain.com') emailStepInstance.validateEmailRemotelyWithCache('some-email@some-domain.com')
.then(response => { .then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalled(); expect(mockedValidateEmailRemotely).toHaveBeenCalled();
expect(response.valid).toBe(false); expect(response.valid).toBe(false);
@ -213,12 +201,9 @@ describe('validateEmailRemotelyWithCache test with successful requests', () => {
}); });
test('validateEmailRemotelyWithCache, called twice with different data, makes two remote requests', done => { test('validateEmailRemotelyWithCache, called twice with different data, makes two remote requests', done => {
const intlWrapper = shallowWithIntl( const emailStepInstance = renderWithIntl(<EmailStep />, 'EmailStep').instance();
<EmailStep />
);
const instance = intlWrapper.dive().instance();
instance.validateEmailRemotelyWithCache('some-email@some-domain.com') emailStepInstance.validateEmailRemotelyWithCache('some-email@some-domain.com')
.then(response => { .then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(1); expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(1);
expect(response.valid).toBe(false); expect(response.valid).toBe(false);
@ -226,7 +211,7 @@ describe('validateEmailRemotelyWithCache test with successful requests', () => {
}) })
.then(() => { .then(() => {
// make the same request a second time // make the same request a second time
instance.validateEmailRemotelyWithCache('different-email@some-domain.org') emailStepInstance.validateEmailRemotelyWithCache('different-email@some-domain.org')
.then(response => { .then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(2); expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(2);
expect(response.valid).toBe(false); expect(response.valid).toBe(false);
@ -237,12 +222,9 @@ describe('validateEmailRemotelyWithCache test with successful requests', () => {
}); });
test('validateEmailRemotelyWithCache, called twice with same data, only makes one remote request', done => { test('validateEmailRemotelyWithCache, called twice with same data, only makes one remote request', done => {
const intlWrapper = shallowWithIntl( const emailStepInstance = renderWithIntl(<EmailStep />, 'EmailStep').instance();
<EmailStep />
);
const instance = intlWrapper.dive().instance();
instance.validateEmailRemotelyWithCache('some-email@some-domain.com') emailStepInstance.validateEmailRemotelyWithCache('some-email@some-domain.com')
.then(response => { .then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(1); expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(1);
expect(response.valid).toBe(false); expect(response.valid).toBe(false);
@ -250,7 +232,7 @@ describe('validateEmailRemotelyWithCache test with successful requests', () => {
}) })
.then(() => { .then(() => {
// make the same request a second time // make the same request a second time
instance.validateEmailRemotelyWithCache('some-email@some-domain.com') emailStepInstance.validateEmailRemotelyWithCache('some-email@some-domain.com')
.then(response => { .then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(1); expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(1);
expect(response.valid).toBe(false); expect(response.valid).toBe(false);
@ -275,11 +257,9 @@ describe('validateEmailRemotelyWithCache test with failing requests', () => {
}); });
test('validateEmailRemotelyWithCache calls validate.validateEmailRemotely', done => { test('validateEmailRemotelyWithCache calls validate.validateEmailRemotely', done => {
const wrapper = shallowWithIntl( const emailStepInstance = renderWithIntl(<EmailStep />, 'EmailStep').instance();
<EmailStep />);
const instance = wrapper.dive().instance();
instance.validateEmailRemotelyWithCache('some-email@some-domain.com') emailStepInstance.validateEmailRemotelyWithCache('some-email@some-domain.com')
.then(response => { .then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalled(); expect(mockedValidateEmailRemotely).toHaveBeenCalled();
expect(response.valid).toBe(false); expect(response.valid).toBe(false);
@ -289,12 +269,9 @@ describe('validateEmailRemotelyWithCache test with failing requests', () => {
}); });
test('validateEmailRemotelyWithCache, called twice with different data, makes two remote requests', done => { test('validateEmailRemotelyWithCache, called twice with different data, makes two remote requests', done => {
const wrapper = shallowWithIntl( const emailStepInstance = renderWithIntl(<EmailStep />, 'EmailStep').instance();
<EmailStep />
);
const instance = wrapper.dive().instance();
instance.validateEmailRemotelyWithCache('some-email@some-domain.com') emailStepInstance.validateEmailRemotelyWithCache('some-email@some-domain.com')
.then(response => { .then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(1); expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(1);
expect(response.valid).toBe(false); expect(response.valid).toBe(false);
@ -302,7 +279,7 @@ describe('validateEmailRemotelyWithCache test with failing requests', () => {
}) })
.then(() => { .then(() => {
// make the same request a second time // make the same request a second time
instance.validateEmailRemotelyWithCache('different-email@some-domain.org') emailStepInstance.validateEmailRemotelyWithCache('different-email@some-domain.org')
.then(response => { .then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(2); expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(2);
expect(response.valid).toBe(false); expect(response.valid).toBe(false);
@ -313,12 +290,9 @@ describe('validateEmailRemotelyWithCache test with failing requests', () => {
}); });
test('validateEmailRemotelyWithCache, called 2x w/same data, makes 2 requests, since 1st not stored', done => { test('validateEmailRemotelyWithCache, called 2x w/same data, makes 2 requests, since 1st not stored', done => {
const wrapper = shallowWithIntl( const emailStepInstance = renderWithIntl(<EmailStep />, 'EmailStep').instance();
<EmailStep />
);
const instance = wrapper.dive().instance();
instance.validateEmailRemotelyWithCache('some-email@some-domain.com') emailStepInstance.validateEmailRemotelyWithCache('some-email@some-domain.com')
.then(response => { .then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(1); expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(1);
expect(response.valid).toBe(false); expect(response.valid).toBe(false);
@ -326,7 +300,7 @@ describe('validateEmailRemotelyWithCache test with failing requests', () => {
}) })
.then(() => { .then(() => {
// make the same request a second time // make the same request a second time
instance.validateEmailRemotelyWithCache('some-email@some-domain.com') emailStepInstance.validateEmailRemotelyWithCache('some-email@some-domain.com')
.then(response => { .then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(2); expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(2);
expect(response.valid).toBe(false); expect(response.valid).toBe(false);

View file

@ -1,24 +1,41 @@
import React from 'react'; import React, {useEffect, useRef, useState} from 'react';
const {mountWithIntl} = require('../../helpers/intl-helpers.jsx');
import CrashMessageComponent from '../../../src/components/crashmessage/crashmessage.jsx';
import ErrorBoundary from '../../../src/components/errorboundary/errorboundary.jsx'; import ErrorBoundary from '../../../src/components/errorboundary/errorboundary.jsx';
import {renderWithIntl} from '../../helpers/react-testing-library-wrapper.jsx';
import {fireEvent, screen} from '@testing-library/react';
import '@testing-library/jest-dom';
const ChildComponent = () => <div>hello</div>; const ChildComponent = () => {
const hasRendered = useRef(false);
const [text, setText] = useState('hello');
useEffect(() => {
if (hasRendered.current) {
throw new Error('This component has been re-rendered!');
} else {
hasRendered.current = true;
}
});
// eslint-disable-next-line react/jsx-no-bind
return <div onClick={() => setText('not hello')}>{text}</div>;
};
describe('ErrorBoundary', () => { describe('ErrorBoundary', () => {
test('ErrorBoundary shows children before error and CrashMessageComponent after', () => { test('ErrorBoundary shows children before error and CrashMessageComponent after', () => {
const child = <ChildComponent />; const child = <ChildComponent />;
const wrapper = mountWithIntl(<ErrorBoundary>{child}</ErrorBoundary>); const {findByComponentName} = renderWithIntl(
const childWrapper = wrapper.childAt(0); <ErrorBoundary>{child}</ErrorBoundary>,
'ErrorBoundary'
);
expect(wrapper.containsMatchingElement(child)).toBeTruthy(); expect(findByComponentName('ChildComponent')).toBeTruthy();
expect(wrapper.containsMatchingElement(<CrashMessageComponent />)).toBeFalsy(); expect(findByComponentName('CrashMessage')).toBeFalsy();
childWrapper.simulateError(new Error('fake error for testing purposes')); const helloDiv = screen.getByText('hello');
fireEvent.click(helloDiv);
expect(wrapper.containsMatchingElement(child)).toBeFalsy(); expect(findByComponentName('ChildComponent')).toBeFalsy();
expect(wrapper.containsMatchingElement(<CrashMessageComponent />)).toBeTruthy(); expect(findByComponentName('CrashMessage')).toBeTruthy();
}); });
}); });

View file

@ -1,13 +1,14 @@
import React from 'react'; import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import ExtensionRequirements from '../../../src/components/extension-landing/extension-requirements'; import ExtensionRequirements from '../../../src/components/extension-landing/extension-requirements';
import {renderWithIntl} from '../../helpers/react-testing-library-wrapper.jsx';
describe('ExtensionRequirements', () => { describe('ExtensionRequirements', () => {
test('shows default extension requirements', () => { test('shows default extension requirements', () => {
const component = mountWithIntl(<ExtensionRequirements />); const {container} = renderWithIntl(<ExtensionRequirements />, 'ExtensionRequirements');
const requirements = component.find('.extension-requirements span').map(span => span.text()); const spans = container.querySelectorAll('.extension-requirements span');
const requirements = Array.from(spans).map(span => span.textContent.trim());
expect(requirements).toEqual( expect(requirements).toEqual(
['Windows 10 version 1709+', 'macOS 10.15+', 'ChromeOS', 'Android 6.0+', 'Bluetooth', 'Scratch Link'] ['Windows 10 version 1709+', 'macOS 10.15+', 'ChromeOS', 'Android 6.0+', 'Bluetooth', 'Scratch Link']
@ -15,15 +16,15 @@ describe('ExtensionRequirements', () => {
}); });
test('hides requirements', () => { test('hides requirements', () => {
const component = mountWithIntl(<ExtensionRequirements const {container} = renderWithIntl(<ExtensionRequirements
hideWindows hideWindows
hideMac hideMac
hideChromeOS hideChromeOS
hideAndroid hideAndroid
hideBluetooth hideBluetooth
hideScratchLink hideScratchLink
/>); />, 'ExtensionRequirements');
expect(component.find('.extension-requirements span').length).toEqual(0); expect(container.querySelectorAll('.extension-requirements span').length).toEqual(0);
}); });
}); });

View file

@ -1,53 +1,50 @@
import React from 'react'; import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import FeedbackForm from '../../../src/components/modal/mute/feedback-form'; import FeedbackForm from '../../../src/components/modal/mute/feedback-form';
import {renderWithIntl} from '../../helpers/react-testing-library-wrapper';
describe('FeedbackFormTest', () => { describe('FeedbackFormTest', () => {
test('Feedback form empty feedback invalid', () => { test('Feedback form empty feedback invalid', () => {
const submitFn = jest.fn(); const submitFn = jest.fn();
const message = 'too short'; const message = 'too short';
const component = mountWithIntl( const feedbackFormInstance = renderWithIntl(
<FeedbackForm <FeedbackForm
emptyErrorMessage={message} emptyErrorMessage={message}
onSubmit={submitFn} onSubmit={submitFn}
/> />,
); 'FeedbackForm'
expect(component.find('FeedbackForm').instance() ).instance();
.validateFeedback('') expect(feedbackFormInstance.validateFeedback('')).toBe(message);
).toBe(message);
}); });
test('Feedback form shorter than minLength invalid', () => { test('Feedback form shorter than minLength invalid', () => {
const submitFn = jest.fn(); const submitFn = jest.fn();
const message = 'too short'; const message = 'too short';
const min = 7; const min = 7;
const component = mountWithIntl( const feedbackFormInstance = renderWithIntl(
<FeedbackForm <FeedbackForm
emptyErrorMessage={message} emptyErrorMessage={message}
minLength={min} minLength={min}
onSubmit={submitFn} onSubmit={submitFn}
/> />,
); 'FeedbackForm'
).instance();
expect(component.find('FeedbackForm').instance()
.validateFeedback('123456') expect(feedbackFormInstance.validateFeedback('123456')).toBe(message);
).toBe(message);
}); });
test('Feedback form greater than or equal to minLength invalid', () => { test('Feedback form greater than or equal to minLength invalid', () => {
const submitFn = jest.fn(); const submitFn = jest.fn();
const message = 'too short'; const message = 'too short';
const min = 7; const min = 7;
const component = mountWithIntl( const feedbackFormInstance = renderWithIntl(
<FeedbackForm <FeedbackForm
emptyErrorMessage={message} emptyErrorMessage={message}
minLength={min} minLength={min}
onSubmit={submitFn} onSubmit={submitFn}
/> />,
); 'FeedbackForm'
).instance();
expect(component.find('FeedbackForm').instance()
.validateFeedback('1234567') expect(feedbackFormInstance.validateFeedback('1234567')).toBeNull();
).toBeNull();
}); });
}); });

View file

@ -1,85 +1,92 @@
import React from 'react'; import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import FormikInput from '../../../src/components/formik-forms/formik-input.jsx'; import FormikInput from '../../../src/components/formik-forms/formik-input.jsx';
import {Formik} from 'formik'; import {Formik} from 'formik';
import '@testing-library/jest-dom';
import {renderWithIntl} from '../../helpers/react-testing-library-wrapper.jsx';
describe('FormikInput', () => { describe('FormikInput', () => {
test('No validation message with empty error, empty toolTip', () => { test('No validation message with empty error, empty toolTip', () => {
const component = mountWithIntl( const {container, findByComponentName} = renderWithIntl(
<Formik> <Formik>
<FormikInput <FormikInput
error="" error=""
toolTip="" toolTip=""
/> />
</Formik> </Formik>,
'Formik'
); );
expect(component.find('ValidationMessage').exists()).toEqual(false); expect(findByComponentName('ValidationMessage')).toBeFalsy();
expect(component.find('div.validation-error').exists()).toEqual(false); expect(container.querySelector('div.validation-error')).not.toBeInTheDocument();
expect(component.find('div.validation-info').exists()).toEqual(false); expect(container.querySelector('div.validation-info')).not.toBeInTheDocument();
}); });
test('No validation message with false error, false toolTip', () => { test('No validation message with false error, false toolTip', () => {
const component = mountWithIntl( const {container, findByComponentName} = renderWithIntl(
<Formik> <Formik>
<FormikInput <FormikInput
error={false} error={false}
toolTip={false} toolTip={false}
/> />
</Formik> </Formik>,
'Formik'
); );
expect(component.find('ValidationMessage').exists()).toEqual(false); expect(findByComponentName('ValidationMessage')).toBeFalsy();
expect(component.find('div.validation-error').exists()).toEqual(false); expect(container.querySelector('div.validation-error')).not.toBeInTheDocument();
expect(component.find('div.validation-info').exists()).toEqual(false); expect(container.querySelector('div.validation-info')).not.toBeInTheDocument();
}); });
test('No validation message with nonexistent error or toolTip', () => { test('No validation message with nonexistent error or toolTip', () => {
const component = mountWithIntl( const {container, findByComponentName} = renderWithIntl(
<Formik> <Formik>
<FormikInput /> <FormikInput />
</Formik> </Formik>,
'Formik'
); );
expect(component.find('ValidationMessage').exists()).toEqual(false); expect(findByComponentName('ValidationMessage')).toBeFalsy();
expect(component.find('div.validation-error').exists()).toEqual(false); expect(container.querySelector('div.validation-error')).not.toBeInTheDocument();
expect(component.find('div.validation-info').exists()).toEqual(false); expect(container.querySelector('div.validation-info')).not.toBeInTheDocument();
}); });
test('Validation message shown when error given', () => { test('Validation message shown when error given', () => {
const component = mountWithIntl( const {container, findByComponentName} = renderWithIntl(
<Formik> <Formik>
<FormikInput <FormikInput
error="There was an error" error="There was an error"
/> />
</Formik> </Formik>,
'Formik'
); );
expect(component.find('ValidationMessage').exists()).toEqual(true); expect(findByComponentName('ValidationMessage')).toBeTruthy();
expect(component.find('div.validation-error').exists()).toEqual(true); expect(container.querySelector('div.validation-error')).toBeInTheDocument();
expect(component.find('div.validation-info').exists()).toEqual(false); expect(container.querySelector('div.validation-info')).not.toBeInTheDocument();
}); });
test('Tooltip shown when toolTip given', () => { test('Tooltip shown when toolTip given', () => {
const component = mountWithIntl( const {container, findByComponentName} = renderWithIntl(
<Formik> <Formik>
<FormikInput <FormikInput
toolTip="Have fun out there!" toolTip="Have fun out there!"
/> />
</Formik> </Formik>,
'Formik'
); );
expect(component.find('ValidationMessage').exists()).toEqual(true); expect(findByComponentName('ValidationMessage')).toBeTruthy();
expect(component.find('div.validation-error').exists()).toEqual(false); expect(container.querySelector('div.validation-error')).not.toBeInTheDocument();
expect(component.find('div.validation-info').exists()).toEqual(true); expect(container.querySelector('div.validation-info')).toBeInTheDocument();
}); });
test('If both error and toolTip messages, error takes precedence', () => { test('If both error and toolTip messages, error takes precedence', () => {
const component = mountWithIntl( const {container, findByComponentName} = renderWithIntl(
<Formik> <Formik>
<FormikInput <FormikInput
error="There was an error" error="There was an error"
toolTip="Have fun out there!" toolTip="Have fun out there!"
/> />
</Formik> </Formik>,
'Formik'
); );
expect(component.find('ValidationMessage').exists()).toEqual(true); expect(findByComponentName('ValidationMessage')).toBeTruthy();
expect(component.find('div.validation-error').exists()).toEqual(true); expect(container.querySelector('div.validation-error')).toBeInTheDocument();
expect(component.find('div.validation-info').exists()).toEqual(false); expect(container.querySelector('div.validation-info')).not.toBeInTheDocument();
}); });
}); });

View file

@ -1,34 +1,36 @@
import React from 'react'; import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import FormikSelect from '../../../src/components/formik-forms/formik-select.jsx'; import FormikSelect from '../../../src/components/formik-forms/formik-select.jsx';
import {Field, Formik} from 'formik'; import {Formik} from 'formik';
import {renderWithIntl} from '../../helpers/react-testing-library-wrapper.jsx';
describe('FormikSelect', () => { describe('FormikSelect', () => {
test('No validation message without an error', () => { test('No validation message without an error', () => {
const component = mountWithIntl( const {findByComponentName} = renderWithIntl(
<Formik> <Formik>
<FormikSelect <FormikSelect
error="" error=""
options={[]} options={[]}
/> />
</Formik> </Formik>,
'Formik'
); );
expect(component.find('ValidationMessage').exists()).toEqual(false); expect(findByComponentName('ValidationMessage')).toBeFalsy();
expect(component.find(Field).exists()).toEqual(true); expect(findByComponentName('Field')).toBeTruthy();
}); });
test('Validation message shown when error present', () => { test('Validation message shown when error present', () => {
const component = mountWithIntl( const {findByComponentName} = renderWithIntl(
<Formik> <Formik>
<FormikSelect <FormikSelect
error="uh oh. error" error="uh oh. error"
options={[]} options={[]}
/> />
</Formik> </Formik>,
'Formik'
); );
expect(component.find('ValidationMessage').exists()).toEqual(true); expect(findByComponentName('ValidationMessage')).toBeTruthy();
expect(component.find(Field).exists()).toEqual(true); expect(findByComponentName('Field')).toBeTruthy();
}); });
test('list of options passed to formik', () => { test('list of options passed to formik', () => {
@ -45,15 +47,16 @@ describe('FormikSelect', () => {
} }
]; ];
const component = mountWithIntl( const {findByComponentName} = renderWithIntl(
<Formik> <Formik>
<FormikSelect <FormikSelect
error="" error=""
options={optionList} options={optionList}
/> />
</Formik> </Formik>,
'Formik'
); );
expect(component.find(Field).exists()).toEqual(true); expect(findByComponentName('Field')).toBeTruthy();
expect(component.find(Field).prop('children').length).toEqual(2); expect(findByComponentName('Field').memoizedProps.children.length).toEqual(2);
}); });
}); });

View file

@ -1,6 +1,7 @@
import React from 'react'; import React, {act} from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import InfoButton from '../../../src/components/info-button/info-button'; import InfoButton from '../../../src/components/info-button/info-button';
import {renderWithIntl} from '../../helpers/react-testing-library-wrapper.jsx';
import {fireEvent} from '@testing-library/react';
describe('InfoButton', () => { describe('InfoButton', () => {
// mock window.addEventListener // mock window.addEventListener
@ -13,125 +14,132 @@ describe('InfoButton', () => {
/* eslint-enable no-undef */ /* eslint-enable no-undef */
test('Info button defaults to not visible', () => { test('Info button defaults to not visible', () => {
const component = mountWithIntl( const {container} = renderWithIntl(
<InfoButton <InfoButton
message="Here is some info about something!" message="Here is some info about something!"
/> />
); );
expect(component.find('div.info-button-message').exists()).toEqual(false); expect(container.querySelector('div.info-button-message')).toBeFalsy();
}); });
test('mouseOver on info button makes info message visible', done => { test('mouseOver on info button makes info message visible', async () => {
const component = mountWithIntl( const {container} = renderWithIntl(
<InfoButton <InfoButton
message="Here is some info about something!" message="Here is some info about something!"
/> />
); );
// mouseOver info button await act(() => {
component.find('div.info-button').simulate('mouseOver'); fireEvent.mouseOver(container.querySelector(('div.info-button')));
setTimeout(() => { // necessary because mouseover uses debounce });
// crucial: if we don't call update(), then find() below looks through an OLD
// version of the DOM! see https://github.com/airbnb/enzyme/issues/1233#issuecomment-358915200 expect(container.querySelector('div.info-button-message')).toBeTruthy();
component.update();
expect(component.find('div.info-button-message').exists()).toEqual(true);
done();
}, 500);
}); });
test('clicking on info button makes info message visible', () => { test('clicking on info button makes info message visible', async () => {
const component = mountWithIntl( const {container, instance} = renderWithIntl(
<InfoButton <InfoButton
message="Here is some info about something!" message="Here is some info about something!"
/> />,
'InfoButton'
); );
const buttonRef = component.instance().buttonRef; const buttonRef = instance().buttonRef;
// click on info button await act(() => {
mockedAddEventListener.mousedown({target: buttonRef}); mockedAddEventListener.mousedown({target: buttonRef});
component.update(); });
expect(component.find('div.info-button').exists()).toEqual(true);
expect(component.find('div.info-button-message').exists()).toEqual(true); expect(container.querySelector('div.info-button')).toBeTruthy();
expect(container.querySelector('div.info-button-message')).toBeTruthy();
}); });
test('clicking on info button, then mousing out makes info message still appear', done => { test('clicking on info button, then mousing out makes info message still appear', async () => {
const component = mountWithIntl( const {container, instance} = renderWithIntl(
<InfoButton <InfoButton
message="Here is some info about something!" message="Here is some info about something!"
/> />,
'InfoButton'
); );
const buttonRef = component.instance().buttonRef; const buttonRef = instance().buttonRef;
await act(() => {
mockedAddEventListener.mousedown({target: buttonRef});
});
// click on info button expect(container.querySelector('div.info-button')).toBeTruthy();
mockedAddEventListener.mousedown({target: buttonRef}); expect(container.querySelector('div.info-button-message')).toBeTruthy();
component.update();
expect(component.find('div.info-button').exists()).toEqual(true);
expect(component.find('div.info-button-message').exists()).toEqual(true);
// mouseLeave from info button // mouseLeave from info button
component.find('div.info-button').simulate('mouseLeave'); await act(() => {
setTimeout(() => { // necessary because mouseover uses debounce fireEvent.mouseLeave(container.querySelector(('div.info-button')));
component.update(); });
expect(component.find('div.info-button-message').exists()).toEqual(true);
done(); expect(container.querySelector('div.info-button-message')).toBeTruthy();
}, 500);
}); });
test('clicking on info button, then clicking on it again makes info message go away', () => { test('clicking on info button, then clicking on it again makes info message go away', async () => {
const component = mountWithIntl( const {container, instance} = renderWithIntl(
<InfoButton <InfoButton
message="Here is some info about something!" message="Here is some info about something!"
/> />,
'InfoButton'
); );
const buttonRef = component.instance().buttonRef; const buttonRef = instance().buttonRef;
// click on info button // click on info button
mockedAddEventListener.mousedown({target: buttonRef}); await act(() => {
component.update(); mockedAddEventListener.mousedown({target: buttonRef});
expect(component.find('div.info-button').exists()).toEqual(true); });
expect(component.find('div.info-button-message').exists()).toEqual(true);
expect(container.querySelector('div.info-button')).toBeTruthy();
expect(container.querySelector('div.info-button-message')).toBeTruthy();
// click on info button again // click on info button again
mockedAddEventListener.mousedown({target: buttonRef}); await act(() => {
component.update(); mockedAddEventListener.mousedown({target: buttonRef});
expect(component.find('div.info-button-message').exists()).toEqual(false); });
expect(container.querySelector('div.info-button-message')).toBeFalsy();
}); });
test('clicking on info button, then clicking somewhere else', () => { test('clicking on info button, then clicking somewhere else', async () => {
const component = mountWithIntl( const {container, instance} = renderWithIntl(
<InfoButton <InfoButton
message="Here is some info about something!" message="Here is some info about something!"
/> />,
'InfoButton'
); );
const buttonRef = component.instance().buttonRef; const buttonRef = instance().buttonRef;
// click on info button // click on info button
mockedAddEventListener.mousedown({target: buttonRef}); await act(() => {
component.update(); mockedAddEventListener.mousedown({target: buttonRef});
expect(component.find('div.info-button').exists()).toEqual(true); });
expect(component.find('div.info-button-message').exists()).toEqual(true); expect(container.querySelector('div.info-button')).toBeTruthy();
expect(container.querySelector('div.info-button-message')).toBeTruthy();
// click on some other target // click on some other target
mockedAddEventListener.mousedown({target: null}); await act(() => {
component.update(); mockedAddEventListener.mousedown({target: null});
expect(component.find('div.info-button-message').exists()).toEqual(false); });
expect(container.querySelector('div.info-button-message')).toBeFalsy();
}); });
test('after message is visible, mouseLeave makes it vanish', () => { test('after message is visible, mouseLeave makes it vanish', async () => {
const component = mountWithIntl( const {container} = renderWithIntl(
<InfoButton <InfoButton
message="Here is some info about something!" message="Here is some info about something!"
/> />,
'InfoButton'
); );
// mouseOver info button // mouseOver info button
component.find('div.info-button').simulate('mouseOver'); await act(() => {
component.update(); fireEvent.mouseOver(container.querySelector(('div.info-button')));
expect(component.find('div.info-button-message').exists()).toEqual(true); });
expect(container.querySelector('div.info-button-message')).toBeTruthy();
// mouseLeave away from info button // mouseLeave away from info button
component.find('div.info-button').simulate('mouseLeave'); await act(() => {
component.update(); fireEvent.mouseLeave(container.querySelector(('div.info-button')));
expect(component.find('div.info-button-message').exists()).toEqual(false); });
expect(container.querySelector('div.info-button-message')).toBeFalsy();
}); });
}); });

View file

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import JoinFlowStep from '../../../src/components/join-flow/join-flow-step'; import JoinFlowStep from '../../../src/components/join-flow/join-flow-step';
import {renderWithIntl} from '../../helpers/react-testing-library-wrapper.jsx';
import {fireEvent} from '@testing-library/react';
describe('JoinFlowStep', () => { describe('JoinFlowStep', () => {
@ -15,50 +16,59 @@ describe('JoinFlowStep', () => {
title: 'join flow step title', title: 'join flow step title',
waiting: true waiting: true
}; };
const component = mountWithIntl( const {container, unmount, findByComponentName} = renderWithIntl(
<JoinFlowStep <JoinFlowStep
{...props} {...props}
/> />,
'JoinFlowStep'
); );
expect(component.find('img.join-flow-header-image').exists()).toEqual(true); const img = container.querySelector('img.join-flow-header-image');
expect(component.find({src: props.headerImgSrc}).exists()).toEqual(true); expect(img).toBeTruthy();
expect(component.find('.join-flow-inner-content').exists()).toEqual(true); expect(img.src).toContain(props.headerImgSrc);
expect(component.find('.join-flow-title').exists()).toEqual(true);
expect(component.find('.join-flow-title').first()
.prop('title')).toEqual(props.title);
expect(component.find('div.join-flow-description').exists()).toEqual(true);
expect(component.find('div.join-flow-description').text()).toEqual(props.description);
expect(component.find('NextStepButton').prop('waiting')).toEqual(true);
expect(component.find('NextStepButton').prop('content')).toEqual(props.nextButton);
component.unmount(); expect(container.querySelector('.join-flow-inner-content')).toBeTruthy();
const title = container.querySelector('.join-flow-title');
expect(title).toBeTruthy();
expect(title.textContent).toEqual(props.title);
expect(container.querySelector('div.join-flow-description')).toBeTruthy();
expect(container.querySelector('div.join-flow-description').textContent).toEqual(props.description);
const nextStepButton = findByComponentName('NextStepButton');
expect(nextStepButton).toBeTruthy();
expect(nextStepButton.memoizedProps.waiting).toEqual(true);
expect(nextStepButton.memoizedProps.content).toEqual(props.nextButton);
unmount();
}); });
test('components do not exist when props not present', () => { test('components do not exist when props not present', () => {
const component = mountWithIntl( const {container, unmount, findByComponentName} = renderWithIntl(
<JoinFlowStep /> <JoinFlowStep />,
'JoinFlowStep'
); );
expect(component.find('img.join-flow-header-image').exists()).toEqual(false); expect(container.querySelector('img.join-flow-header-image')).toBeFalsy();
expect(component.find('.join-flow-inner-content').exists()).toEqual(true); expect(container.querySelector('.join-flow-inner-content')).toBeTruthy();
expect(component.find('.join-flow-title').exists()).toEqual(false); expect(container.querySelector('.join-flow-title')).toBeFalsy();
expect(component.find('div.join-flow-description').exists()).toEqual(false); expect(container.querySelector('div.join-flow-description')).toBeFalsy();
expect(component.find('NextStepButton').prop('waiting')).toEqual(false); expect(findByComponentName('NextStepButton').memoizedProps.waiting).toEqual(false);
component.unmount(); unmount();
}); });
test('clicking submit calls passed in function', () => { test('clicking submit calls passed in function', () => {
const props = { const props = {
onSubmit: jest.fn() onSubmit: jest.fn()
}; };
const component = mountWithIntl( const {container, unmount} = renderWithIntl(
<JoinFlowStep <JoinFlowStep
{...props} {...props}
/> />
); );
component.find('button[type="submit"]').simulate('submit'); fireEvent.submit(container.querySelector('button[type="submit"]'));
expect(props.onSubmit).toHaveBeenCalled(); expect(props.onSubmit).toHaveBeenCalled();
component.unmount(); unmount();
}); });
}); });

View file

@ -1,10 +1,9 @@
import React from 'react'; import React, {act} from 'react';
const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx');
const defaults = require('lodash.defaultsdeep'); const defaults = require('lodash.defaultsdeep');
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import JoinFlow from '../../../src/components/join-flow/join-flow'; import JoinFlow from '../../../src/components/join-flow/join-flow';
import Progression from '../../../src/components/progression/progression.jsx'; import {renderWithIntl} from '../../helpers/react-testing-library-wrapper.jsx';
import RegistrationErrorStep from '../../../src/components/join-flow/registration-error-step'; import {Provider} from 'react-redux';
describe('JoinFlow', () => { describe('JoinFlow', () => {
const mockStore = configureStore(); const mockStore = configureStore();
@ -46,160 +45,200 @@ describe('JoinFlow', () => {
}); });
const getJoinFlowWrapper = props => { const getJoinFlowWrapper = props => {
const wrapper = shallowWithIntl( const wrapper = renderWithIntl(
<JoinFlow <Provider store={store}>
{...props} <JoinFlow
/> {...props}
, {context: {store}} />
</Provider>,
'JoinFlow'
); );
return wrapper return wrapper;
.dive() // unwrap redux connect(injectIntl(JoinFlow))
.dive(); // unwrap injectIntl(JoinFlow)
}; };
test('handleCaptchaError gives state with captcha message', () => { test('handleCaptchaError gives state with captcha message', async () => {
const joinFlowInstance = getJoinFlowWrapper().instance(); const joinFlowInstance = getJoinFlowWrapper().instance();
joinFlowInstance.setState({}); await act(() => {
joinFlowInstance.handleCaptchaError(); joinFlowInstance.setState({});
});
await act(() => {
joinFlowInstance.handleCaptchaError();
});
expect(joinFlowInstance.state.registrationError).toEqual({ expect(joinFlowInstance.state.registrationError).toEqual({
errorAllowsTryAgain: false, errorAllowsTryAgain: false,
errorMsg: 'registration.errorCaptcha' errorMsg: 'There was a problem with the CAPTCHA test.'
}); });
}); });
test('sendAnalytics calls GTM with correct params', () => { test('sendAnalytics calls GTM with correct params', async () => {
const joinFlowInstance = getJoinFlowWrapper().instance(); const joinFlowInstance = getJoinFlowWrapper().instance();
global.window.dataLayer = {push: jest.fn()}; global.window.dataLayer = {push: jest.fn()};
global.window.GA_ID = '1234'; global.window.GA_ID = '1234';
joinFlowInstance.sendAnalytics('page-path'); await act(() => {
joinFlowInstance.sendAnalytics('page-path');
});
expect(global.window.dataLayer.push).toHaveBeenCalledWith({ expect(global.window.dataLayer.push).toHaveBeenCalledWith({
event: 'join_flow', event: 'join_flow',
joinFlowStep: 'page-path' joinFlowStep: 'page-path'
}); });
}); });
test('handleAdvanceStep', () => { test('handleAdvanceStep', async () => {
const joinFlowInstance = getJoinFlowWrapper().instance(); const joinFlowInstance = getJoinFlowWrapper().instance();
joinFlowInstance.setState({formData: {username: 'ScratchCat123'}, step: 2}); await act(() => {
joinFlowInstance.handleAdvanceStep({email: 'scratchcat123@scratch.mit.edu'}); joinFlowInstance.setState({formData: {username: 'ScratchCat123'}, step: 2});
});
await act(() => {
joinFlowInstance.handleAdvanceStep({email: 'scratchcat123@scratch.mit.edu'});
});
expect(joinFlowInstance.state.formData.username).toBe('ScratchCat123'); expect(joinFlowInstance.state.formData.username).toBe('ScratchCat123');
expect(joinFlowInstance.state.formData.email).toBe('scratchcat123@scratch.mit.edu'); expect(joinFlowInstance.state.formData.email).toBe('scratchcat123@scratch.mit.edu');
expect(joinFlowInstance.state.step).toBe(3); expect(joinFlowInstance.state.step).toBe(3);
}); });
test('when state.registrationError has error message, we show RegistrationErrorStep', () => { test('when state.registrationError has error message, we show RegistrationErrorStep', async () => {
const joinFlowWrapper = getJoinFlowWrapper(); const joinFlowWrapper = getJoinFlowWrapper();
joinFlowWrapper.instance().setState({registrationError: 'halp there is a errors!!'}); await act(() => {
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep); joinFlowWrapper.instance().setState({registrationError: 'halp there is a errors!!'});
const progressionWrapper = joinFlowWrapper.find(Progression); });
joinFlowWrapper.rerenderWithIntl();
const registrationErrorWrapper = joinFlowWrapper.findAllByComponentName('RegistrationErrorStep');
const progressionWrapper = joinFlowWrapper.findAllByComponentName('Progression');
expect(registrationErrorWrapper).toHaveLength(1); expect(registrationErrorWrapper).toHaveLength(1);
expect(progressionWrapper).toHaveLength(0); expect(progressionWrapper).toHaveLength(0);
}); });
test('when state.registrationError has null error message, we show Progression', () => { test('when state.registrationError has null error message, we show Progression', async () => {
const joinFlowWrapper = getJoinFlowWrapper(); const joinFlowWrapper = getJoinFlowWrapper();
joinFlowWrapper.instance().setState({registrationError: null}); await act(() => {
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep); joinFlowWrapper.instance().setState({registrationError: null});
const progressionWrapper = joinFlowWrapper.find(Progression); });
joinFlowWrapper.rerenderWithIntl();
const registrationErrorWrapper = joinFlowWrapper.findAllByComponentName('RegistrationErrorStep');
const progressionWrapper = joinFlowWrapper.findAllByComponentName('Progression');
expect(registrationErrorWrapper).toHaveLength(0); expect(registrationErrorWrapper).toHaveLength(0);
expect(progressionWrapper).toHaveLength(1); expect(progressionWrapper).toHaveLength(1);
}); });
test('when state.registrationError has empty error message, we show Progression', () => { test('when state.registrationError has empty error message, we show Progression', async () => {
const joinFlowWrapper = getJoinFlowWrapper(); const joinFlowWrapper = getJoinFlowWrapper();
joinFlowWrapper.instance().setState({registrationError: ''}); await act(() => {
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep); joinFlowWrapper.instance().setState({registrationError: ''});
const progressionWrapper = joinFlowWrapper.find(Progression); });
joinFlowWrapper.rerenderWithIntl();
const registrationErrorWrapper = joinFlowWrapper.findAllByComponentName('RegistrationErrorStep');
const progressionWrapper = joinFlowWrapper.findAllByComponentName('Progression');
expect(registrationErrorWrapper).toHaveLength(0); expect(registrationErrorWrapper).toHaveLength(0);
expect(progressionWrapper).toHaveLength(1); expect(progressionWrapper).toHaveLength(1);
}); });
test('when numAttempts is 0 and registrationError errorAllowsTryAgain is true, ' + test('when numAttempts is 0 and registrationError errorAllowsTryAgain is true, ' +
'RegistrationErrorStep receives errorAllowsTryAgain prop with value true', () => { 'RegistrationErrorStep receives errorAllowsTryAgain prop with value true', async () => {
const joinFlowWrapper = getJoinFlowWrapper(); const joinFlowWrapper = getJoinFlowWrapper();
joinFlowWrapper.instance().setState({ await act(() => {
numAttempts: 0, joinFlowWrapper.instance().setState({
registrationError: { numAttempts: 0,
errorAllowsTryAgain: true, registrationError: {
errorMsg: 'halp there is a errors!!' errorAllowsTryAgain: true,
} errorMsg: 'halp there is a errors!!'
}
});
}); });
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep); joinFlowWrapper.rerenderWithIntl();
expect(registrationErrorWrapper.first().props().canTryAgain).toEqual(true); const registrationErrorWrapper = joinFlowWrapper.findAllByComponentName('RegistrationErrorStep');
expect(registrationErrorWrapper[0].stateNode.props.canTryAgain).toEqual(true);
}); });
test('when numAttempts is 1 and registrationError errorAllowsTryAgain is true, ' + test('when numAttempts is 1 and registrationError errorAllowsTryAgain is true, ' +
'RegistrationErrorStep receives errorAllowsTryAgain prop with value true', () => { 'RegistrationErrorStep receives errorAllowsTryAgain prop with value true', async () => {
const joinFlowWrapper = getJoinFlowWrapper(); const joinFlowWrapper = getJoinFlowWrapper();
joinFlowWrapper.instance().setState({ await act(() => {
numAttempts: 1, joinFlowWrapper.instance().setState({
registrationError: { numAttempts: 1,
errorAllowsTryAgain: true, registrationError: {
errorMsg: 'halp there is a errors!!' errorAllowsTryAgain: true,
} errorMsg: 'halp there is a errors!!'
}
});
}); });
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep); joinFlowWrapper.rerenderWithIntl();
expect(registrationErrorWrapper.first().props().canTryAgain).toEqual(true); const registrationErrorWrapper = joinFlowWrapper.findAllByComponentName('RegistrationErrorStep');
expect(registrationErrorWrapper[0].stateNode.props.canTryAgain).toEqual(true);
}); });
test('when numAttempts is 2 and registrationError errorAllowsTryAgain is true, ' + test('when numAttempts is 2 and registrationError errorAllowsTryAgain is true, ' +
'RegistrationErrorStep receives errorAllowsTryAgain prop with value false', () => { 'RegistrationErrorStep receives errorAllowsTryAgain prop with value false', async () => {
const joinFlowWrapper = getJoinFlowWrapper(); const joinFlowWrapper = getJoinFlowWrapper();
joinFlowWrapper.instance().setState({ await act(() => {
numAttempts: 2, joinFlowWrapper.instance().setState({
registrationError: { numAttempts: 2,
errorAllowsTryAgain: true, registrationError: {
errorMsg: 'halp there is a errors!!' errorAllowsTryAgain: true,
} errorMsg: 'halp there is a errors!!'
}
});
}); });
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep); joinFlowWrapper.rerenderWithIntl();
expect(registrationErrorWrapper.first().props().canTryAgain).toEqual(false); const registrationErrorWrapper = joinFlowWrapper.findAllByComponentName('RegistrationErrorStep');
expect(registrationErrorWrapper[0].memoizedProps.canTryAgain).toEqual(false);
}); });
test('when numAttempts is 0 and registrationError errorAllowsTryAgain is false, ' + test('when numAttempts is 0 and registrationError errorAllowsTryAgain is false, ' +
'RegistrationErrorStep receives errorAllowsTryAgain prop with value false', () => { 'RegistrationErrorStep receives errorAllowsTryAgain prop with value false', async () => {
const joinFlowWrapper = getJoinFlowWrapper(); const joinFlowWrapper = getJoinFlowWrapper();
joinFlowWrapper.instance().setState({ await act(() => {
numAttempts: 0, joinFlowWrapper.instance().setState({
registrationError: { numAttempts: 0,
errorAllowsTryAgain: false, registrationError: {
errorMsg: 'halp there is a errors!!' errorAllowsTryAgain: false,
} errorMsg: 'halp there is a errors!!'
}
});
}); });
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep); joinFlowWrapper.rerenderWithIntl();
expect(registrationErrorWrapper.first().props().canTryAgain).toEqual(false); const registrationErrorWrapper = joinFlowWrapper.findAllByComponentName('RegistrationErrorStep');
expect(registrationErrorWrapper[0].stateNode.props.canTryAgain).toEqual(false);
}); });
test('resetState resets entire state, does not leave any state keys out', () => { test('resetState resets entire state, does not leave any state keys out', async () => {
const joinFlowInstance = getJoinFlowWrapper().instance(); const joinFlowInstance = getJoinFlowWrapper().instance();
Object.keys(joinFlowInstance.state).forEach(key => { await act(() => {
joinFlowInstance.setState({[key]: 'Different than the initial value'}); Object.keys(joinFlowInstance.state).forEach(key => {
joinFlowInstance.setState({[key]: 'Different than the initial value'});
});
joinFlowInstance.resetState();
}); });
joinFlowInstance.resetState();
Object.keys(joinFlowInstance.state).forEach(key => { Object.keys(joinFlowInstance.state).forEach(key => {
expect(joinFlowInstance.state[key]).not.toEqual('Different than the initial value'); expect(joinFlowInstance.state[key]).not.toEqual('Different than the initial value');
}); });
}); });
test('resetState makes each state field match initial state', () => { test('resetState makes each state field match initial state', async () => {
const joinFlowInstance = getJoinFlowWrapper().instance(); const joinFlowInstance = getJoinFlowWrapper().instance();
const stateSnapshot = {}; const stateSnapshot = {};
Object.keys(joinFlowInstance.state).forEach(key => { Object.keys(joinFlowInstance.state).forEach(key => {
stateSnapshot[key] = joinFlowInstance.state[key]; stateSnapshot[key] = joinFlowInstance.state[key];
}); });
joinFlowInstance.resetState(); await act(() => {
joinFlowInstance.resetState();
});
Object.keys(joinFlowInstance.state).forEach(key => { Object.keys(joinFlowInstance.state).forEach(key => {
expect(stateSnapshot[key]).toEqual(joinFlowInstance.state[key]); expect(stateSnapshot[key]).toEqual(joinFlowInstance.state[key]);
}); });
}); });
test('calling resetState results in state.formData which is not same reference as before', () => { test('calling resetState results in state.formData which is not same reference as before', async () => {
const joinFlowInstance = getJoinFlowWrapper().instance(); const joinFlowInstance = getJoinFlowWrapper().instance();
joinFlowInstance.setState({ await act(() => {
formData: defaults({}, {username: 'abcdef'}) joinFlowInstance.setState({
formData: defaults({}, {username: 'abcdef'})
});
}); });
const formDataReference = joinFlowInstance.state.formData; const formDataReference = joinFlowInstance.state.formData;
joinFlowInstance.resetState(); await act(() => {
joinFlowInstance.resetState();
});
expect(formDataReference).not.toBe(joinFlowInstance.state.formData); expect(formDataReference).not.toBe(joinFlowInstance.state.formData);
expect(formDataReference).not.toEqual(joinFlowInstance.state.formData); expect(formDataReference).not.toEqual(joinFlowInstance.state.formData);
}); });
@ -245,7 +284,7 @@ describe('JoinFlow', () => {
const errorsFromResponse = const errorsFromResponse =
joinFlowInstance.getErrorsFromResponse(null, responseBodyMultipleErrs, {statusCode: 200}); joinFlowInstance.getErrorsFromResponse(null, responseBodyMultipleErrs, {statusCode: 200});
const customErrMsg = joinFlowInstance.getCustomErrMsg(errorsFromResponse); const customErrMsg = joinFlowInstance.getCustomErrMsg(errorsFromResponse);
expect(customErrMsg).toEqual('registration.problemsAre: "username: This field is required.; ' + expect(customErrMsg).toEqual('The problems are:: "username: This field is required.; ' +
'recaptcha: Incorrect, please try again."'); 'recaptcha: Incorrect, please try again."');
}); });
@ -254,7 +293,7 @@ describe('JoinFlow', () => {
const errorsFromResponse = const errorsFromResponse =
joinFlowInstance.getErrorsFromResponse(null, responseBodySingleErr, {statusCode: 200}); joinFlowInstance.getErrorsFromResponse(null, responseBodySingleErr, {statusCode: 200});
const customErrMsg = joinFlowInstance.getCustomErrMsg(errorsFromResponse); const customErrMsg = joinFlowInstance.getCustomErrMsg(errorsFromResponse);
expect(customErrMsg).toEqual('registration.problemsAre: "recaptcha: Incorrect, please try again."'); expect(customErrMsg).toEqual('The problems are:: "recaptcha: Incorrect, please try again."');
}); });
test('registrationIsSuccessful returns true when given response body with single error', () => { test('registrationIsSuccessful returns true when given response body with single error', () => {
@ -291,14 +330,18 @@ describe('JoinFlow', () => {
joinFlowInstance.handleRegistrationResponse(null, responseBodySuccess, {statusCode: 200}); joinFlowInstance.handleRegistrationResponse(null, responseBodySuccess, {statusCode: 200});
}); });
test('handleRegistrationResponse advances to next step when passed body with success', () => { test('handleRegistrationResponse advances to next step when passed body with success', async () => {
const props = { const props = {
refreshSessionWithRetry: () => (new Promise(resolve => { // eslint-disable-line no-undef refreshSessionWithRetry: () => (new Promise(resolve => { // eslint-disable-line no-undef
resolve(); resolve();
})) }))
}; };
const joinFlowInstance = getJoinFlowWrapper(props).instance(); const joinFlowWrapper = getJoinFlowWrapper(props);
joinFlowInstance.handleRegistrationResponse(null, responseBodySuccess, {statusCode: 200}); const joinFlowInstance = joinFlowWrapper.instance();
await act(() => {
joinFlowInstance.handleRegistrationResponse(null, responseBodySuccess, {statusCode: 200});
});
joinFlowWrapper.rerenderWithIntl();
process.nextTick( process.nextTick(
() => { () => {
expect(joinFlowInstance.state.registrationError).toEqual(null); expect(joinFlowInstance.state.registrationError).toEqual(null);
@ -308,20 +351,25 @@ describe('JoinFlow', () => {
); );
}); });
test('handleRegistrationResponse when passed body with preset server error', () => { test('handleRegistrationResponse when passed body with preset server error', async () => {
const joinFlowInstance = getJoinFlowWrapper().instance(); const joinFlowWrapper = getJoinFlowWrapper();
joinFlowInstance.handleRegistrationResponse(null, responseBodySingleErr, {statusCode: 200}); const joinFlowInstance = joinFlowWrapper.instance();
await act(() => {
joinFlowInstance.handleRegistrationResponse(null, responseBodySingleErr, {statusCode: 200});
});
joinFlowWrapper.rerenderWithIntl();
expect(joinFlowInstance.state.registrationError).toEqual({ expect(joinFlowInstance.state.registrationError).toEqual({
errorAllowsTryAgain: false, errorAllowsTryAgain: false,
errorMsg: 'registration.errorCaptcha' errorMsg: 'There was a problem with the CAPTCHA test.'
}); });
}); });
test('handleRegistrationResponse with failure response, with error fields missing', () => { test('handleRegistrationResponse with failure response, with error fields missing', async () => {
const props = { const props = {
refreshSessionWithRetry: jest.fn() refreshSessionWithRetry: jest.fn()
}; };
const joinFlowInstance = getJoinFlowWrapper(props).instance(); const joinFlowWrapper = getJoinFlowWrapper(props);
const joinFlowInstance = joinFlowWrapper.instance();
const responseErr = null; const responseErr = null;
const responseBody = [ const responseBody = [
{ {
@ -332,7 +380,10 @@ describe('JoinFlow', () => {
const responseObj = { const responseObj = {
statusCode: 200 statusCode: 200
}; };
joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj); await act(() => {
joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj);
});
joinFlowWrapper.rerenderWithIntl();
expect(joinFlowInstance.props.refreshSessionWithRetry).not.toHaveBeenCalled(); expect(joinFlowInstance.props.refreshSessionWithRetry).not.toHaveBeenCalled();
expect(joinFlowInstance.state.registrationError).toEqual({ expect(joinFlowInstance.state.registrationError).toEqual({
errorAllowsTryAgain: false, errorAllowsTryAgain: false,
@ -340,21 +391,26 @@ describe('JoinFlow', () => {
}); });
}); });
test('handleRegistrationResponse when passed body with unfamiliar server error', () => { test('handleRegistrationResponse when passed body with unfamiliar server error', async () => {
const joinFlowInstance = getJoinFlowWrapper().instance(); const joinFlowWrapper = getJoinFlowWrapper();
joinFlowInstance.handleRegistrationResponse(null, responseBodyMultipleErrs, {statusCode: 200}); const joinFlowInstance = joinFlowWrapper.instance();
await act(() => {
joinFlowInstance.handleRegistrationResponse(null, responseBodyMultipleErrs, {statusCode: 200});
});
joinFlowWrapper.rerenderWithIntl();
expect(joinFlowInstance.state.registrationError).toEqual({ expect(joinFlowInstance.state.registrationError).toEqual({
errorAllowsTryAgain: false, errorAllowsTryAgain: false,
errorMsg: 'registration.problemsAre: "username: This field is required.; ' + errorMsg: 'The problems are:: "username: This field is required.; ' +
'recaptcha: Incorrect, please try again."' 'recaptcha: Incorrect, please try again."'
}); });
}); });
test('handleRegistrationResponse with failure response, with no text explanation', () => { test('handleRegistrationResponse with failure response, with no text explanation', async () => {
const props = { const props = {
refreshSessionWithRetry: jest.fn() refreshSessionWithRetry: jest.fn()
}; };
const joinFlowInstance = getJoinFlowWrapper(props).instance(); const joinFlowWrapper = getJoinFlowWrapper(props);
const joinFlowInstance = joinFlowWrapper.instance();
const responseErr = null; const responseErr = null;
const responseBody = [ const responseBody = [
{ {
@ -364,7 +420,10 @@ describe('JoinFlow', () => {
const responseObj = { const responseObj = {
statusCode: 200 statusCode: 200
}; };
joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj); await act(() => {
joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj);
});
joinFlowWrapper.rerenderWithIntl();
expect(joinFlowInstance.props.refreshSessionWithRetry).not.toHaveBeenCalled(); expect(joinFlowInstance.props.refreshSessionWithRetry).not.toHaveBeenCalled();
expect(joinFlowInstance.state.registrationError).toEqual({ expect(joinFlowInstance.state.registrationError).toEqual({
errorAllowsTryAgain: false, errorAllowsTryAgain: false,
@ -372,29 +431,41 @@ describe('JoinFlow', () => {
}); });
}); });
test('handleRegistrationResponse when passed non null outgoing request error', () => { test('handleRegistrationResponse when passed non null outgoing request error', async () => {
const joinFlowInstance = getJoinFlowWrapper().instance(); const joinFlowWrapper = getJoinFlowWrapper();
joinFlowInstance.handleRegistrationResponse({}, responseBodyMultipleErrs, {statusCode: 200}); const joinFlowInstance = joinFlowWrapper.instance();
await act(() => {
joinFlowInstance.handleRegistrationResponse({}, responseBodyMultipleErrs, {statusCode: 200});
});
joinFlowWrapper.rerenderWithIntl();
expect(joinFlowInstance.state.registrationError).toEqual({ expect(joinFlowInstance.state.registrationError).toEqual({
errorAllowsTryAgain: true errorAllowsTryAgain: true
}); });
}); });
test('handleRegistrationResponse when passed status 400', () => { test('handleRegistrationResponse when passed status 400', async () => {
const props = { const props = {
refreshSessionWithRetry: jest.fn() refreshSessionWithRetry: jest.fn()
}; };
const joinFlowInstance = getJoinFlowWrapper(props).instance(); const joinFlowWrapper = getJoinFlowWrapper(props);
joinFlowInstance.handleRegistrationResponse({}, responseBodyMultipleErrs, {statusCode: 400}); const joinFlowInstance = joinFlowWrapper.instance();
await act(() => {
joinFlowInstance.handleRegistrationResponse({}, responseBodyMultipleErrs, {statusCode: 400});
});
joinFlowWrapper.rerenderWithIntl();
expect(joinFlowInstance.props.refreshSessionWithRetry).not.toHaveBeenCalled(); expect(joinFlowInstance.props.refreshSessionWithRetry).not.toHaveBeenCalled();
expect(joinFlowInstance.state.registrationError).toEqual({ expect(joinFlowInstance.state.registrationError).toEqual({
errorAllowsTryAgain: true errorAllowsTryAgain: true
}); });
}); });
test('handleRegistrationResponse when passed status 500', () => { test('handleRegistrationResponse when passed status 500', async () => {
const joinFlowInstance = getJoinFlowWrapper().instance(); const joinFlowWrapper = getJoinFlowWrapper();
joinFlowInstance.handleRegistrationResponse(null, responseBodyMultipleErrs, {statusCode: 500}); const joinFlowInstance = joinFlowWrapper.instance();
await act(() => {
joinFlowInstance.handleRegistrationResponse(null, responseBodyMultipleErrs, {statusCode: 500});
});
joinFlowWrapper.rerenderWithIntl();
expect(joinFlowInstance.state.registrationError).toEqual({ expect(joinFlowInstance.state.registrationError).toEqual({
errorAllowsTryAgain: true errorAllowsTryAgain: true
}); });

View file

@ -1,34 +1,40 @@
const React = require('react'); const React = require('react');
const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx');
const Modal = require('../../../src/components/modal/base/modal.jsx'); const Modal = require('../../../src/components/modal/base/modal.jsx');
const {
renderWithIntl
} = require('../../helpers/react-testing-library-wrapper.jsx');
describe('Modal', () => { describe('Modal', () => {
test('Close button not shown when showCloseButton false', () => { test('Close button not shown when showCloseButton false', () => {
const showClose = true; renderWithIntl(<Modal
const component = shallowWithIntl( isOpen
<Modal showCloseButton
showCloseButton={showClose} />);
/> expect(
); global.document.querySelector('div.modal-content-close')
expect(component.find('div.modal-content-close').exists()).toBe(true); ).toBeTruthy();
expect(component.find('img.modal-content-close-img').exists()).toBe(true); expect(
global.document.querySelector('img.modal-content-close-img')
).toBeTruthy();
}); });
test('Close button shown by default', () => { test('Close button shown by default', () => {
const component = shallowWithIntl( renderWithIntl(<Modal isOpen />);
<Modal /> expect(
); global.document.querySelector('div.modal-content-close')
expect(component.find('div.modal-content-close').exists()).toBe(true); ).toBeTruthy();
expect(component.find('img.modal-content-close-img').exists()).toBe(true); expect(
global.document.querySelector('img.modal-content-close-img')
).toBeTruthy();
}); });
test('Close button shown when showCloseButton true', () => { test('Close button shown when showCloseButton true', () => {
const showClose = false; renderWithIntl(<Modal showCloseButton={false} />);
const component = shallowWithIntl( expect(
<Modal global.document.querySelector('div.modal-content-close')
showCloseButton={showClose} ).toBeFalsy();
/> expect(
); global.document.querySelector('img.modal-content-close-img')
expect(component.find('div.modal-content-close').exists()).toBe(false); ).toBeFalsy();
expect(component.find('img.modal-content-close-img').exists()).toBe(false);
}); });
}); });

View file

@ -1,7 +1,7 @@
import React from 'react'; import React, {act} from 'react';
import {mountWithIntl, shallowWithIntl} from '../../helpers/intl-helpers.jsx';
import MuteModal from '../../../src/components/modal/mute/modal'; import MuteModal from '../../../src/components/modal/mute/modal';
import Modal from '../../../src/components/modal/base/modal'; import {renderWithIntl} from '../../helpers/react-testing-library-wrapper.jsx';
import {fireEvent} from '@testing-library/react';
describe('MuteModalTest', () => { describe('MuteModalTest', () => {
@ -11,183 +11,214 @@ describe('MuteModalTest', () => {
muteStepContent: ['comment.general.content1'] muteStepContent: ['comment.general.content1']
}; };
test('Mute Modal rendering', () => { test('Mute Modal rendering', () => {
const component = shallowWithIntl( renderWithIntl(
<MuteModal muteModalMessages={defaultMessages} /> <MuteModal muteModalMessages={defaultMessages} />
).dive(); );
expect(component.find('div.mute-modal-header').exists()).toEqual(true); expect(global.document.querySelector('div.mute-modal-header')).toBeTruthy();
}); });
test('Mute Modal only shows next button on initial step', () => { test('Mute Modal only shows next button on initial step', () => {
const component = mountWithIntl( const {findAllByComponentName, instance} = renderWithIntl(
<MuteModal muteModalMessages={defaultMessages} /> <MuteModal muteModalMessages={defaultMessages} />,
'MuteModal'
); );
expect(component.find('div.mute-nav').exists()).toEqual(true); expect(global.document.querySelector('div.mute-nav')).toBeTruthy();
expect(component.find('button.next-button').exists()).toEqual(true); expect(global.document.querySelector('button.next-button')).toBeTruthy();
expect(component.find('button.next-button').getElements()[0].props.onClick) expect(findAllByComponentName('Button')[0].memoizedProps.onClick)
.toEqual(component.find('MuteModal').instance().handleNext); .toEqual(instance().handleNext);
expect(component.find('button.close-button').exists()).toEqual(false); expect(global.document.querySelector('button.close-button')).toBeFalsy();
expect(component.find('button.back-button').exists()).toEqual(false); expect(global.document.querySelector('button.back-button')).toBeFalsy();
}); });
test('Mute Modal shows extra showWarning step', () => { test('Mute Modal shows extra showWarning step', () => {
const component = mountWithIntl( const {findAllByComponentName, instance} = renderWithIntl(
<MuteModal <MuteModal
showWarning showWarning
muteModalMessages={defaultMessages} muteModalMessages={defaultMessages}
/> />,
'MuteModal'
); );
component.find('MuteModal').instance() const muteModalInstance = instance();
.setState({step: 1}); const nextButton = findAllByComponentName('Button')[0];
expect(component.find('button.next-button').exists()).toEqual(true); act(() => {
expect(component.find('button.next-button').getElements()[0].props.onClick) muteModalInstance.setState({step: 1});
.toEqual(component.find('MuteModal').instance().handleNext); });
component.find('MuteModal').instance() expect(global.document.querySelector('button.next-button')).toBeTruthy();
.handleNext(); expect(nextButton.memoizedProps.onClick)
expect(component.find('MuteModal').instance().state.step).toEqual(2); .toEqual(muteModalInstance.handleNext);
fireEvent.click(global.document.querySelector('button.next-button'));
expect(muteModalInstance.state.step).toEqual(2);
}); });
test('Mute Modal shows back & close button on last step', () => { test('Mute Modal shows back & close button on last step', () => {
const component = mountWithIntl( const {findAllByComponentName, instance} = renderWithIntl(
<MuteModal muteModalMessages={defaultMessages} /> <MuteModal muteModalMessages={defaultMessages} />,
'MuteModal'
); );
// Step 1 is the last step. const muteModalInstance = instance();
component.find('MuteModal').instance() act(() => {
.setState({step: 1}); muteModalInstance.setState({step: 1});
component.update(); });
const buttons = findAllByComponentName('Button');
const closeButton = buttons[0];
const backButton = buttons[1];
expect(component.find('div.mute-nav').exists()).toEqual(true); expect(global.document.querySelector('div.mute-nav')).toBeTruthy();
expect(component.find('button.next-button').exists()).toEqual(false); expect(global.document.querySelector('button.next-button')).toBeFalsy();
expect(component.find('button.back-button').exists()).toEqual(true); expect(global.document.querySelector('button.back-button')).toBeTruthy();
expect(component.find('button.back-button').getElements()[0].props.onClick) expect(backButton.memoizedProps.onClick)
.toEqual(component.find('MuteModal').instance().handlePrevious); .toEqual(muteModalInstance.handlePrevious);
expect(component.find('button.close-button').exists()).toEqual(true); expect(global.document.querySelector('button.close-button')).toBeTruthy();
expect(component.find('button.close-button').getElements()[0].props.onClick) expect(closeButton.memoizedProps.onClick)
.toEqual(component.find('MuteModal').instance().props.onRequestClose); .toEqual(muteModalInstance.onRequestClose);
}); });
test('Mute modal sends correct props to Modal', () => { test('Mute modal sends correct props to Modal', () => {
const closeFn = jest.fn(); const closeFn = jest.fn();
const component = shallowWithIntl( const {findByComponentName} = renderWithIntl(
<MuteModal <MuteModal
muteModalMessages={defaultMessages} muteModalMessages={defaultMessages}
onRequestClose={closeFn} onRequestClose={closeFn}
/> />,
).dive(); 'MuteModal'
const modal = component.find(Modal); );
expect(modal).toHaveLength(1); const modal = findByComponentName('Modal');
expect(modal.props().showCloseButton).toBe(false); expect(modal.props.showCloseButton).toBe(false);
expect(modal.props().isOpen).toBe(true); expect(modal.props.isOpen).toBe(true);
expect(modal.props().className).toBe('modal-mute'); expect(modal.props.className).toBe('modal-mute');
expect(modal.props().onRequestClose).toBe(closeFn); expect(modal.props.onRequestClose).toBe(closeFn);
}); });
test('Mute modal handle next step', () => { test('Mute modal handle next step', () => {
const closeFn = jest.fn(); const closeFn = jest.fn();
const component = shallowWithIntl( const {instance} = renderWithIntl(
<MuteModal <MuteModal
muteModalMessages={defaultMessages} muteModalMessages={defaultMessages}
onRequestClose={closeFn} onRequestClose={closeFn}
/> />,
).dive(); 'MuteModal'
expect(component.instance().state.step).toBe(0); );
component.instance().handleNext(); const muteModalInstance = instance();
expect(component.instance().state.step).toBe(1); expect(muteModalInstance.state.step).toBe(0);
fireEvent.click(global.document.querySelector('button.next-button'));
expect(muteModalInstance.state.step).toBe(1);
}); });
test('Mute modal handle previous step', () => { test('Mute modal handle previous step', () => {
const component = shallowWithIntl( const {instance} = renderWithIntl(
<MuteModal muteModalMessages={defaultMessages} /> <MuteModal muteModalMessages={defaultMessages} />,
).dive(); 'MuteModal'
component.instance().setState({step: 1}); );
const muteModalInstance = instance();
act(() => {
muteModalInstance.setState({step: 1});
});
component.instance().handlePrevious(); fireEvent.click(global.document.querySelector('button.back-button'));
expect(component.instance().state.step).toBe(0); expect(muteModalInstance.state.step).toBe(0);
}); });
test('Mute modal handle previous step stops at 0', () => { test('Mute modal handle previous step stops at 0', () => {
const component = shallowWithIntl( const {instance} = renderWithIntl(
<MuteModal muteModalMessages={defaultMessages} /> <MuteModal muteModalMessages={defaultMessages} />,
).dive(); 'MuteModal'
component.instance().setState({step: 0}); );
component.instance().handlePrevious(); const muteModalInstance = instance();
expect(component.instance().state.step).toBe(0); act(() => {
muteModalInstance.setState({step: 0});
});
muteModalInstance.handlePrevious();
expect(muteModalInstance.state.step).toBe(0);
}); });
test('Mute modal asks for feedback if showFeedback', () => { test('Mute modal asks for feedback if showFeedback', () => {
const component = mountWithIntl( const {instance} = renderWithIntl(
<MuteModal <MuteModal
showFeedback showFeedback
muteModalMessages={defaultMessages} muteModalMessages={defaultMessages}
/> />,
'MuteModal'
); );
component.find('MuteModal').instance() const muteModalInstance = instance();
.setState({step: 1}); act(() => {
component.update(); muteModalInstance.setState({step: 1});
expect(component.find('p.feedback-prompt').exists()).toEqual(true); });
expect(global.document.querySelector('p.feedback-prompt')).toBeTruthy();
}); });
test('Mute modal do not ask for feedback if not showFeedback', () => { test('Mute modal do not ask for feedback if not showFeedback', () => {
const component = mountWithIntl( const {instance} = renderWithIntl(
<MuteModal <MuteModal
muteModalMessages={defaultMessages} muteModalMessages={defaultMessages}
/> />,
'MuteModal'
); );
component.find('MuteModal').instance() const muteModalInstance = instance();
.setState({step: 1}); act(() => {
component.update(); muteModalInstance.setState({step: 1});
expect(component.find('p.feedback-prompt').exists()).toEqual(false); });
expect(global.document.querySelector('p.feedback-prompt')).toBeFalsy();
}); });
test('Mute modal asks for feedback on extra showWarning step if showFeedback', () => { test('Mute modal asks for feedback on extra showWarning step if showFeedback', () => {
const component = mountWithIntl( const {instance} = renderWithIntl(
<MuteModal <MuteModal
showFeedback showFeedback
showWarning showWarning
muteModalMessages={defaultMessages} muteModalMessages={defaultMessages}
/> />,
'MuteModal'
); );
component.find('MuteModal').instance() const muteModalInstance = instance();
.setState({step: 1}); act(() => {
component.update(); muteModalInstance.setState({step: 1});
expect(component.find('p.feedback-prompt').exists()).toEqual(false); });
component.find('MuteModal').instance() expect(global.document.querySelector('p.feedback-prompt')).toBeFalsy();
.setState({step: 2}); act(() => {
component.update(); muteModalInstance.setState({step: 2});
expect(component.find('p.feedback-prompt').exists()).toEqual(true); });
expect(global.document.querySelector('p.feedback-prompt')).toBeTruthy();
}); });
test('Mute modal does not for feedback on extra showWarning step if not showFeedback', () => { test('Mute modal does not for feedback on extra showWarning step if not showFeedback', () => {
const component = mountWithIntl( const {instance} = renderWithIntl(
<MuteModal <MuteModal
showWarning showWarning
muteModalMessages={defaultMessages} muteModalMessages={defaultMessages}
/> />,
'MuteModal'
); );
component.find('MuteModal').instance() const muteModalInstance = instance();
.setState({step: 1}); act(() => {
component.update(); muteModalInstance.setState({step: 1});
expect(component.find('p.feedback-prompt').exists()).toEqual(false); });
component.find('MuteModal').instance() expect(global.document.querySelector('p.feedback-prompt')).toBeFalsy();
.setState({step: 2}); act(() => {
component.update(); muteModalInstance.setState({step: 2});
expect(component.find('p.feedback-prompt').exists()).toEqual(false); });
expect(global.document.querySelector('p.feedback-prompt')).toBeFalsy();
}); });
test('Mute modal handle go to feedback', () => { test('Mute modal handle go to feedback', () => {
const component = shallowWithIntl( const {instance} = renderWithIntl(
<MuteModal <MuteModal
muteModalMessages={defaultMessages} muteModalMessages={defaultMessages}
/> />,
).dive(); 'MuteModal'
component.instance().handleGoToFeedback(); );
expect(component.instance().state.step).toBe(3); const muteModalInstance = instance();
act(() => {
muteModalInstance.handleGoToFeedback();
});
expect(muteModalInstance.state.step).toBe(3);
}); });
test('Mute modal submit feedback gives thank you step', () => { test('Mute modal submit feedback gives thank you step', () => {
const component = shallowWithIntl( const {instance} = renderWithIntl(
<MuteModal <MuteModal
muteModalMessages={defaultMessages} muteModalMessages={defaultMessages}
user={{ user={{
@ -196,9 +227,13 @@ describe('MuteModalTest', () => {
token: 'mytoken', token: 'mytoken',
thumbnailUrl: 'mythumbnail' thumbnailUrl: 'mythumbnail'
}} }}
/> />,
).dive(); 'MuteModal'
component.instance().handleFeedbackSubmit('something'); );
expect(component.instance().state.step).toBe(4); const muteModalInstance = instance();
act(() => {
muteModalInstance.handleFeedbackSubmit('something');
});
expect(muteModalInstance.state.step).toBe(4);
}); });
}); });

View file

@ -1,49 +1,52 @@
import React from 'react'; import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import MuteStep from '../../../src/components/modal/mute/mute-step'; import MuteStep from '../../../src/components/modal/mute/mute-step';
import {renderWithIntl} from '../../helpers/react-testing-library-wrapper';
describe('MuteStepTest', () => { describe('MuteStepTest', () => {
test('Mute Step with no images', () => { test('Mute Step with no images', () => {
const component = mountWithIntl( const {container} = renderWithIntl(
<MuteStep <MuteStep
header="header text" header="header text"
/> />,
'MuteStep'
); );
expect(component.find('div.mute-step').exists()).toEqual(true); expect(container.querySelector('div.mute-step')).toBeTruthy();
expect(component.find('div.mute-header').exists()).toEqual(true); expect(container.querySelector('div.mute-header')).toBeTruthy();
expect(component.find('div.mute-right-column').exists()).toEqual(true); expect(container.querySelector('div.mute-right-column')).toBeTruthy();
// No images and no left column. // No images and no left column.
expect(component.find('img').exists()).toEqual(false); expect(container.querySelector('img')).toBeFalsy();
expect(component.find('div.left-column').exists()).toEqual(false); expect(container.querySelector('div.left-column')).toBeFalsy();
}); });
test('Mute Step with side image', () => { test('Mute Step with side image', () => {
const component = mountWithIntl( const {container} = renderWithIntl(
<MuteStep <MuteStep
sideImg="/path/to/img.png" sideImg="/path/to/img.png"
sideImgClass="side-img" sideImgClass="side-img"
/> />,
'MuteStep'
); );
expect(component.find('div.mute-step').exists()).toEqual(true); expect(container.querySelector('div.mute-step')).toBeTruthy();
expect(component.find('div.mute-header').exists()).toEqual(true); expect(container.querySelector('div.mute-header')).toBeTruthy();
expect(component.find('div.mute-right-column').exists()).toEqual(true); expect(container.querySelector('div.mute-right-column')).toBeTruthy();
expect(component.find('div.left-column').exists()).toEqual(true); expect(container.querySelector('div.left-column')).toBeTruthy();
expect(component.find('img.side-img').exists()).toEqual(true); expect(container.querySelector('img.side-img')).toBeTruthy();
}); });
test('Mute Step with bottom image', () => { test('Mute Step with bottom image', () => {
const component = mountWithIntl( const {container} = renderWithIntl(
<MuteStep <MuteStep
bottomImg="/path/to/img.png" bottomImg="/path/to/img.png"
bottomImgClass="bottom-image" bottomImgClass="bottom-image"
/> />,
'MuteStep'
); );
expect(component.find('div.mute-step').exists()).toEqual(true); expect(container.querySelector('div.mute-step')).toBeTruthy();
expect(component.find('div.mute-header').exists()).toEqual(true); expect(container.querySelector('div.mute-header')).toBeTruthy();
expect(component.find('div.mute-right-column').exists()).toEqual(true); expect(container.querySelector('div.mute-right-column')).toBeTruthy();
expect(component.find('img.bottom-image').exists()).toEqual(true); expect(container.querySelector('img.bottom-image')).toBeTruthy();
}); });
}); });

View file

@ -1,8 +1,9 @@
const React = require('react'); const React = require('react');
const {shallowWithIntl, mountWithIntl} = require('../../helpers/intl-helpers.jsx'); import {Provider} from 'react-redux';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import {renderWithIntl} from '../../helpers/react-testing-library-wrapper.jsx';
import {fireEvent} from '@testing-library/react';
const Navigation = require('../../../src/components/navigation/www/navigation.jsx'); const Navigation = require('../../../src/components/navigation/www/navigation.jsx');
const Registration = require('../../../src/components/registration/registration.jsx');
const sessionActions = require('../../../src/redux/session.js'); const sessionActions = require('../../../src/redux/session.js');
describe('Navigation', () => { describe('Navigation', () => {
@ -17,15 +18,15 @@ describe('Navigation', () => {
}); });
const getNavigationWrapper = props => { const getNavigationWrapper = props => {
const wrapper = shallowWithIntl( const wrapper = renderWithIntl(
<Navigation <Provider store={store}>
{...props} <Navigation
/> {...props}
, {context: {store}} />
</Provider>,
'Navigation'
); );
return wrapper return wrapper; // unwrap injectIntl(JoinFlow)
.dive() // unwrap redux connect(injectIntl(JoinFlow))
.dive(); // unwrap injectIntl(JoinFlow)
}; };
test('when using old join flow, when registrationOpen is true, iframe shows', () => { test('when using old join flow, when registrationOpen is true, iframe shows', () => {
@ -42,7 +43,7 @@ describe('Navigation', () => {
} }
}); });
const navWrapper = getNavigationWrapper(); const navWrapper = getNavigationWrapper();
expect(navWrapper.contains(<Registration />)).toEqual(true); expect(navWrapper.findByComponentName('Registration')).toBeTruthy();
}); });
test('when using new join flow, when registrationOpen is true, iframe does not show', () => { test('when using new join flow, when registrationOpen is true, iframe does not show', () => {
@ -59,7 +60,7 @@ describe('Navigation', () => {
} }
}); });
const navWrapper = getNavigationWrapper(); const navWrapper = getNavigationWrapper();
expect(navWrapper.contains(<Registration />)).toEqual(false); expect(navWrapper.findByComponentName('Registration')).toBeFalsy();
}); });
test('when using old join flow, clicking Join Scratch calls handleRegistrationRequested', () => { test('when using old join flow, clicking Join Scratch calls handleRegistrationRequested', () => {
@ -81,7 +82,7 @@ describe('Navigation', () => {
const navInstance = navWrapper.instance(); const navInstance = navWrapper.instance();
// simulate click, with mocked event // simulate click, with mocked event
navWrapper.find('a.registrationLink').simulate('click', {preventDefault () {}}); fireEvent.click(navWrapper.container.querySelector('a.registrationLink'));
expect(navInstance.props.handleClickRegistration).toHaveBeenCalled(); expect(navInstance.props.handleClickRegistration).toHaveBeenCalled();
}); });
@ -103,7 +104,7 @@ describe('Navigation', () => {
const navWrapper = getNavigationWrapper(props); const navWrapper = getNavigationWrapper(props);
const navInstance = navWrapper.instance(); const navInstance = navWrapper.instance();
navWrapper.find('a.registrationLink').simulate('click', {preventDefault () {}}); fireEvent.click(navWrapper.container.querySelector('a.registrationLink'));
expect(navInstance.props.handleClickRegistration).toHaveBeenCalled(); expect(navInstance.props.handleClickRegistration).toHaveBeenCalled();
}); });
@ -123,15 +124,9 @@ describe('Navigation', () => {
}, },
getMessageCount: jest.fn() getMessageCount: jest.fn()
}; };
const intlWrapper = mountWithIntl( const intlWrapper = getNavigationWrapper(props);
<Navigation
{...props}
/>, {context: {store},
childContextTypes: {store}
});
const navInstance = intlWrapper.children().find('Navigation') const navInstance = intlWrapper.findByComponentName('Navigation');
.instance();
const twoMin = 2 * 60 * 1000; const twoMin = 2 * 60 * 1000;
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), twoMin); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), twoMin);
expect(navInstance.messageCountTimeoutId).not.toEqual(-1); expect(navInstance.messageCountTimeoutId).not.toEqual(-1);
@ -157,15 +152,9 @@ describe('Navigation', () => {
}, },
getMessageCount: jest.fn() getMessageCount: jest.fn()
}; };
const intlWrapper = mountWithIntl( const intlWrapper = getNavigationWrapper(props);
<Navigation
{...props}
/>, {context: {store},
childContextTypes: {store}
});
const navInstance = intlWrapper.children().find('Navigation') const navInstance = intlWrapper.findByComponentName('Navigation');
.instance();
const twoMin = 2 * 60 * 1000; const twoMin = 2 * 60 * 1000;
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), twoMin); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), twoMin);
expect(navInstance.messageCountTimeoutId).not.toEqual(-1); expect(navInstance.messageCountTimeoutId).not.toEqual(-1);
@ -190,15 +179,9 @@ describe('Navigation', () => {
}, },
getMessageCount: jest.fn() getMessageCount: jest.fn()
}; };
const intlWrapper = mountWithIntl( const intlWrapper = getNavigationWrapper(props);
<Navigation
{...props}
/>, {context: {store},
childContextTypes: {store}
});
const navInstance = intlWrapper.children().find('Navigation') const navInstance = intlWrapper.findByComponentName('Navigation');
.instance();
// Clear the timers and mocks because componentDidMount // Clear the timers and mocks because componentDidMount
// has already called pollForMessages. // has already called pollForMessages.
jest.clearAllTimers(); jest.clearAllTimers();

View file

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import NextStepButton from '../../../src/components/join-flow/next-step-button'; import NextStepButton from '../../../src/components/join-flow/next-step-button';
import Spinner from '../../../src/components/spinner/spinner.jsx'; import {renderWithIntl} from '../../helpers/react-testing-library-wrapper.jsx';
describe('NextStepButton', () => { describe('NextStepButton', () => {
const defaultProps = () => ({ const defaultProps = () => ({
@ -10,23 +9,25 @@ describe('NextStepButton', () => {
}); });
test('testing spinner does not show and button enabled', () => { test('testing spinner does not show and button enabled', () => {
const component = mountWithIntl( const {container, findByComponentName} = renderWithIntl(
<NextStepButton <NextStepButton
{...defaultProps()} {...defaultProps()}
/> />,
'NextStepButton'
); );
expect(component.find(Spinner).exists()).toEqual(false); expect(findByComponentName('Spinner')).toBeFalsy();
expect(component.find('button[type="submit"]').prop('disabled')).toBe(false); expect(container.querySelector('button[type="submit"]').disabled).toBe(false);
}); });
test('testing spinner does show and button disabled', () => { test('testing spinner does show and button disabled', () => {
const component = mountWithIntl( const {container, findByComponentName} = renderWithIntl(
<NextStepButton <NextStepButton
{...defaultProps()} {...defaultProps()}
/> waiting
/>,
'NextStepButton'
); );
component.setProps({waiting: true}); expect(findByComponentName('Spinner')).toBeTruthy();
expect(component.find(Spinner).exists()).toEqual(true); expect(container.querySelector('button[type="submit"]').disabled).toBe(true);
expect(component.find('button[type="submit"]').prop('disabled')).toBe(true);
}); });
}); });

View file

@ -1,32 +1,35 @@
import React from 'react'; import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import OSChooser from '../../../src/components/os-chooser/os-chooser'; import OSChooser from '../../../src/components/os-chooser/os-chooser';
import {renderWithIntl} from '../../helpers/react-testing-library-wrapper';
import {fireEvent} from '@testing-library/react';
describe('OSChooser', () => { describe('OSChooser', () => {
test('calls callback when OS is selected', () => { test('calls callback when OS is selected', () => {
const onSetOs = jest.fn(); const onSetOs = jest.fn();
const component = mountWithIntl(<OSChooser handleSetOS={onSetOs} />); const {container} = renderWithIntl(<OSChooser handleSetOS={onSetOs} />, 'OSChooser');
component.find('button').last() const buttons = container.querySelectorAll('button');
.simulate('click'); fireEvent.click(buttons[buttons.length - 1]);
expect(onSetOs).toHaveBeenCalledWith('Android'); expect(onSetOs).toHaveBeenCalledWith('Android');
}); });
test('has all 4 operating systems', () => { test('has all 4 operating systems', () => {
const component = mountWithIntl(<OSChooser />); const {container} = renderWithIntl(<OSChooser />, 'OSChooser');
expect(component.find('button').length).toEqual(4); expect(container.querySelectorAll('button').length).toEqual(4);
}); });
test('hides operating systems', () => { test('hides operating systems', () => {
const component = mountWithIntl(<OSChooser const {container} = renderWithIntl(<OSChooser
hideWindows hideWindows
hideMac hideMac
hideChromeOS hideChromeOS
hideAndroid hideAndroid
/>); />,
'OSChooser'
);
expect(component.find('button').length).toEqual(0); expect(container.querySelectorAll('button').length).toEqual(0);
}); });
}); });

View file

@ -1,21 +1,48 @@
const React = require('react'); const React = require('react');
const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx');
const Page = require('../../../src/components/page/www/page.jsx'); const Page = require('../../../src/components/page/www/page.jsx');
const {renderWithIntl} = require('../../helpers/react-testing-library-wrapper.jsx');
const {default: configureStore} = require('redux-mock-store');
const {Provider} = require('react-redux');
const sessionActions = require('../../../src/redux/session.js');
describe('Page', () => { describe('Page', () => {
const mockStore = configureStore();
let store;
beforeEach(() => {
store = mockStore({
navigation: {
registrationOpen: true,
useScratch3Registration: false
},
session: {
status: sessionActions.Status.FETCHED
},
messageCount: {
messageCount: 0
}
});
});
test('Do not show donor recognition', () => { test('Do not show donor recognition', () => {
const component = shallowWithIntl( const {findAllByComponentName} = renderWithIntl(
<Page /> <Provider store={store}>
<Page />
</Provider>,
'Page'
); );
expect(component.find('#donor')).toHaveLength(0); expect(findAllByComponentName('DonorRecognition')).toHaveLength(0);
}); });
test('Show donor recognition', () => { test('Show donor recognition', () => {
const component = shallowWithIntl( const {findAllByComponentName} = renderWithIntl(
<Page <Provider store={store}>
showDonorRecognition <Page
/> showDonorRecognition
/>
</Provider>,
'Page'
); );
expect(component.find('#donor')).toHaveLength(1); expect(findAllByComponentName('DonorRecognition')).toHaveLength(1);
}); });
}); });

View file

@ -1,20 +1,19 @@
import React from 'react'; import React from 'react';
import {mountWithIntl, shallowWithIntl} from '../../helpers/intl-helpers.jsx';
import JoinFlowStep from '../../../src/components/join-flow/join-flow-step';
import RegistrationErrorStep from '../../../src/components/join-flow/registration-error-step'; import RegistrationErrorStep from '../../../src/components/join-flow/registration-error-step';
import {renderWithIntl} from '../../helpers/react-testing-library-wrapper';
describe('RegistrationErrorStep', () => { describe('RegistrationErrorStep', () => {
const onSubmit = jest.fn(); const onSubmit = jest.fn();
const getRegistrationErrorStepWrapper = props => { const getRegistrationErrorStepWrapper = props => {
const wrapper = shallowWithIntl( const wrapper = renderWithIntl(
<RegistrationErrorStep <RegistrationErrorStep
sendAnalytics={jest.fn()} sendAnalytics={jest.fn()}
{...props} {...props}
/> />,
'RegistrationErrorStep'
); );
return wrapper return wrapper;
.dive(); // unwrap injectIntl()
}; };
test('registrationError has JoinFlowStep', () => { test('registrationError has JoinFlowStep', () => {
@ -22,7 +21,10 @@ describe('RegistrationErrorStep', () => {
canTryAgain: true, canTryAgain: true,
onSubmit: onSubmit onSubmit: onSubmit
}; };
const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep); const joinFlowStepWrapper =
getRegistrationErrorStepWrapper(props).findAllByComponentName(
'JoinFlowStep'
);
expect(joinFlowStepWrapper).toHaveLength(1); expect(joinFlowStepWrapper).toHaveLength(1);
}); });
@ -32,11 +34,11 @@ describe('RegistrationErrorStep', () => {
errorMsg: 'halp there is a errors!!', errorMsg: 'halp there is a errors!!',
onSubmit: onSubmit onSubmit: onSubmit
}; };
const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep); const errMsgElement = getRegistrationErrorStepWrapper(
const joinFlowStepInstance = joinFlowStepWrapper.dive(); props
const errMsgElement = joinFlowStepInstance.find('.registration-error-msg'); ).container.querySelector('.registration-error-msg');
expect(errMsgElement).toHaveLength(1); expect(errMsgElement).toBeTruthy();
expect(errMsgElement.text()).toEqual('halp there is a errors!!'); expect(errMsgElement.textContent).toEqual('halp there is a errors!!');
}); });
test('when errorMsg is null, registrationError does not show it', () => { test('when errorMsg is null, registrationError does not show it', () => {
@ -45,10 +47,10 @@ describe('RegistrationErrorStep', () => {
errorMsg: null, errorMsg: null,
onSubmit: onSubmit onSubmit: onSubmit
}; };
const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep); const errMsgElement = getRegistrationErrorStepWrapper(
const joinFlowStepInstance = joinFlowStepWrapper.dive(); props
const errMsgElement = joinFlowStepInstance.find('.registration-error-msg'); ).container.querySelector('.registration-error-msg');
expect(errMsgElement).toHaveLength(0); expect(errMsgElement).toBeFalsy();
}); });
test('when no errorMsg provided, registrationError does not show it', () => { test('when no errorMsg provided, registrationError does not show it', () => {
@ -56,18 +58,15 @@ describe('RegistrationErrorStep', () => {
canTryAgain: true, canTryAgain: true,
onSubmit: onSubmit onSubmit: onSubmit
}; };
const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep); const errMsgElement = getRegistrationErrorStepWrapper(
const joinFlowStepInstance = joinFlowStepWrapper.dive(); props
const errMsgElement = joinFlowStepInstance.find('.registration-error-msg'); ).container.querySelector('.registration-error-msg');
expect(errMsgElement).toHaveLength(0); expect(errMsgElement).toBeFalsy();
}); });
test('logs to analytics', () => { test('logs to analytics', () => {
const analyticsFn = jest.fn(); const analyticsFn = jest.fn();
mountWithIntl( renderWithIntl(<RegistrationErrorStep sendAnalytics={analyticsFn} />);
<RegistrationErrorStep
sendAnalytics={analyticsFn}
/>);
expect(analyticsFn).toHaveBeenCalledWith('join-error'); expect(analyticsFn).toHaveBeenCalledWith('join-error');
}); });
test('when canTryAgain is true, show tryAgain message', () => { test('when canTryAgain is true, show tryAgain message', () => {
@ -76,9 +75,12 @@ describe('RegistrationErrorStep', () => {
errorMsg: 'halp there is a errors!!', errorMsg: 'halp there is a errors!!',
onSubmit: onSubmit onSubmit: onSubmit
}; };
const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep); const joinFlowStepWrapper =
getRegistrationErrorStepWrapper(props).findAllByComponentName(
'JoinFlowStep'
);
expect(joinFlowStepWrapper).toHaveLength(1); expect(joinFlowStepWrapper).toHaveLength(1);
expect(joinFlowStepWrapper.props().nextButton).toBe('general.tryAgain'); expect(joinFlowStepWrapper[0].memoizedProps.nextButton).toBe('Try again');
}); });
test('when canTryAgain is false, show startOver message', () => { test('when canTryAgain is false, show startOver message', () => {
@ -87,9 +89,12 @@ describe('RegistrationErrorStep', () => {
errorMsg: 'halp there is a errors!!', errorMsg: 'halp there is a errors!!',
onSubmit: onSubmit onSubmit: onSubmit
}; };
const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep); const joinFlowStepWrapper =
getRegistrationErrorStepWrapper(props).findAllByComponentName(
'JoinFlowStep'
);
expect(joinFlowStepWrapper).toHaveLength(1); expect(joinFlowStepWrapper).toHaveLength(1);
expect(joinFlowStepWrapper.props().nextButton).toBe('general.startOver'); expect(joinFlowStepWrapper[0].memoizedProps.nextButton).toBe('Start over');
}); });
test('when canTryAgain is missing, show startOver message', () => { test('when canTryAgain is missing, show startOver message', () => {
@ -97,9 +102,12 @@ describe('RegistrationErrorStep', () => {
errorMsg: 'halp there is a errors!!', errorMsg: 'halp there is a errors!!',
onSubmit: onSubmit onSubmit: onSubmit
}; };
const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep); const joinFlowStepWrapper =
getRegistrationErrorStepWrapper(props).findAllByComponentName(
'JoinFlowStep'
);
expect(joinFlowStepWrapper).toHaveLength(1); expect(joinFlowStepWrapper).toHaveLength(1);
expect(joinFlowStepWrapper.props().nextButton).toBe('general.startOver'); expect(joinFlowStepWrapper[0].memoizedProps.nextButton).toBe('Start over');
}); });
test('when submitted, onSubmit is called', () => { test('when submitted, onSubmit is called', () => {
@ -108,8 +116,11 @@ describe('RegistrationErrorStep', () => {
errorMsg: 'halp there is a errors!!', errorMsg: 'halp there is a errors!!',
onSubmit: onSubmit onSubmit: onSubmit
}; };
const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep); const joinFlowStepWrapper =
joinFlowStepWrapper.props().onSubmit(new Event('event')); // eslint-disable-line no-undef getRegistrationErrorStepWrapper(props).findByComponentName(
'JoinFlowStep'
);
joinFlowStepWrapper.memoizedProps.onSubmit(new Event('event')); // eslint-disable-line no-undef
expect(onSubmit).toHaveBeenCalled(); expect(onSubmit).toHaveBeenCalled();
}); });
}); });

View file

@ -1,11 +1,10 @@
import React from 'react'; import React, {act} from 'react';
import {act} from 'react-dom/test-utils';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import AdminPanel from '../../../src/components/adminpanel/adminpanel.jsx';
import { import {
StudioAdminPanel, adminPanelOpenClass, adminPanelOpenKey StudioAdminPanel, adminPanelOpenClass, adminPanelOpenKey
} from '../../../src/views/studio/studio-admin-panel.jsx'; } from '../../../src/views/studio/studio-admin-panel.jsx';
import {renderWithIntl} from '../../helpers/react-testing-library-wrapper.jsx';
import {fireEvent} from '@testing-library/react';
let viewEl; let viewEl;
describe('Studio comments', () => { describe('Studio comments', () => {
@ -21,60 +20,61 @@ describe('Studio comments', () => {
describe('gets stored state from local storage if available', () => { describe('gets stored state from local storage if available', () => {
test('stored as open', () => { test('stored as open', () => {
global.localStorage.setItem(adminPanelOpenKey, 'open'); global.localStorage.setItem(adminPanelOpenKey, 'open');
const component = mountWithIntl(<StudioAdminPanel showAdminPanel />); const {findByComponentName} = renderWithIntl(<StudioAdminPanel showAdminPanel />, 'StudioAdminPanel');
const child = component.find(AdminPanel); const child = findByComponentName('AdminPanel');
expect(child.prop('isOpen')).toBe(true); expect(child.memoizedProps.isOpen).toBe(true);
}); });
test('stored as closed', () => { test('stored as closed', () => {
global.localStorage.setItem(adminPanelOpenKey, 'closed'); global.localStorage.setItem(adminPanelOpenKey, 'closed');
const component = mountWithIntl(<StudioAdminPanel showAdminPanel />); const {findByComponentName} = renderWithIntl(<StudioAdminPanel showAdminPanel />, 'StudioAdminPanel');
const child = component.find(AdminPanel); const child = findByComponentName('AdminPanel');
expect(child.prop('isOpen')).toBe(false); expect(child.memoizedProps.isOpen).toBe(false);
}); });
test('not stored', () => { test('not stored', () => {
const component = mountWithIntl( const {findByComponentName} = renderWithIntl(
<StudioAdminPanel showAdminPanel /> <StudioAdminPanel showAdminPanel />, 'StudioAdminPanel'
); );
const child = component.find(AdminPanel); const child = findByComponentName('AdminPanel');
expect(child.prop('isOpen')).toBe(false); expect(child.memoizedProps.isOpen).toBe(false);
}); });
}); });
describe('non admins', () => { describe('non admins', () => {
test('should not have localStorage set with a false value', () => { test('should not have localStorage set with a false value', () => {
mountWithIntl(<StudioAdminPanel showAdminPanel={false} />); renderWithIntl(<StudioAdminPanel showAdminPanel={false} />);
expect(global.localStorage.getItem(adminPanelOpenKey)).toBe(null); expect(global.localStorage.getItem(adminPanelOpenKey)).toBe(null);
}); });
test('should not have css class set even if localStorage contains open key', () => { test('should not have css class set even if localStorage contains open key', () => {
// Regression test for situation where admin logs out but localStorage still // Regression test for situation where admin logs out but localStorage still
// contains "open", causing extra space to appear // contains "open", causing extra space to appear
global.localStorage.setItem(adminPanelOpenKey, 'open'); global.localStorage.setItem(adminPanelOpenKey, 'open');
mountWithIntl(<StudioAdminPanel showAdminPanel={false} />); renderWithIntl(<StudioAdminPanel showAdminPanel={false} />);
expect(viewEl.classList.contains(adminPanelOpenClass)).toBe(false); expect(viewEl.classList.contains(adminPanelOpenClass)).toBe(false);
}); });
}); });
test('calling onOpen sets a class on the #viewEl and records in local storage', () => { test('calling onOpen sets a class on the #viewEl and records in local storage', () => {
const component = mountWithIntl(<StudioAdminPanel showAdminPanel />); const {container} = renderWithIntl(<StudioAdminPanel showAdminPanel />, 'StudioAdminPanel');
const child = component.find(AdminPanel); const child = container.querySelector('.toggle');
expect(viewEl.classList.contains(adminPanelOpenClass)).toBe(false); expect(viewEl.classList.contains(adminPanelOpenClass)).toBe(false);
// `act` is a test-util function for making react state updates sync // `act` is a test-util function for making react state updates sync
act(child.prop('onOpen')); fireEvent.click(child);
expect(viewEl.classList.contains(adminPanelOpenClass)).toBe(true); expect(viewEl.classList.contains(adminPanelOpenClass)).toBe(true);
expect(global.localStorage.getItem(adminPanelOpenKey)).toBe('open'); expect(global.localStorage.getItem(adminPanelOpenKey)).toBe('open');
}); });
test('renders the correct iframe when open', () => { test('renders the correct iframe when open', () => {
global.localStorage.setItem(adminPanelOpenKey, 'open'); global.localStorage.setItem(adminPanelOpenKey, 'open');
const component = mountWithIntl( const {container} = renderWithIntl(
<StudioAdminPanel <StudioAdminPanel
studioId={123} studioId={123}
showAdminPanel showAdminPanel
/> />,
'StudioAdminPanel'
); );
const child = component.find('iframe'); const child = container.querySelector('iframe');
expect(child.getDOMNode().src).toMatch('/scratch2-studios/123/adminpanel'); expect(child.src).toMatch('/scratch2-studios/123/adminpanel');
}); });
test('responds to closePanel MessageEvent from the iframe', () => { test('responds to closePanel MessageEvent from the iframe', () => {
global.localStorage.setItem(adminPanelOpenKey, 'open'); global.localStorage.setItem(adminPanelOpenKey, 'open');
mountWithIntl(<StudioAdminPanel showAdminPanel />); renderWithIntl(<StudioAdminPanel showAdminPanel />);
expect(viewEl.classList.contains(adminPanelOpenClass)).toBe(true); expect(viewEl.classList.contains(adminPanelOpenClass)).toBe(true);
act(() => { act(() => {
global.window.dispatchEvent(new global.MessageEvent('message', {data: 'closePanel'})); global.window.dispatchEvent(new global.MessageEvent('message', {data: 'closePanel'}));

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import {StudioComments} from '../../../src/views/studio/studio-comments.jsx'; import {StudioComments} from '../../../src/views/studio/studio-comments.jsx';
import {renderWithIntl} from '../../helpers/react-testing-library-wrapper.jsx';
// Replace customized studio comment with default comment to avoid redux issues in the test // Replace customized studio comment with default comment to avoid redux issues in the test
jest.mock('../../../src/views/studio/studio-comment.js', () => ( jest.mock('../../../src/views/studio/studio-comment.js', () => (
@ -13,61 +13,76 @@ describe('Studio comments', () => {
test('if there are no comments, they get loaded', () => { test('if there are no comments, they get loaded', () => {
const loadComments = jest.fn(); const loadComments = jest.fn();
const component = mountWithIntl( const props = {
hasFetchedSession: false,
comments: [],
handleLoadMoreComments: loadComments
};
const {rerenderWithIntl} = renderWithIntl(
<StudioComments <StudioComments
hasFetchedSession={false} {...props}
comments={[]} />,
handleLoadMoreComments={loadComments} 'StudioComments'
/>
); );
expect(loadComments).not.toHaveBeenCalled(); expect(loadComments).not.toHaveBeenCalled();
component.setProps({hasFetchedSession: true}); rerenderWithIntl(<StudioComments
component.update(); {...props}
hasFetchedSession
/>);
expect(loadComments).toHaveBeenCalled(); expect(loadComments).toHaveBeenCalled();
// When updated to have comments, load is not called again // When updated to have comments, load is not called again
loadComments.mockClear(); loadComments.mockClear();
component.setProps({comments: testComments}); rerenderWithIntl(<StudioComments
component.update(); {...props}
comments={testComments}
/>);
expect(loadComments).not.toHaveBeenCalled(); expect(loadComments).not.toHaveBeenCalled();
// When reset to have no comments again, load is called again // When reset to have no comments again, load is called again
loadComments.mockClear(); loadComments.mockClear();
component.setProps({comments: []}); rerenderWithIntl(<StudioComments
component.update(); {...props}
hasFetchedSession
comments={[]}
/>);
expect(loadComments).toHaveBeenCalled(); expect(loadComments).toHaveBeenCalled();
}); });
test('becoming an admin resets the comments', () => { test('becoming an admin resets the comments', () => {
const resetComments = jest.fn(); const resetComments = jest.fn();
const component = mountWithIntl( const props = {
hasFetchedSession: true,
isAdmin: false,
comments: testComments,
handleResetComments: resetComments
};
const {rerenderWithIntl} = renderWithIntl(
<StudioComments <StudioComments
hasFetchedSession {...props}
isAdmin={false}
comments={testComments}
handleResetComments={resetComments}
/> />
); );
expect(resetComments).not.toHaveBeenCalled(); expect(resetComments).not.toHaveBeenCalled();
// When updated to isAdmin=true, reset is called // When updated to isAdmin=true, reset is called
resetComments.mockClear(); resetComments.mockClear();
component.setProps({isAdmin: true}); rerenderWithIntl(<StudioComments
component.update(); {...props}
isAdmin
/>);
expect(resetComments).toHaveBeenCalled(); expect(resetComments).toHaveBeenCalled();
// If updated back to isAdmin=false, reset is also called // If updated back to isAdmin=false, reset is also called
// not currently possible in the UI, but if it was, we'd want to clear comments // not currently possible in the UI, but if it was, we'd want to clear comments
resetComments.mockClear(); resetComments.mockClear();
component.setProps({isAdmin: false}); rerenderWithIntl();
component.update();
expect(resetComments).toHaveBeenCalled(); expect(resetComments).toHaveBeenCalled();
}); });
test('being an admin on initial render doesnt reset comments', () => { test('being an admin on initial render doesnt reset comments', () => {
// This ensures that comments don't get reloaded when changing tabs // This ensures that comments don't get reloaded when changing tabs
const resetComments = jest.fn(); const resetComments = jest.fn();
mountWithIntl( renderWithIntl(
<StudioComments <StudioComments
isAdmin isAdmin
hasFetchedSession hasFetchedSession
@ -79,85 +94,90 @@ describe('Studio comments', () => {
}); });
test('Comments do not show when shouldShowCommentsList is false', () => { test('Comments do not show when shouldShowCommentsList is false', () => {
const component = mountWithIntl( const {container, findByComponentName} = renderWithIntl(
<StudioComments <StudioComments
hasFetchedSession hasFetchedSession
isAdmin={false} isAdmin={false}
comments={testComments} comments={testComments}
shouldShowCommentsList={false} shouldShowCommentsList={false}
/> />,
'StudioComments'
); );
expect(component.find('div.studio-compose-container').exists()).toBe(true); expect(container.querySelector('div.studio-compose-container')).toBeTruthy();
expect(component.find('TopLevelComment').exists()).toBe(false); expect(findByComponentName('TopLevelComment')).toBeFalsy();
}); });
test('Comments show when shouldShowCommentsList is true', () => { test('Comments show when shouldShowCommentsList is true', () => {
const component = mountWithIntl( const {container, findByComponentName} = renderWithIntl(
<StudioComments <StudioComments
hasFetchedSession hasFetchedSession
isAdmin={false} isAdmin={false}
comments={testComments} comments={testComments}
shouldShowCommentsList shouldShowCommentsList
/> />,
'StudioComments'
); );
expect(component.find('div.studio-compose-container').exists()).toBe(true); expect(container.querySelector('div.studio-compose-container')).toBeTruthy();
expect(component.find('TopLevelComment').exists()).toBe(true); expect(findByComponentName('TopLevelComment')).toBeTruthy();
}); });
test('Single comment load more shows when shouldShowCommentsList is true', () => { test('Single comment load more shows when shouldShowCommentsList is true', () => {
// Make the component think this is a single view. // Make the component think this is a single view.
global.window.location.hash = '#comments-6'; global.window.location.hash = '#comments-6';
const component = mountWithIntl( const {container, findByComponentName} = renderWithIntl(
<StudioComments <StudioComments
hasFetchedSession hasFetchedSession
isAdmin={false} isAdmin={false}
comments={testComments} comments={testComments}
shouldShowCommentsList shouldShowCommentsList
singleCommentId singleCommentId
/> />,
'StudioComments'
); );
expect(component.find('div.studio-compose-container').exists()).toBe(true); expect(container.querySelector('div.studio-compose-container')).toBeTruthy();
expect(component.find('TopLevelComment').exists()).toBe(true); expect(findByComponentName('TopLevelComment')).toBeTruthy();
expect(component.find('Button').exists()).toBe(true); expect(findByComponentName('Button')).toBeTruthy();
expect(component.find('button.load-more-button').exists()).toBe(true); expect(container.querySelector('button.load-more-button')).toBeTruthy();
global.window.location.hash = ''; global.window.location.hash = '';
}); });
test('Single comment does not show when shouldShowCommentsList is false', () => { test('Single comment does not show when shouldShowCommentsList is false', () => {
// Make the component think this is a single view. // Make the component think this is a single view.
global.window.location.hash = '#comments-6'; global.window.location.hash = '#comments-6';
const component = mountWithIntl( const {container, findByComponentName} = renderWithIntl(
<StudioComments <StudioComments
hasFetchedSession hasFetchedSession
isAdmin={false} isAdmin={false}
comments={testComments} comments={testComments}
shouldShowCommentsList={false} shouldShowCommentsList={false}
singleCommentId singleCommentId
/> />,
'StudioComments'
); );
expect(component.find('div.studio-compose-container').exists()).toBe(true); expect(container.querySelector('div.studio-compose-container')).toBeTruthy();
expect(component.find('TopLevelComment').exists()).toBe(false); expect(findByComponentName('TopLevelComment')).toBeFalsy();
expect(component.find('Button').exists()).toBe(false); expect(container.querySelector('Button')).toBeFalsy();
expect(component.find('button.load-more-button').exists()).toBe(false); expect(container.querySelector('button.load-more-button')).toBeFalsy();
global.window.location.hash = ''; global.window.location.hash = '';
}); });
test('Comment status error shows when shoudlShowCommentsGloballyOffError is true', () => { test('Comment status error shows when shoudlShowCommentsGloballyOffError is true', () => {
const component = mountWithIntl( const {container, findByComponentName} = renderWithIntl(
<StudioComments <StudioComments
hasFetchedSession={false} hasFetchedSession={false}
isAdmin={false} isAdmin={false}
comments={testComments} comments={testComments}
shouldShowCommentsGloballyOffError shouldShowCommentsGloballyOffError
/> />,
'StudioComments'
); );
expect(component.find('div.studio-compose-container').exists()).toBe(true); expect(container.querySelector('div.studio-compose-container')).toBeTruthy();
expect(component.find('CommentingStatus').exists()).toBe(true); expect(findByComponentName('CommentingStatus')).toBeTruthy();
}); });
test('Comment status error does not show when shoudlShowCommentsGloballyOffError is false', () => { test('Comment status error does not show when shoudlShowCommentsGloballyOffError is false', () => {
const component = mountWithIntl( const {container} = renderWithIntl(
<StudioComments <StudioComments
hasFetchedSession={false} hasFetchedSession={false}
isAdmin={false} isAdmin={false}
@ -165,7 +185,7 @@ describe('Studio comments', () => {
shouldShowCommentsGloballyOffError={false} shouldShowCommentsGloballyOffError={false}
/> />
); );
expect(component.find('div.studio-compose-container').exists()).toBe(true); expect(container.querySelector('div.studio-compose-container')).toBeTruthy();
expect(component.find('CommentingStatus').exists()).toBe(false); expect(container.querySelector('CommentingStatus')).toBeFalsy();
}); });
}); });

View file

@ -1,6 +1,4 @@
const React = require('react'); const React = require('react');
const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx');
const {mountWithIntl} = require('../../helpers/intl-helpers.jsx');
const requestSuccessResponse = { const requestSuccessResponse = {
requestSucceeded: true, requestSucceeded: true,
@ -33,6 +31,7 @@ jest.mock('../../../src/lib/validate.js', () => (
// must come after validation mocks, so validate.js will be mocked before it is required // must come after validation mocks, so validate.js will be mocked before it is required
const UsernameStep = require('../../../src/components/join-flow/username-step.jsx'); const UsernameStep = require('../../../src/components/join-flow/username-step.jsx');
const {renderWithIntl} = require('../../helpers/react-testing-library-wrapper.jsx');
describe('UsernameStep tests', () => { describe('UsernameStep tests', () => {
@ -44,25 +43,27 @@ describe('UsernameStep tests', () => {
}); });
test('send correct props to formik', () => { test('send correct props to formik', () => {
const wrapper = shallowWithIntl(<UsernameStep const {instance, findByComponentName} = renderWithIntl(<UsernameStep
{...defaultProps()} {...defaultProps()}
/>); />,
const formikWrapper = wrapper.dive(); 'UsernameStep');
expect(formikWrapper.props().initialValues.username).toBe(''); const formikComponent = findByComponentName('Formik');
expect(formikWrapper.props().initialValues.password).toBe(''); const usernameStepInstance = instance();
expect(formikWrapper.props().initialValues.passwordConfirm).toBe(''); expect(formikComponent.memoizedProps.initialValues.username).toBe('');
expect(formikWrapper.props().initialValues.showPassword).toBe(true); expect(formikComponent.memoizedProps.initialValues.password).toBe('');
expect(formikWrapper.props().validateOnBlur).toBe(false); expect(formikComponent.memoizedProps.initialValues.passwordConfirm).toBe('');
expect(formikWrapper.props().validateOnChange).toBe(false); expect(formikComponent.memoizedProps.initialValues.showPassword).toBe(true);
expect(formikWrapper.props().validate).toBe(formikWrapper.instance().validateForm); expect(formikComponent.memoizedProps.validateOnBlur).toBe(false);
expect(formikWrapper.props().onSubmit).toBe(formikWrapper.instance().handleValidSubmit); expect(formikComponent.memoizedProps.validateOnChange).toBe(false);
expect(formikComponent.memoizedProps.validate).toBe(usernameStepInstance.validateForm);
expect(formikComponent.memoizedProps.onSubmit).toBe(usernameStepInstance.handleValidSubmit);
}); });
test('Component does not log if path is /join', () => { test('Component does not log if path is /join', () => {
const sendAnalyticsFn = jest.fn(); const sendAnalyticsFn = jest.fn();
global.window.history.pushState({}, '', '/join'); global.window.history.pushState({}, '', '/join');
mountWithIntl( renderWithIntl(
<UsernameStep <UsernameStep
sendAnalytics={sendAnalyticsFn} sendAnalytics={sendAnalyticsFn}
/>); />);
@ -73,7 +74,7 @@ describe('UsernameStep tests', () => {
// Make sure '/join' is NOT in the path // Make sure '/join' is NOT in the path
global.window.history.pushState({}, '', '/'); global.window.history.pushState({}, '', '/');
const sendAnalyticsFn = jest.fn(); const sendAnalyticsFn = jest.fn();
mountWithIntl( renderWithIntl(
<UsernameStep <UsernameStep
sendAnalytics={sendAnalyticsFn} sendAnalytics={sendAnalyticsFn}
/>); />);
@ -86,13 +87,14 @@ describe('UsernameStep tests', () => {
}; };
const formData = {item1: 'thing', item2: 'otherthing'}; const formData = {item1: 'thing', item2: 'otherthing'};
const mockedOnNextStep = jest.fn(); const mockedOnNextStep = jest.fn();
const wrapper = shallowWithIntl( const wrapper = renderWithIntl(
<UsernameStep <UsernameStep
{...defaultProps()} {...defaultProps()}
onNextStep={mockedOnNextStep} onNextStep={mockedOnNextStep}
/> />,
'UsernameStep'
); );
const instance = wrapper.dive().instance(); const instance = wrapper.instance();
instance.handleValidSubmit(formData, formikBag); instance.handleValidSubmit(formData, formikBag);
expect(formikBag.setSubmitting).toHaveBeenCalledWith(false); expect(formikBag.setSubmitting).toHaveBeenCalledWith(false);
@ -108,8 +110,8 @@ describe('validateUsernameRemotelyWithCache test with successful requests', () =
}); });
test('validateUsernameRemotelyWithCache calls validate.validateUsernameRemotely', done => { test('validateUsernameRemotelyWithCache calls validate.validateUsernameRemotely', done => {
const wrapper = shallowWithIntl(<UsernameStep />); const wrapper = renderWithIntl(<UsernameStep />, 'UsernameStep');
const instance = wrapper.dive().instance(); const instance = wrapper.instance();
instance.validateUsernameRemotelyWithCache('newUniqueUsername55') instance.validateUsernameRemotelyWithCache('newUniqueUsername55')
.then(response => { .then(response => {
@ -122,10 +124,11 @@ describe('validateUsernameRemotelyWithCache test with successful requests', () =
}); });
test('validateUsernameRemotelyWithCache, called twice with different data, makes two remote requests', done => { test('validateUsernameRemotelyWithCache, called twice with different data, makes two remote requests', done => {
const wrapper = shallowWithIntl( const wrapper = renderWithIntl(
<UsernameStep /> <UsernameStep />,
'UsernameStep'
); );
const instance = wrapper.dive().instance(); const instance = wrapper.instance();
instance.validateUsernameRemotelyWithCache('newUniqueUsername55') instance.validateUsernameRemotelyWithCache('newUniqueUsername55')
.then(response => { .then(response => {
@ -148,10 +151,11 @@ describe('validateUsernameRemotelyWithCache test with successful requests', () =
}); });
test('validateUsernameRemotelyWithCache, called twice with same data, only makes one remote request', done => { test('validateUsernameRemotelyWithCache, called twice with same data, only makes one remote request', done => {
const wrapper = shallowWithIntl( const wrapper = renderWithIntl(
<UsernameStep /> <UsernameStep />,
'UsernameStep'
); );
const instance = wrapper.dive().instance(); const instance = wrapper.instance();
instance.validateUsernameRemotelyWithCache('newUniqueUsername55') instance.validateUsernameRemotelyWithCache('newUniqueUsername55')
.then(response => { .then(response => {
@ -187,8 +191,8 @@ describe('validateUsernameRemotelyWithCache test with failing requests', () => {
}); });
test('validateUsernameRemotelyWithCache calls validate.validateUsernameRemotely', done => { test('validateUsernameRemotelyWithCache calls validate.validateUsernameRemotely', done => {
const wrapper = shallowWithIntl(<UsernameStep />); const wrapper = renderWithIntl(<UsernameStep />, 'UsernameStep');
const instance = wrapper.dive().instance(); const instance = wrapper.instance();
instance.validateUsernameRemotelyWithCache('newUniqueUsername55') instance.validateUsernameRemotelyWithCache('newUniqueUsername55')
.then(response => { .then(response => {
@ -201,10 +205,11 @@ describe('validateUsernameRemotelyWithCache test with failing requests', () => {
}); });
test('validateUsernameRemotelyWithCache, called twice with different data, makes two remote requests', done => { test('validateUsernameRemotelyWithCache, called twice with different data, makes two remote requests', done => {
const wrapper = shallowWithIntl( const wrapper = renderWithIntl(
<UsernameStep /> <UsernameStep />,
'UsernameStep'
); );
const instance = wrapper.dive().instance(); const instance = wrapper.instance();
instance.validateUsernameRemotelyWithCache('newUniqueUsername55') instance.validateUsernameRemotelyWithCache('newUniqueUsername55')
.then(response => { .then(response => {
@ -227,10 +232,11 @@ describe('validateUsernameRemotelyWithCache test with failing requests', () => {
}); });
test('validateUsernameRemotelyWithCache, called 2x w/same data, makes 2 requests, since 1st not stored', done => { test('validateUsernameRemotelyWithCache, called 2x w/same data, makes 2 requests, since 1st not stored', done => {
const wrapper = shallowWithIntl( const wrapper = renderWithIntl(
<UsernameStep /> <UsernameStep />,
'UsernameStep'
); );
const instance = wrapper.dive().instance(); const instance = wrapper.instance();
instance.validateUsernameRemotelyWithCache('newUniqueUsername55') instance.validateUsernameRemotelyWithCache('newUniqueUsername55')
.then(response => { .then(response => {