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/results/*
/test/generated/generated-locales.js
/.nyc_output
/coverage
/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:health": "jest ./test/health/*.test.js",
"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:unit": "jest ./test/unit/ --env=jsdom --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: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:build-translations": "node ./bin/build-translations.js",
"build": "npm run clean && npm run translate && NODE_OPTIONS=--max_old_space_size=8000 webpack --bail",
"build:analyze": "ANALYZE_BUNDLE=true npm run build",
"clean": "rm -rf ./build && rm -rf ./intl && mkdir -p build && mkdir -p intl",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,6 +10,8 @@ require('slick-carousel/slick/slick.scss');
require('slick-carousel/slick/slick-theme.scss');
require('./nestedcarousel.scss');
const defaultItems = require('./nestedcarousel.json');
/*
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.
*/
const NestedCarousel = props => {
defaults(props.settings, {
const NestedCarousel = ({
className,
items = defaultItems,
settings = {}
}) => {
defaults(settings, {
dots: true,
infinite: false,
lazyLoad: true,
@ -27,36 +33,32 @@ const NestedCarousel = props => {
slidesToScroll: 1,
variableWidth: false
});
const arrows = props.items.length > props.settings.slidesToShow;
const arrows = items.length > settings.slidesToShow;
const stages = [];
for (let i = 0; i < props.items.length; i++) {
const items = props.items[i].thumbnails;
const thumbnails = [];
for (let j = 0; j < items.length; j++) {
const item = items[j];
thumbnails.push(
<Thumbnail
key={`inner_${i}_${j}`}
linkTitle={false}
src={item.thumbnailUrl}
title={item.title}
/>
);
}
for (let i = 0; i < items.length; i++) {
const thumbnails = items[i].thumbnails.map((item, j) => (
<Thumbnail
key={`inner_${i}_${j}`}
linkTitle={false}
src={item.thumbnailUrl}
title={item.title}
/>
));
stages.push(
<div key={`outer_${i}`}>
<h3>{props.items[i].title}</h3>
<h3>{items[i].title}</h3>
{thumbnails}
</div>
);
}
return (
<Slider
arrows={arrows}
className={classNames('nestedcarousel', 'carousel', props.className)}
{...props.settings}
className={classNames('nestedcarousel', 'carousel', className)}
{...settings}
>
{stages}
</Slider>
@ -76,9 +78,4 @@ NestedCarousel.propTypes = {
})
};
NestedCarousel.defaultProps = {
settings: {},
items: require('./nestedcarousel.json')
};
module.exports = NestedCarousel;

View file

@ -5,15 +5,23 @@ const Box = require('../box/box.jsx');
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
className="news"
moreHref="/discuss/5/"
moreTitle={props.messages['general.viewAll']}
title={props.messages['news.scratchNews']}
moreTitle={messages['general.viewAll']}
title={messages['news.scratchNews']}
>
<ul>
{props.items.map(item => (
{items.map(item => (
<li key={item.id}>
<a href={item.url}>
<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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,10 +5,7 @@ const classNames = require('classnames');
require('./spinner.scss');
// Adapted from http://tobiasahlin.com/spinkit/
const Spinner = ({
className,
color
}) => (
const Spinner = ({className, color = 'white'}) => (
<img
alt="loading animation"
className={classNames('studio-status-icon-spinner', className)}
@ -16,10 +13,6 @@ const Spinner = ({
/>
);
Spinner.defaultProps = {
color: 'white'
};
Spinner.propTypes = {
className: PropTypes.string,
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
* that can be displayed within a view or component.
*/
const SubNavigation = props => (
const SubNavigation = ({align = 'middle', children, className, role}) => (
<div
className={classNames(
[
'sub-nav',
props.className
className
],
{
'sub-nav-align-left': props.align === 'left',
'sub-nav-align-right': props.align === 'right'
'sub-nav-align-left': align === 'left',
'sub-nav-align-right': align === 'right'
}
)}
role={props.role}
role={role}
>
{props.children}
{children}
</div>
);
@ -33,8 +33,4 @@ SubNavigation.propTypes = {
className: PropTypes.string
};
SubNavigation.defaultProps = {
align: 'middle'
};
module.exports = SubNavigation;

View file

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

View file

@ -4,51 +4,70 @@ const React = require('react');
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 info = [];
if (props.loves && props.showLoves) {
if (loves && showLoves) {
extra.push(
<div
className="thumbnail-loves"
key="loves"
title={`${props.loves} loves`}
title={`${loves} loves`}
>
{props.loves}
{loves}
</div>
);
}
if (props.favorites && props.showFavorites) {
if (favorites && showFavorites) {
extra.push(
<div
className="thumbnail-favorites"
key="favorites"
title={`${props.favorites} favorites`}
title={`${favorites} favorites`}
>
{props.favorites}
{favorites}
</div>
);
}
if (props.remixes && props.showRemixes) {
if (remixes && showRemixes) {
extra.push(
<div
className="thumbnail-remixes"
key="remixes"
title={`${props.remixes} remixes`}
title={`${remixes} remixes`}
>
{props.remixes}
{remixes}
</div>
);
}
if (props.views && props.showViews) {
if (views && showViews) {
extra.push(
<div
className="thumbnail-views"
key="views"
title={`${props.views} views`}
title={`${views} views`}
>
{props.views}
{views}
</div>
);
}
@ -57,65 +76,66 @@ const Thumbnail = props => {
let titleElement;
let avatarElement;
if (props.linkTitle) {
if (linkTitle) {
imgElement = (
<a
className="thumbnail-image"
href={props.href}
href={href}
key="imgElement"
>
<img
alt={props.alt}
src={props.src}
alt={alt}
src={src}
/>
</a>
);
titleElement = (
<a
href={props.href}
href={href}
key="titleElement"
title={props.title}
title={title}
>
{props.title}
{title}
</a>
);
} else {
imgElement = <img src={props.src} />;
titleElement = props.title;
imgElement = <img src={src} />;
titleElement = title;
}
info.push(titleElement);
if (props.creator) {
if (creator) {
info.push(
<div
className="thumbnail-creator"
key="creator"
>
<a href={`/users/${props.creator}/`}>{props.creator}</a>
<a href={`/users/${creator}/`}>{creator}</a>
</div>
);
}
if (props.avatar && props.showAvatar) {
if (avatar && showAvatar) {
avatarElement = (
<a
className="creator-image"
href={`/users/${props.creator}/`}
href={`/users/${creator}/`}
>
<img
alt={props.creator}
src={props.avatar}
alt={creator}
src={avatar}
/>
</a>
);
}
return (
<div
className={classNames(
'thumbnail',
props.type,
props.className
type,
className
)}
>
{imgElement}
@ -151,19 +171,4 @@ Thumbnail.propTypes = {
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;

View file

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

View file

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

View file

@ -6,17 +6,30 @@ const Box = require('../box/box.jsx');
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
className="welcome"
moreHref="#"
moreProps={{
className: 'close',
title: 'Dismiss',
onClick: props.onDismiss
onClick: onDismiss
}}
moreTitle="x"
title={props.messages['welcome.welcomeToScratch']}
title={messages['welcome.welcomeToScratch']}
>
<div className="welcome-options">
<a
@ -24,9 +37,9 @@ const Welcome = props => (
className="welcome-option-button"
href="/starter-projects"
>
{props.messages['welcome.explore']}
{messages['welcome.explore']}
<img
alt={props.messages['welcome.exploreAlt']}
alt={messages['welcome.exploreAlt']}
src="/images/explore_starter_projects.svg"
/>
</a>
@ -35,9 +48,9 @@ const Welcome = props => (
className="welcome-option-button"
href="/community_guidelines"
>
{props.messages['welcome.community']}
{messages['welcome.community']}
<img
alt={props.messages['welcome.communityAlt']}
alt={messages['welcome.communityAlt']}
src="/images/learn_about_the_community.svg"
/>
</a>
@ -45,14 +58,14 @@ const Welcome = props => (
id="welcome.create"
className="welcome-option-button"
href={
shouldDisplayOnboarding(props.user, props.permissions) ?
shouldDisplayOnboarding(user, permissions) ?
'/projects/editor/' :
'/projects/editor/?tutorial=getStarted'
}
>
{props.messages['welcome.create']}
{messages['welcome.create']}
<img
alt={props.messages['welcome.createAlt']}
alt={messages['welcome.createAlt']}
src="/images/create_a_project.svg"
/>
</a>
@ -75,16 +88,4 @@ Welcome.propTypes = {
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;

View file

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

View file

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

View file

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

View file

@ -1,45 +1,13 @@
import React from 'react';
import {render} from '@testing-library/react';
import {IntlProvider} from 'react-intl';
import routes from '../../src/routes.json';
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);
import {generatedLocales} from '../generated/generated-locales';
const renderWithIntl = ui => ({
...render(
<IntlProvider
locale="en"
messages={generalLocales.en}
messages={generatedLocales.en}
>
{ui}
</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 */
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 sessionActions from '../../../src/redux/session.js';
import configureStore from 'redux-mock-store';
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');
@ -26,7 +26,9 @@ describe('BecomeAScratcherPage', () => {
}
});
const {container} = renderWithIntl(
<BecomeAScratcherPage />, {context: {store: NotLoggedInUserStore}}
<Provider store={NotLoggedInUserStore}>
<BecomeAScratcherPage />
</Provider>
);
expect(container.querySelector('div.not-available-outer')).toBeInTheDocument();
});
@ -47,9 +49,11 @@ describe('BecomeAScratcherPage', () => {
}
});
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', () => {
@ -69,9 +73,11 @@ describe('BecomeAScratcherPage', () => {
}
});
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', () => {
@ -91,8 +97,10 @@ describe('BecomeAScratcherPage', () => {
}
});
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 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', () => {
global.grecaptcha = {
@ -25,10 +25,8 @@ describe('Captcha test', () => {
const props = {
onCaptchaLoad: jest.fn()
};
const {container} = (<Captcha
{...props}
/>);
container.executeCaptcha();
const captchaInstance = render(<Captcha {...props} />, 'Captcha').instance();
captchaInstance.executeCaptcha();
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');
import configureStore from 'redux-mock-store';
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';
describe('Compose Comment test', () => {

View file

@ -1,3 +1,4 @@
/* eslint-disable max-len */
import React from 'react';
import {renderWithIntl} from '../../helpers/intl-helpers.jsx';
import DonateTopBanner from '../../../src/views/splash/donate/donate-banner';
@ -14,27 +15,26 @@ describe('DonateBannerTest', () => {
});
test('Testing 2024 EOY campaign message', () => {
global.Date.now = () => new Date(2024, 11, 16).getTime();
const {container} = renderWithIntl(
<DonateTopBanner />
);
const {container} = renderWithIntl(<DonateTopBanner />);
expect(container.querySelector('div.donate-banner')).toBeInTheDocument();
expect(container.querySelector('p.donate-text')).toBeInTheDocument();
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', () => {
// Date after Scratch week
// Date after Scratch week
global.Date.now = () => new Date(2025, 0, 10).getTime();
const {container} = renderWithIntl(
<DonateTopBanner />
);
const {container} = renderWithIntl(<DonateTopBanner />);
expect(container.querySelector('div.donate-banner')).toBeInTheDocument();
expect(container.querySelector('p.donate-text')).toBeInTheDocument();
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 {shallowWithIntl} = require('../../helpers/intl-helpers.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');
import {renderWithIntl} from '../../helpers/react-testing-library-wrapper.jsx';
const requestSuccessResponse = {
requestSucceeded: true,
@ -44,52 +41,55 @@ describe('EmailStep test', () => {
});
test('send correct props to formik', () => {
const intlWrapper = shallowWithIntl(<EmailStep
const {findByComponentName, instance} = renderWithIntl(<EmailStep
{...defaultProps()}
/>);
const emailStepWrapper = intlWrapper.dive();
expect(emailStepWrapper.props().initialValues.email).toBe('');
expect(emailStepWrapper.props().validateOnBlur).toBe(false);
expect(emailStepWrapper.props().validateOnChange).toBe(false);
expect(emailStepWrapper.props().validate).toBe(emailStepWrapper.instance().validateForm);
expect(emailStepWrapper.props().onSubmit).toBe(emailStepWrapper.instance().handleValidSubmit);
/>, 'EmailStep');
const formikComponent = findByComponentName('Formik');
const emailStepInstance = instance();
expect(formikComponent.memoizedProps.initialValues.email).toBe('');
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', () => {
const intlWrapper = shallowWithIntl(<EmailStep
const {findAllByComponentName} = renderWithIntl(<EmailStep
{...defaultProps()}
/>);
// Dive to get past the intl wrapper
const emailStepWrapper = intlWrapper.dive();
// Dive to get past the anonymous component.
const joinFlowWrapper = emailStepWrapper.dive().find(JoinFlowStep);
expect(joinFlowWrapper).toHaveLength(1);
expect(joinFlowWrapper.props().footerContent.props.id).toBe('registration.acceptTermsOfUse');
expect(joinFlowWrapper.props().headerImgSrc).toBe('/images/join-flow/email-header.png');
expect(joinFlowWrapper.props().innerClassName).toBe('join-flow-inner-email-step');
expect(joinFlowWrapper.props().nextButton).toBe('registration.createAccount');
expect(joinFlowWrapper.props().title).toBe('registration.emailStepTitle');
expect(joinFlowWrapper.props().titleClassName).toBe('join-flow-email-title');
expect(joinFlowWrapper.props().waiting).toBe(true);
/>, 'EmailStep');
const joinFlowSteps = findAllByComponentName('JoinFlowStep');
expect(joinFlowSteps).toHaveLength(1);
expect(joinFlowSteps[0].memoizedProps.footerContent.props.id).toBe('registration.acceptTermsOfUse');
expect(joinFlowSteps[0].memoizedProps.headerImgSrc).toBe('/images/join-flow/email-header.png');
expect(joinFlowSteps[0].memoizedProps.innerClassName).toBe('join-flow-inner-email-step');
expect(joinFlowSteps[0].memoizedProps.nextButton).toBe('Create Your Account');
expect(joinFlowSteps[0].memoizedProps.title).toBe('What\'s your email?');
expect(joinFlowSteps[0].memoizedProps.titleClassName).toBe('join-flow-email-title');
expect(joinFlowSteps[0].memoizedProps.waiting).toBe(true);
});
test('props sent to FormikInput for email', () => {
const intlWrapper = shallowWithIntl(<EmailStep
const {findByComponentName, findAllByComponentName, instance} = renderWithIntl(<EmailStep
{...defaultProps()}
/>);
// Dive to get past the intl wrapper
const emailStepWrapper = intlWrapper.dive();
// Dive to get past the anonymous component.
const joinFlowWrapper = emailStepWrapper.dive().find(JoinFlowStep);
expect(joinFlowWrapper).toHaveLength(1);
const emailInputWrapper = joinFlowWrapper.find(FormikInput).first();
expect(emailInputWrapper.props().id).toEqual('email');
expect(emailInputWrapper.props().error).toBeUndefined();
expect(emailInputWrapper.props().name).toEqual('email');
expect(emailInputWrapper.props().placeholder).toEqual('general.emailAddress');
expect(emailInputWrapper.props().validationClassName).toEqual('validation-full-width-input');
expect(emailInputWrapper.props().onSetRef).toEqual(emailStepWrapper.instance().handleSetEmailRef);
expect(emailInputWrapper.props().validate).toEqual(emailStepWrapper.instance().validateEmail);
/>, 'EmailStep');
const emailStepInstance = instance();
const joinFlowSteps = findAllByComponentName('JoinFlowStep');
expect(joinFlowSteps).toHaveLength(1);
const emailInput = findByComponentName('FormikInput');
expect(emailInput.memoizedProps.id).toEqual('email');
expect(emailInput.memoizedProps.error).toBeUndefined();
expect(emailInput.memoizedProps.name).toEqual('email');
expect(emailInput.memoizedProps.placeholder).toEqual('Email address');
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', () => {
@ -101,14 +101,11 @@ describe('EmailStep test', () => {
executeCaptcha: jest.fn()
};
const formData = {item1: 'thing', item2: 'otherthing'};
const intlWrapper = shallowWithIntl(
<EmailStep
{...defaultProps()}
/>);
const emailStepWrapper = intlWrapper.dive();
emailStepWrapper.instance().setCaptchaRef(captchaRef);
emailStepWrapper.instance().handleValidSubmit(formData, formikBag);
const emailStepInstance = renderWithIntl(<EmailStep
{...defaultProps()}
/>, 'EmailStep').instance();
emailStepInstance.setCaptchaRef(captchaRef);
emailStepInstance.handleValidSubmit(formData, formikBag);
expect(formikBag.setSubmitting).toHaveBeenCalledWith(false);
expect(captchaRef.executeCaptcha).toHaveBeenCalled();
@ -125,19 +122,16 @@ describe('EmailStep test', () => {
executeCaptcha: jest.fn()
};
const formData = {item1: 'thing', item2: 'otherthing'};
const intlWrapper = shallowWithIntl(
<EmailStep
{...defaultProps()}
{...props}
/>);
const emailStepInstance = renderWithIntl(<EmailStep
{...defaultProps()}
{...props}
/>, 'EmailStep').instance();
const emailStepWrapper = intlWrapper.dive();
// Call these to setup captcha.
emailStepWrapper.instance().setCaptchaRef(captchaRef); // to setup catpcha state
emailStepWrapper.instance().handleValidSubmit(formData, formikBag);
emailStepInstance.setCaptchaRef(captchaRef); // to setup catpcha state
emailStepInstance.handleValidSubmit(formData, formikBag);
const captchaToken = 'abcd';
emailStepWrapper.instance().handleCaptchaSolved(captchaToken);
emailStepInstance.handleCaptchaSolved(captchaToken);
// Make sure captchaSolved calls onNextStep with formData that has
// a captcha token and left everything else in the object in place.
expect(props.onNextStep).toHaveBeenCalledWith(
@ -152,7 +146,7 @@ describe('EmailStep test', () => {
test('Component logs analytics', () => {
const sendAnalyticsFn = jest.fn();
const onCaptchaError = jest.fn();
mountWithIntl(
renderWithIntl(
<EmailStep
sendAnalytics={sendAnalyticsFn}
onCaptchaError={onCaptchaError}
@ -161,33 +155,30 @@ describe('EmailStep test', () => {
});
test('validateEmail test email empty', () => {
const intlWrapper = shallowWithIntl(
<EmailStep
{...defaultProps()}
/>);
const emailStepWrapper = intlWrapper.dive();
const val = emailStepWrapper.instance().validateEmail('');
expect(val).toBe('general.required');
const emailStepInstance = renderWithIntl(<EmailStep
{...defaultProps()}
/>, 'EmailStep').instance();
const val = emailStepInstance.validateEmail('');
expect(val).toBe('Required');
});
test('validateEmail test email null', () => {
const intlWrapper = shallowWithIntl(
<EmailStep
{...defaultProps()}
/>);
const emailStepWrapper = intlWrapper.dive();
const val = emailStepWrapper.instance().validateEmail(null);
expect(val).toBe('general.required');
const emailStepInstance = renderWithIntl(<EmailStep
{...defaultProps()}
/>, 'EmailStep').instance();
const val = emailStepInstance.validateEmail(null);
expect(val).toBe('Required');
});
test('validateEmail test email undefined', () => {
const intlWrapper = shallowWithIntl(
<EmailStep
{...defaultProps()}
/>);
const emailStepWrapper = intlWrapper.dive();
const val = emailStepWrapper.instance().validateEmail();
expect(val).toBe('general.required');
const emailStepInstance = renderWithIntl(<EmailStep
{...defaultProps()}
/>, 'EmailStep').instance();
const val = emailStepInstance.validateEmail();
expect(val).toBe('Required');
});
});
@ -198,12 +189,9 @@ describe('validateEmailRemotelyWithCache test with successful requests', () => {
});
test('validateEmailRemotelyWithCache calls validate.validateEmailRemotely', done => {
const intlWrapper = shallowWithIntl(
<EmailStep />
);
const instance = intlWrapper.dive().instance();
const emailStepInstance = renderWithIntl(<EmailStep />, 'EmailStep').instance();
instance.validateEmailRemotelyWithCache('some-email@some-domain.com')
emailStepInstance.validateEmailRemotelyWithCache('some-email@some-domain.com')
.then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalled();
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 => {
const intlWrapper = shallowWithIntl(
<EmailStep />
);
const instance = intlWrapper.dive().instance();
const emailStepInstance = renderWithIntl(<EmailStep />, 'EmailStep').instance();
instance.validateEmailRemotelyWithCache('some-email@some-domain.com')
emailStepInstance.validateEmailRemotelyWithCache('some-email@some-domain.com')
.then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(1);
expect(response.valid).toBe(false);
@ -226,7 +211,7 @@ describe('validateEmailRemotelyWithCache test with successful requests', () => {
})
.then(() => {
// make the same request a second time
instance.validateEmailRemotelyWithCache('different-email@some-domain.org')
emailStepInstance.validateEmailRemotelyWithCache('different-email@some-domain.org')
.then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(2);
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 => {
const intlWrapper = shallowWithIntl(
<EmailStep />
);
const instance = intlWrapper.dive().instance();
const emailStepInstance = renderWithIntl(<EmailStep />, 'EmailStep').instance();
instance.validateEmailRemotelyWithCache('some-email@some-domain.com')
emailStepInstance.validateEmailRemotelyWithCache('some-email@some-domain.com')
.then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(1);
expect(response.valid).toBe(false);
@ -250,7 +232,7 @@ describe('validateEmailRemotelyWithCache test with successful requests', () => {
})
.then(() => {
// make the same request a second time
instance.validateEmailRemotelyWithCache('some-email@some-domain.com')
emailStepInstance.validateEmailRemotelyWithCache('some-email@some-domain.com')
.then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(1);
expect(response.valid).toBe(false);
@ -275,11 +257,9 @@ describe('validateEmailRemotelyWithCache test with failing requests', () => {
});
test('validateEmailRemotelyWithCache calls validate.validateEmailRemotely', done => {
const wrapper = shallowWithIntl(
<EmailStep />);
const instance = wrapper.dive().instance();
const emailStepInstance = renderWithIntl(<EmailStep />, 'EmailStep').instance();
instance.validateEmailRemotelyWithCache('some-email@some-domain.com')
emailStepInstance.validateEmailRemotelyWithCache('some-email@some-domain.com')
.then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalled();
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 => {
const wrapper = shallowWithIntl(
<EmailStep />
);
const instance = wrapper.dive().instance();
const emailStepInstance = renderWithIntl(<EmailStep />, 'EmailStep').instance();
instance.validateEmailRemotelyWithCache('some-email@some-domain.com')
emailStepInstance.validateEmailRemotelyWithCache('some-email@some-domain.com')
.then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(1);
expect(response.valid).toBe(false);
@ -302,7 +279,7 @@ describe('validateEmailRemotelyWithCache test with failing requests', () => {
})
.then(() => {
// make the same request a second time
instance.validateEmailRemotelyWithCache('different-email@some-domain.org')
emailStepInstance.validateEmailRemotelyWithCache('different-email@some-domain.org')
.then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(2);
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 => {
const wrapper = shallowWithIntl(
<EmailStep />
);
const instance = wrapper.dive().instance();
const emailStepInstance = renderWithIntl(<EmailStep />, 'EmailStep').instance();
instance.validateEmailRemotelyWithCache('some-email@some-domain.com')
emailStepInstance.validateEmailRemotelyWithCache('some-email@some-domain.com')
.then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(1);
expect(response.valid).toBe(false);
@ -326,7 +300,7 @@ describe('validateEmailRemotelyWithCache test with failing requests', () => {
})
.then(() => {
// make the same request a second time
instance.validateEmailRemotelyWithCache('some-email@some-domain.com')
emailStepInstance.validateEmailRemotelyWithCache('some-email@some-domain.com')
.then(response => {
expect(mockedValidateEmailRemotely).toHaveBeenCalledTimes(2);
expect(response.valid).toBe(false);

View file

@ -1,24 +1,41 @@
import React from 'react';
const {mountWithIntl} = require('../../helpers/intl-helpers.jsx');
import CrashMessageComponent from '../../../src/components/crashmessage/crashmessage.jsx';
import React, {useEffect, useRef, useState} from 'react';
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', () => {
test('ErrorBoundary shows children before error and CrashMessageComponent after', () => {
const child = <ChildComponent />;
const wrapper = mountWithIntl(<ErrorBoundary>{child}</ErrorBoundary>);
const childWrapper = wrapper.childAt(0);
const {findByComponentName} = renderWithIntl(
<ErrorBoundary>{child}</ErrorBoundary>,
'ErrorBoundary'
);
expect(wrapper.containsMatchingElement(child)).toBeTruthy();
expect(wrapper.containsMatchingElement(<CrashMessageComponent />)).toBeFalsy();
expect(findByComponentName('ChildComponent')).toBeTruthy();
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(wrapper.containsMatchingElement(<CrashMessageComponent />)).toBeTruthy();
expect(findByComponentName('ChildComponent')).toBeFalsy();
expect(findByComponentName('CrashMessage')).toBeTruthy();
});
});

View file

@ -1,13 +1,14 @@
import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import ExtensionRequirements from '../../../src/components/extension-landing/extension-requirements';
import {renderWithIntl} from '../../helpers/react-testing-library-wrapper.jsx';
describe('ExtensionRequirements', () => {
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(
['Windows 10 version 1709+', 'macOS 10.15+', 'ChromeOS', 'Android 6.0+', 'Bluetooth', 'Scratch Link']
@ -15,15 +16,15 @@ describe('ExtensionRequirements', () => {
});
test('hides requirements', () => {
const component = mountWithIntl(<ExtensionRequirements
const {container} = renderWithIntl(<ExtensionRequirements
hideWindows
hideMac
hideChromeOS
hideAndroid
hideBluetooth
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 {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import FeedbackForm from '../../../src/components/modal/mute/feedback-form';
import {renderWithIntl} from '../../helpers/react-testing-library-wrapper';
describe('FeedbackFormTest', () => {
test('Feedback form empty feedback invalid', () => {
const submitFn = jest.fn();
const message = 'too short';
const component = mountWithIntl(
const feedbackFormInstance = renderWithIntl(
<FeedbackForm
emptyErrorMessage={message}
onSubmit={submitFn}
/>
);
expect(component.find('FeedbackForm').instance()
.validateFeedback('')
).toBe(message);
/>,
'FeedbackForm'
).instance();
expect(feedbackFormInstance.validateFeedback('')).toBe(message);
});
test('Feedback form shorter than minLength invalid', () => {
const submitFn = jest.fn();
const message = 'too short';
const min = 7;
const component = mountWithIntl(
const feedbackFormInstance = renderWithIntl(
<FeedbackForm
emptyErrorMessage={message}
minLength={min}
onSubmit={submitFn}
/>
);
expect(component.find('FeedbackForm').instance()
.validateFeedback('123456')
).toBe(message);
/>,
'FeedbackForm'
).instance();
expect(feedbackFormInstance.validateFeedback('123456')).toBe(message);
});
test('Feedback form greater than or equal to minLength invalid', () => {
const submitFn = jest.fn();
const message = 'too short';
const min = 7;
const component = mountWithIntl(
const feedbackFormInstance = renderWithIntl(
<FeedbackForm
emptyErrorMessage={message}
minLength={min}
onSubmit={submitFn}
/>
);
expect(component.find('FeedbackForm').instance()
.validateFeedback('1234567')
).toBeNull();
/>,
'FeedbackForm'
).instance();
expect(feedbackFormInstance.validateFeedback('1234567')).toBeNull();
});
});

View file

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

View file

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

View file

@ -1,6 +1,7 @@
import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import React, {act} from 'react';
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', () => {
// mock window.addEventListener
@ -13,125 +14,132 @@ describe('InfoButton', () => {
/* eslint-enable no-undef */
test('Info button defaults to not visible', () => {
const component = mountWithIntl(
const {container} = renderWithIntl(
<InfoButton
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 => {
const component = mountWithIntl(
test('mouseOver on info button makes info message visible', async () => {
const {container} = renderWithIntl(
<InfoButton
message="Here is some info about something!"
/>
);
// mouseOver info button
component.find('div.info-button').simulate('mouseOver');
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
component.update();
expect(component.find('div.info-button-message').exists()).toEqual(true);
done();
}, 500);
await act(() => {
fireEvent.mouseOver(container.querySelector(('div.info-button')));
});
expect(container.querySelector('div.info-button-message')).toBeTruthy();
});
test('clicking on info button makes info message visible', () => {
const component = mountWithIntl(
test('clicking on info button makes info message visible', async () => {
const {container, instance} = renderWithIntl(
<InfoButton
message="Here is some info about something!"
/>
/>,
'InfoButton'
);
const buttonRef = component.instance().buttonRef;
const buttonRef = instance().buttonRef;
// click on info button
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);
await act(() => {
mockedAddEventListener.mousedown({target: buttonRef});
});
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 => {
const component = mountWithIntl(
test('clicking on info button, then mousing out makes info message still appear', async () => {
const {container, instance} = renderWithIntl(
<InfoButton
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
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();
// mouseLeave from info button
component.find('div.info-button').simulate('mouseLeave');
setTimeout(() => { // necessary because mouseover uses debounce
component.update();
expect(component.find('div.info-button-message').exists()).toEqual(true);
done();
}, 500);
await act(() => {
fireEvent.mouseLeave(container.querySelector(('div.info-button')));
});
expect(container.querySelector('div.info-button-message')).toBeTruthy();
});
test('clicking on info button, then clicking on it again makes info message go away', () => {
const component = mountWithIntl(
test('clicking on info button, then clicking on it again makes info message go away', async () => {
const {container, instance} = renderWithIntl(
<InfoButton
message="Here is some info about something!"
/>
/>,
'InfoButton'
);
const buttonRef = component.instance().buttonRef;
const buttonRef = instance().buttonRef;
// click on info button
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);
await act(() => {
mockedAddEventListener.mousedown({target: buttonRef});
});
expect(container.querySelector('div.info-button')).toBeTruthy();
expect(container.querySelector('div.info-button-message')).toBeTruthy();
// click on info button again
mockedAddEventListener.mousedown({target: buttonRef});
component.update();
expect(component.find('div.info-button-message').exists()).toEqual(false);
await act(() => {
mockedAddEventListener.mousedown({target: buttonRef});
});
expect(container.querySelector('div.info-button-message')).toBeFalsy();
});
test('clicking on info button, then clicking somewhere else', () => {
const component = mountWithIntl(
test('clicking on info button, then clicking somewhere else', async () => {
const {container, instance} = renderWithIntl(
<InfoButton
message="Here is some info about something!"
/>
/>,
'InfoButton'
);
const buttonRef = component.instance().buttonRef;
const buttonRef = instance().buttonRef;
// click on info button
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);
await act(() => {
mockedAddEventListener.mousedown({target: buttonRef});
});
expect(container.querySelector('div.info-button')).toBeTruthy();
expect(container.querySelector('div.info-button-message')).toBeTruthy();
// click on some other target
mockedAddEventListener.mousedown({target: null});
component.update();
expect(component.find('div.info-button-message').exists()).toEqual(false);
await act(() => {
mockedAddEventListener.mousedown({target: null});
});
expect(container.querySelector('div.info-button-message')).toBeFalsy();
});
test('after message is visible, mouseLeave makes it vanish', () => {
const component = mountWithIntl(
test('after message is visible, mouseLeave makes it vanish', async () => {
const {container} = renderWithIntl(
<InfoButton
message="Here is some info about something!"
/>
/>,
'InfoButton'
);
// mouseOver info button
component.find('div.info-button').simulate('mouseOver');
component.update();
expect(component.find('div.info-button-message').exists()).toEqual(true);
await act(() => {
fireEvent.mouseOver(container.querySelector(('div.info-button')));
});
expect(container.querySelector('div.info-button-message')).toBeTruthy();
// mouseLeave away from info button
component.find('div.info-button').simulate('mouseLeave');
component.update();
expect(component.find('div.info-button-message').exists()).toEqual(false);
await act(() => {
fireEvent.mouseLeave(container.querySelector(('div.info-button')));
});
expect(container.querySelector('div.info-button-message')).toBeFalsy();
});
});

View file

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

View file

@ -1,10 +1,9 @@
import React from 'react';
const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx');
import React, {act} from 'react';
const defaults = require('lodash.defaultsdeep');
import configureStore from 'redux-mock-store';
import JoinFlow from '../../../src/components/join-flow/join-flow';
import Progression from '../../../src/components/progression/progression.jsx';
import RegistrationErrorStep from '../../../src/components/join-flow/registration-error-step';
import {renderWithIntl} from '../../helpers/react-testing-library-wrapper.jsx';
import {Provider} from 'react-redux';
describe('JoinFlow', () => {
const mockStore = configureStore();
@ -46,160 +45,200 @@ describe('JoinFlow', () => {
});
const getJoinFlowWrapper = props => {
const wrapper = shallowWithIntl(
<JoinFlow
{...props}
/>
, {context: {store}}
const wrapper = renderWithIntl(
<Provider store={store}>
<JoinFlow
{...props}
/>
</Provider>,
'JoinFlow'
);
return wrapper
.dive() // unwrap redux connect(injectIntl(JoinFlow))
.dive(); // unwrap injectIntl(JoinFlow)
return wrapper;
};
test('handleCaptchaError gives state with captcha message', () => {
test('handleCaptchaError gives state with captcha message', async () => {
const joinFlowInstance = getJoinFlowWrapper().instance();
joinFlowInstance.setState({});
joinFlowInstance.handleCaptchaError();
await act(() => {
joinFlowInstance.setState({});
});
await act(() => {
joinFlowInstance.handleCaptchaError();
});
expect(joinFlowInstance.state.registrationError).toEqual({
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();
global.window.dataLayer = {push: jest.fn()};
global.window.GA_ID = '1234';
joinFlowInstance.sendAnalytics('page-path');
await act(() => {
joinFlowInstance.sendAnalytics('page-path');
});
expect(global.window.dataLayer.push).toHaveBeenCalledWith({
event: 'join_flow',
joinFlowStep: 'page-path'
});
});
test('handleAdvanceStep', () => {
test('handleAdvanceStep', async () => {
const joinFlowInstance = getJoinFlowWrapper().instance();
joinFlowInstance.setState({formData: {username: 'ScratchCat123'}, step: 2});
joinFlowInstance.handleAdvanceStep({email: 'scratchcat123@scratch.mit.edu'});
await act(() => {
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.email).toBe('scratchcat123@scratch.mit.edu');
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();
joinFlowWrapper.instance().setState({registrationError: 'halp there is a errors!!'});
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep);
const progressionWrapper = joinFlowWrapper.find(Progression);
await act(() => {
joinFlowWrapper.instance().setState({registrationError: 'halp there is a errors!!'});
});
joinFlowWrapper.rerenderWithIntl();
const registrationErrorWrapper = joinFlowWrapper.findAllByComponentName('RegistrationErrorStep');
const progressionWrapper = joinFlowWrapper.findAllByComponentName('Progression');
expect(registrationErrorWrapper).toHaveLength(1);
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();
joinFlowWrapper.instance().setState({registrationError: null});
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep);
const progressionWrapper = joinFlowWrapper.find(Progression);
await act(() => {
joinFlowWrapper.instance().setState({registrationError: null});
});
joinFlowWrapper.rerenderWithIntl();
const registrationErrorWrapper = joinFlowWrapper.findAllByComponentName('RegistrationErrorStep');
const progressionWrapper = joinFlowWrapper.findAllByComponentName('Progression');
expect(registrationErrorWrapper).toHaveLength(0);
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();
joinFlowWrapper.instance().setState({registrationError: ''});
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep);
const progressionWrapper = joinFlowWrapper.find(Progression);
await act(() => {
joinFlowWrapper.instance().setState({registrationError: ''});
});
joinFlowWrapper.rerenderWithIntl();
const registrationErrorWrapper = joinFlowWrapper.findAllByComponentName('RegistrationErrorStep');
const progressionWrapper = joinFlowWrapper.findAllByComponentName('Progression');
expect(registrationErrorWrapper).toHaveLength(0);
expect(progressionWrapper).toHaveLength(1);
});
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();
joinFlowWrapper.instance().setState({
numAttempts: 0,
registrationError: {
errorAllowsTryAgain: true,
errorMsg: 'halp there is a errors!!'
}
await act(() => {
joinFlowWrapper.instance().setState({
numAttempts: 0,
registrationError: {
errorAllowsTryAgain: true,
errorMsg: 'halp there is a errors!!'
}
});
});
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep);
expect(registrationErrorWrapper.first().props().canTryAgain).toEqual(true);
joinFlowWrapper.rerenderWithIntl();
const registrationErrorWrapper = joinFlowWrapper.findAllByComponentName('RegistrationErrorStep');
expect(registrationErrorWrapper[0].stateNode.props.canTryAgain).toEqual(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();
joinFlowWrapper.instance().setState({
numAttempts: 1,
registrationError: {
errorAllowsTryAgain: true,
errorMsg: 'halp there is a errors!!'
}
await act(() => {
joinFlowWrapper.instance().setState({
numAttempts: 1,
registrationError: {
errorAllowsTryAgain: true,
errorMsg: 'halp there is a errors!!'
}
});
});
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep);
expect(registrationErrorWrapper.first().props().canTryAgain).toEqual(true);
joinFlowWrapper.rerenderWithIntl();
const registrationErrorWrapper = joinFlowWrapper.findAllByComponentName('RegistrationErrorStep');
expect(registrationErrorWrapper[0].stateNode.props.canTryAgain).toEqual(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();
joinFlowWrapper.instance().setState({
numAttempts: 2,
registrationError: {
errorAllowsTryAgain: true,
errorMsg: 'halp there is a errors!!'
}
await act(() => {
joinFlowWrapper.instance().setState({
numAttempts: 2,
registrationError: {
errorAllowsTryAgain: true,
errorMsg: 'halp there is a errors!!'
}
});
});
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep);
expect(registrationErrorWrapper.first().props().canTryAgain).toEqual(false);
joinFlowWrapper.rerenderWithIntl();
const registrationErrorWrapper = joinFlowWrapper.findAllByComponentName('RegistrationErrorStep');
expect(registrationErrorWrapper[0].memoizedProps.canTryAgain).toEqual(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();
joinFlowWrapper.instance().setState({
numAttempts: 0,
registrationError: {
errorAllowsTryAgain: false,
errorMsg: 'halp there is a errors!!'
}
await act(() => {
joinFlowWrapper.instance().setState({
numAttempts: 0,
registrationError: {
errorAllowsTryAgain: false,
errorMsg: 'halp there is a errors!!'
}
});
});
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep);
expect(registrationErrorWrapper.first().props().canTryAgain).toEqual(false);
joinFlowWrapper.rerenderWithIntl();
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();
Object.keys(joinFlowInstance.state).forEach(key => {
joinFlowInstance.setState({[key]: 'Different than the initial value'});
await act(() => {
Object.keys(joinFlowInstance.state).forEach(key => {
joinFlowInstance.setState({[key]: 'Different than the initial value'});
});
joinFlowInstance.resetState();
});
joinFlowInstance.resetState();
Object.keys(joinFlowInstance.state).forEach(key => {
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 stateSnapshot = {};
Object.keys(joinFlowInstance.state).forEach(key => {
stateSnapshot[key] = joinFlowInstance.state[key];
});
joinFlowInstance.resetState();
await act(() => {
joinFlowInstance.resetState();
});
Object.keys(joinFlowInstance.state).forEach(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();
joinFlowInstance.setState({
formData: defaults({}, {username: 'abcdef'})
await act(() => {
joinFlowInstance.setState({
formData: defaults({}, {username: 'abcdef'})
});
});
const formDataReference = joinFlowInstance.state.formData;
joinFlowInstance.resetState();
await act(() => {
joinFlowInstance.resetState();
});
expect(formDataReference).not.toBe(joinFlowInstance.state.formData);
expect(formDataReference).not.toEqual(joinFlowInstance.state.formData);
});
@ -245,7 +284,7 @@ describe('JoinFlow', () => {
const errorsFromResponse =
joinFlowInstance.getErrorsFromResponse(null, responseBodyMultipleErrs, {statusCode: 200});
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."');
});
@ -254,7 +293,7 @@ describe('JoinFlow', () => {
const errorsFromResponse =
joinFlowInstance.getErrorsFromResponse(null, responseBodySingleErr, {statusCode: 200});
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', () => {
@ -291,14 +330,18 @@ describe('JoinFlow', () => {
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 = {
refreshSessionWithRetry: () => (new Promise(resolve => { // eslint-disable-line no-undef
resolve();
}))
};
const joinFlowInstance = getJoinFlowWrapper(props).instance();
joinFlowInstance.handleRegistrationResponse(null, responseBodySuccess, {statusCode: 200});
const joinFlowWrapper = getJoinFlowWrapper(props);
const joinFlowInstance = joinFlowWrapper.instance();
await act(() => {
joinFlowInstance.handleRegistrationResponse(null, responseBodySuccess, {statusCode: 200});
});
joinFlowWrapper.rerenderWithIntl();
process.nextTick(
() => {
expect(joinFlowInstance.state.registrationError).toEqual(null);
@ -308,20 +351,25 @@ describe('JoinFlow', () => {
);
});
test('handleRegistrationResponse when passed body with preset server error', () => {
const joinFlowInstance = getJoinFlowWrapper().instance();
joinFlowInstance.handleRegistrationResponse(null, responseBodySingleErr, {statusCode: 200});
test('handleRegistrationResponse when passed body with preset server error', async () => {
const joinFlowWrapper = getJoinFlowWrapper();
const joinFlowInstance = joinFlowWrapper.instance();
await act(() => {
joinFlowInstance.handleRegistrationResponse(null, responseBodySingleErr, {statusCode: 200});
});
joinFlowWrapper.rerenderWithIntl();
expect(joinFlowInstance.state.registrationError).toEqual({
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 = {
refreshSessionWithRetry: jest.fn()
};
const joinFlowInstance = getJoinFlowWrapper(props).instance();
const joinFlowWrapper = getJoinFlowWrapper(props);
const joinFlowInstance = joinFlowWrapper.instance();
const responseErr = null;
const responseBody = [
{
@ -332,7 +380,10 @@ describe('JoinFlow', () => {
const responseObj = {
statusCode: 200
};
joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj);
await act(() => {
joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj);
});
joinFlowWrapper.rerenderWithIntl();
expect(joinFlowInstance.props.refreshSessionWithRetry).not.toHaveBeenCalled();
expect(joinFlowInstance.state.registrationError).toEqual({
errorAllowsTryAgain: false,
@ -340,21 +391,26 @@ describe('JoinFlow', () => {
});
});
test('handleRegistrationResponse when passed body with unfamiliar server error', () => {
const joinFlowInstance = getJoinFlowWrapper().instance();
joinFlowInstance.handleRegistrationResponse(null, responseBodyMultipleErrs, {statusCode: 200});
test('handleRegistrationResponse when passed body with unfamiliar server error', async () => {
const joinFlowWrapper = getJoinFlowWrapper();
const joinFlowInstance = joinFlowWrapper.instance();
await act(() => {
joinFlowInstance.handleRegistrationResponse(null, responseBodyMultipleErrs, {statusCode: 200});
});
joinFlowWrapper.rerenderWithIntl();
expect(joinFlowInstance.state.registrationError).toEqual({
errorAllowsTryAgain: false,
errorMsg: 'registration.problemsAre: "username: This field is required.; ' +
errorMsg: 'The problems are:: "username: This field is required.; ' +
'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 = {
refreshSessionWithRetry: jest.fn()
};
const joinFlowInstance = getJoinFlowWrapper(props).instance();
const joinFlowWrapper = getJoinFlowWrapper(props);
const joinFlowInstance = joinFlowWrapper.instance();
const responseErr = null;
const responseBody = [
{
@ -364,7 +420,10 @@ describe('JoinFlow', () => {
const responseObj = {
statusCode: 200
};
joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj);
await act(() => {
joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj);
});
joinFlowWrapper.rerenderWithIntl();
expect(joinFlowInstance.props.refreshSessionWithRetry).not.toHaveBeenCalled();
expect(joinFlowInstance.state.registrationError).toEqual({
errorAllowsTryAgain: false,
@ -372,29 +431,41 @@ describe('JoinFlow', () => {
});
});
test('handleRegistrationResponse when passed non null outgoing request error', () => {
const joinFlowInstance = getJoinFlowWrapper().instance();
joinFlowInstance.handleRegistrationResponse({}, responseBodyMultipleErrs, {statusCode: 200});
test('handleRegistrationResponse when passed non null outgoing request error', async () => {
const joinFlowWrapper = getJoinFlowWrapper();
const joinFlowInstance = joinFlowWrapper.instance();
await act(() => {
joinFlowInstance.handleRegistrationResponse({}, responseBodyMultipleErrs, {statusCode: 200});
});
joinFlowWrapper.rerenderWithIntl();
expect(joinFlowInstance.state.registrationError).toEqual({
errorAllowsTryAgain: true
});
});
test('handleRegistrationResponse when passed status 400', () => {
test('handleRegistrationResponse when passed status 400', async () => {
const props = {
refreshSessionWithRetry: jest.fn()
};
const joinFlowInstance = getJoinFlowWrapper(props).instance();
joinFlowInstance.handleRegistrationResponse({}, responseBodyMultipleErrs, {statusCode: 400});
const joinFlowWrapper = getJoinFlowWrapper(props);
const joinFlowInstance = joinFlowWrapper.instance();
await act(() => {
joinFlowInstance.handleRegistrationResponse({}, responseBodyMultipleErrs, {statusCode: 400});
});
joinFlowWrapper.rerenderWithIntl();
expect(joinFlowInstance.props.refreshSessionWithRetry).not.toHaveBeenCalled();
expect(joinFlowInstance.state.registrationError).toEqual({
errorAllowsTryAgain: true
});
});
test('handleRegistrationResponse when passed status 500', () => {
const joinFlowInstance = getJoinFlowWrapper().instance();
joinFlowInstance.handleRegistrationResponse(null, responseBodyMultipleErrs, {statusCode: 500});
test('handleRegistrationResponse when passed status 500', async () => {
const joinFlowWrapper = getJoinFlowWrapper();
const joinFlowInstance = joinFlowWrapper.instance();
await act(() => {
joinFlowInstance.handleRegistrationResponse(null, responseBodyMultipleErrs, {statusCode: 500});
});
joinFlowWrapper.rerenderWithIntl();
expect(joinFlowInstance.state.registrationError).toEqual({
errorAllowsTryAgain: true
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,32 +1,35 @@
import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
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', () => {
test('calls callback when OS is selected', () => {
const onSetOs = jest.fn();
const component = mountWithIntl(<OSChooser handleSetOS={onSetOs} />);
const {container} = renderWithIntl(<OSChooser handleSetOS={onSetOs} />, 'OSChooser');
component.find('button').last()
.simulate('click');
const buttons = container.querySelectorAll('button');
fireEvent.click(buttons[buttons.length - 1]);
expect(onSetOs).toHaveBeenCalledWith('Android');
});
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', () => {
const component = mountWithIntl(<OSChooser
const {container} = renderWithIntl(<OSChooser
hideWindows
hideMac
hideChromeOS
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 {shallowWithIntl} = require('../../helpers/intl-helpers.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', () => {
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', () => {
const component = shallowWithIntl(
<Page />
const {findAllByComponentName} = renderWithIntl(
<Provider store={store}>
<Page />
</Provider>,
'Page'
);
expect(component.find('#donor')).toHaveLength(0);
expect(findAllByComponentName('DonorRecognition')).toHaveLength(0);
});
test('Show donor recognition', () => {
const component = shallowWithIntl(
<Page
showDonorRecognition
/>
const {findAllByComponentName} = renderWithIntl(
<Provider store={store}>
<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 {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 {renderWithIntl} from '../../helpers/react-testing-library-wrapper';
describe('RegistrationErrorStep', () => {
const onSubmit = jest.fn();
const getRegistrationErrorStepWrapper = props => {
const wrapper = shallowWithIntl(
const wrapper = renderWithIntl(
<RegistrationErrorStep
sendAnalytics={jest.fn()}
{...props}
/>
/>,
'RegistrationErrorStep'
);
return wrapper
.dive(); // unwrap injectIntl()
return wrapper;
};
test('registrationError has JoinFlowStep', () => {
@ -22,7 +21,10 @@ describe('RegistrationErrorStep', () => {
canTryAgain: true,
onSubmit: onSubmit
};
const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep);
const joinFlowStepWrapper =
getRegistrationErrorStepWrapper(props).findAllByComponentName(
'JoinFlowStep'
);
expect(joinFlowStepWrapper).toHaveLength(1);
});
@ -32,11 +34,11 @@ describe('RegistrationErrorStep', () => {
errorMsg: 'halp there is a errors!!',
onSubmit: onSubmit
};
const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep);
const joinFlowStepInstance = joinFlowStepWrapper.dive();
const errMsgElement = joinFlowStepInstance.find('.registration-error-msg');
expect(errMsgElement).toHaveLength(1);
expect(errMsgElement.text()).toEqual('halp there is a errors!!');
const errMsgElement = getRegistrationErrorStepWrapper(
props
).container.querySelector('.registration-error-msg');
expect(errMsgElement).toBeTruthy();
expect(errMsgElement.textContent).toEqual('halp there is a errors!!');
});
test('when errorMsg is null, registrationError does not show it', () => {
@ -45,10 +47,10 @@ describe('RegistrationErrorStep', () => {
errorMsg: null,
onSubmit: onSubmit
};
const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep);
const joinFlowStepInstance = joinFlowStepWrapper.dive();
const errMsgElement = joinFlowStepInstance.find('.registration-error-msg');
expect(errMsgElement).toHaveLength(0);
const errMsgElement = getRegistrationErrorStepWrapper(
props
).container.querySelector('.registration-error-msg');
expect(errMsgElement).toBeFalsy();
});
test('when no errorMsg provided, registrationError does not show it', () => {
@ -56,18 +58,15 @@ describe('RegistrationErrorStep', () => {
canTryAgain: true,
onSubmit: onSubmit
};
const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep);
const joinFlowStepInstance = joinFlowStepWrapper.dive();
const errMsgElement = joinFlowStepInstance.find('.registration-error-msg');
expect(errMsgElement).toHaveLength(0);
const errMsgElement = getRegistrationErrorStepWrapper(
props
).container.querySelector('.registration-error-msg');
expect(errMsgElement).toBeFalsy();
});
test('logs to analytics', () => {
const analyticsFn = jest.fn();
mountWithIntl(
<RegistrationErrorStep
sendAnalytics={analyticsFn}
/>);
renderWithIntl(<RegistrationErrorStep sendAnalytics={analyticsFn} />);
expect(analyticsFn).toHaveBeenCalledWith('join-error');
});
test('when canTryAgain is true, show tryAgain message', () => {
@ -76,9 +75,12 @@ describe('RegistrationErrorStep', () => {
errorMsg: 'halp there is a errors!!',
onSubmit: onSubmit
};
const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep);
const joinFlowStepWrapper =
getRegistrationErrorStepWrapper(props).findAllByComponentName(
'JoinFlowStep'
);
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', () => {
@ -87,9 +89,12 @@ describe('RegistrationErrorStep', () => {
errorMsg: 'halp there is a errors!!',
onSubmit: onSubmit
};
const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep);
const joinFlowStepWrapper =
getRegistrationErrorStepWrapper(props).findAllByComponentName(
'JoinFlowStep'
);
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', () => {
@ -97,9 +102,12 @@ describe('RegistrationErrorStep', () => {
errorMsg: 'halp there is a errors!!',
onSubmit: onSubmit
};
const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep);
const joinFlowStepWrapper =
getRegistrationErrorStepWrapper(props).findAllByComponentName(
'JoinFlowStep'
);
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', () => {
@ -108,8 +116,11 @@ describe('RegistrationErrorStep', () => {
errorMsg: 'halp there is a errors!!',
onSubmit: onSubmit
};
const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep);
joinFlowStepWrapper.props().onSubmit(new Event('event')); // eslint-disable-line no-undef
const joinFlowStepWrapper =
getRegistrationErrorStepWrapper(props).findByComponentName(
'JoinFlowStep'
);
joinFlowStepWrapper.memoizedProps.onSubmit(new Event('event')); // eslint-disable-line no-undef
expect(onSubmit).toHaveBeenCalled();
});
});

View file

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

View file

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

View file

@ -1,6 +1,4 @@
const React = require('react');
const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx');
const {mountWithIntl} = require('../../helpers/intl-helpers.jsx');
const requestSuccessResponse = {
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
const UsernameStep = require('../../../src/components/join-flow/username-step.jsx');
const {renderWithIntl} = require('../../helpers/react-testing-library-wrapper.jsx');
describe('UsernameStep tests', () => {
@ -44,25 +43,27 @@ describe('UsernameStep tests', () => {
});
test('send correct props to formik', () => {
const wrapper = shallowWithIntl(<UsernameStep
const {instance, findByComponentName} = renderWithIntl(<UsernameStep
{...defaultProps()}
/>);
const formikWrapper = wrapper.dive();
expect(formikWrapper.props().initialValues.username).toBe('');
expect(formikWrapper.props().initialValues.password).toBe('');
expect(formikWrapper.props().initialValues.passwordConfirm).toBe('');
expect(formikWrapper.props().initialValues.showPassword).toBe(true);
expect(formikWrapper.props().validateOnBlur).toBe(false);
expect(formikWrapper.props().validateOnChange).toBe(false);
expect(formikWrapper.props().validate).toBe(formikWrapper.instance().validateForm);
expect(formikWrapper.props().onSubmit).toBe(formikWrapper.instance().handleValidSubmit);
/>,
'UsernameStep');
const formikComponent = findByComponentName('Formik');
const usernameStepInstance = instance();
expect(formikComponent.memoizedProps.initialValues.username).toBe('');
expect(formikComponent.memoizedProps.initialValues.password).toBe('');
expect(formikComponent.memoizedProps.initialValues.passwordConfirm).toBe('');
expect(formikComponent.memoizedProps.initialValues.showPassword).toBe(true);
expect(formikComponent.memoizedProps.validateOnBlur).toBe(false);
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', () => {
const sendAnalyticsFn = jest.fn();
global.window.history.pushState({}, '', '/join');
mountWithIntl(
renderWithIntl(
<UsernameStep
sendAnalytics={sendAnalyticsFn}
/>);
@ -73,7 +74,7 @@ describe('UsernameStep tests', () => {
// Make sure '/join' is NOT in the path
global.window.history.pushState({}, '', '/');
const sendAnalyticsFn = jest.fn();
mountWithIntl(
renderWithIntl(
<UsernameStep
sendAnalytics={sendAnalyticsFn}
/>);
@ -86,13 +87,14 @@ describe('UsernameStep tests', () => {
};
const formData = {item1: 'thing', item2: 'otherthing'};
const mockedOnNextStep = jest.fn();
const wrapper = shallowWithIntl(
const wrapper = renderWithIntl(
<UsernameStep
{...defaultProps()}
onNextStep={mockedOnNextStep}
/>
/>,
'UsernameStep'
);
const instance = wrapper.dive().instance();
const instance = wrapper.instance();
instance.handleValidSubmit(formData, formikBag);
expect(formikBag.setSubmitting).toHaveBeenCalledWith(false);
@ -108,8 +110,8 @@ describe('validateUsernameRemotelyWithCache test with successful requests', () =
});
test('validateUsernameRemotelyWithCache calls validate.validateUsernameRemotely', done => {
const wrapper = shallowWithIntl(<UsernameStep />);
const instance = wrapper.dive().instance();
const wrapper = renderWithIntl(<UsernameStep />, 'UsernameStep');
const instance = wrapper.instance();
instance.validateUsernameRemotelyWithCache('newUniqueUsername55')
.then(response => {
@ -122,10 +124,11 @@ describe('validateUsernameRemotelyWithCache test with successful requests', () =
});
test('validateUsernameRemotelyWithCache, called twice with different data, makes two remote requests', done => {
const wrapper = shallowWithIntl(
<UsernameStep />
const wrapper = renderWithIntl(
<UsernameStep />,
'UsernameStep'
);
const instance = wrapper.dive().instance();
const instance = wrapper.instance();
instance.validateUsernameRemotelyWithCache('newUniqueUsername55')
.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 => {
const wrapper = shallowWithIntl(
<UsernameStep />
const wrapper = renderWithIntl(
<UsernameStep />,
'UsernameStep'
);
const instance = wrapper.dive().instance();
const instance = wrapper.instance();
instance.validateUsernameRemotelyWithCache('newUniqueUsername55')
.then(response => {
@ -187,8 +191,8 @@ describe('validateUsernameRemotelyWithCache test with failing requests', () => {
});
test('validateUsernameRemotelyWithCache calls validate.validateUsernameRemotely', done => {
const wrapper = shallowWithIntl(<UsernameStep />);
const instance = wrapper.dive().instance();
const wrapper = renderWithIntl(<UsernameStep />, 'UsernameStep');
const instance = wrapper.instance();
instance.validateUsernameRemotelyWithCache('newUniqueUsername55')
.then(response => {
@ -201,10 +205,11 @@ describe('validateUsernameRemotelyWithCache test with failing requests', () => {
});
test('validateUsernameRemotelyWithCache, called twice with different data, makes two remote requests', done => {
const wrapper = shallowWithIntl(
<UsernameStep />
const wrapper = renderWithIntl(
<UsernameStep />,
'UsernameStep'
);
const instance = wrapper.dive().instance();
const instance = wrapper.instance();
instance.validateUsernameRemotelyWithCache('newUniqueUsername55')
.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 => {
const wrapper = shallowWithIntl(
<UsernameStep />
const wrapper = renderWithIntl(
<UsernameStep />,
'UsernameStep'
);
const instance = wrapper.dive().instance();
const instance = wrapper.instance();
instance.validateUsernameRemotelyWithCache('newUniqueUsername55')
.then(response => {