feat: [UEPR-57] migrate to react v18

This commit is contained in:
MiroslavDionisiev 2025-03-13 15:35:10 +02:00
parent c2c9f4523c
commit a739682d91
21 changed files with 2538 additions and 3112 deletions

4602
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -52,10 +52,10 @@
"lodash.defaults": "4.2.0",
"lodash.get": "4.4.2",
"react-confetti": "6.1.0",
"react-helmet": "5.2.1",
"react-helmet": "^6.0.0",
"react-onclickoutside": "6.13.0",
"react-router-dom": "5.3.4",
"react-twitter-embed": "3.0.3",
"react-router-dom": "6.30.0",
"react-twitter-embed": "4.0.4",
"react-use": "17.6.0",
"scratch-parser": "6.0.0",
"scratch-storage": "^4.0.55"
@ -75,6 +75,9 @@
"@formatjs/intl-pluralrules": "5.4.3",
"@formatjs/intl-relativetimeformat": "11.4.10",
"@scratch/scratch-gui": "^11.0.0-beta.2",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.6.1",
"@types/jest": "29.5.14",
"async": "3.2.6",
"autoprefixer": "10.4.20",
@ -90,8 +93,6 @@
"css-loader": "5.2.7",
"email-validator": "2.0.4",
"emit-file-webpack-plugin": "2.0.1",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.8",
"eslint": "8.57.1",
"eslint-config-scratch": "9.0.9",
"eslint-plugin-jest": "27.9.0",
@ -100,7 +101,7 @@
"eslint-plugin-react-hooks": "4.6.2",
"fastly": "1.2.1",
"file-loader": "6.2.0",
"formik": "1.5.8",
"formik": "2.4.6",
"formsy-react": "1.1.6",
"formsy-react-components": "1.1.0",
"git-bundle-sha": "0.0.2",
@ -130,18 +131,18 @@
"postcss-simple-vars": "5.0.2",
"prop-types": "15.8.1",
"query-string": "9.1.1",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-intl": "5.25.1",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-intl": "6.8.9",
"react-modal": "3.16.3",
"react-plotly.js": "2.6.0",
"react-redux": "5.1.2",
"react-responsive": "3.0.0",
"react-redux": "^8.0.0",
"react-responsive": "9.0.0",
"react-slick": "0.30.3",
"react-string-replace": "0.4.1",
"react-telephone-input": "4.75.5",
"react-test-renderer": "16.14.0",
"redux": "3.7.2",
"react-test-renderer": "18.3.1",
"redux": "^4.0.0",
"redux-mock-store": "1.5.5",
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.13.9",
@ -152,7 +153,7 @@
"slick-carousel": "1.8.1",
"stream-browserify": "3.0.0",
"style-loader": "4.0.0",
"tap": "14.11.0",
"tap": "16.3.10",
"url-loader": "3.0.0",
"use-onclickoutside": "0.4.1",
"webpack": "5.98.0",
@ -163,9 +164,9 @@
},
"jest": {
"setupFiles": [
"<rootDir>/test/helpers/enzyme-setup.js",
"jest-canvas-mock"
],
"testEnvironment": "jsdom",
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/fileMock.js",
"\\.(css|less|scss)$": "<rootDir>/test/__mocks__/styleMock.js"

View file

@ -1,6 +1,6 @@
// JSX syntax transforms to React.createElement
const React = require('react'); // eslint-disable-line
const ReactDOM = require('react-dom');
const ReactDOM = require('react-dom/client');
const StoreProvider = require('react-redux').Provider;
const IntlProvider = require('react-intl').IntlProvider;
@ -33,8 +33,10 @@ const render = (jsx, element, reducers, initialState, enhancer) => {
intlPolyfill(intlLocale).then(() => {
const store = configureStore(reducers, initialState, enhancer);
const root = ReactDOM.createRoot(element);
// Render view component
ReactDOM.render(
root.render(
<StoreProvider store={store}>
<IntlProvider
locale={intlLocale}
@ -43,8 +45,7 @@ const render = (jsx, element, reducers, initialState, enhancer) => {
>
{jsx}
</IntlProvider>
</StoreProvider>,
element
</StoreProvider>
);
// Get initial session & permissions

View file

@ -117,7 +117,7 @@ const Ideas = () => {
src="https://scratch.mit.edu/projects/1108790117/embed"
width="485"
height="402"
allowfullscreen
allowFullScreen
className="ideas-project"
/>
<div className="banner-description">

View file

@ -1,10 +1,10 @@
/* eslint-disable react/jsx-no-bind */
import React, {useState} from 'react';
import React, {useRef, useState} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import onClickOutside from 'react-onclickoutside';
import useOnClickOutside from 'use-onclickoutside';
import {selectStudioDescription, selectIsFetchingInfo} from '../../redux/studio';
import {selectCanEditInfo, selectShowEditMuteError} from '../../redux/studio-permissions';
@ -33,9 +33,11 @@ const StudioDescription = ({
const [showMuteMessage, setShowMuteMessage] = useState(false);
const [hideValidationMessage, setHideValidationMessage] = useState(false);
StudioDescription.handleClickOutside = () => {
const ref = useRef(null);
useOnClickOutside(ref, () => {
setHideValidationMessage(true);
};
});
const fieldClassName = classNames('studio-description', {
'mod-fetching': isFetching,
@ -49,6 +51,7 @@ const StudioDescription = ({
className="studio-info-section"
onMouseEnter={() => isMutedEditor && setShowMuteMessage(true)}
onMouseLeave={() => isMutedEditor && setShowMuteMessage(false)}
ref={ref}
>
{canEditInfo || isMutedEditor ? (
<React.Fragment>
@ -81,10 +84,6 @@ const StudioDescription = ({
);
};
const clickOutsideConfig = {
handleClickOutside: () => StudioDescription.handleClickOutside
};
StudioDescription.propTypes = {
descriptionError: PropTypes.string,
canEditInfo: PropTypes.bool,
@ -95,7 +94,7 @@ StudioDescription.propTypes = {
handleUpdate: PropTypes.func
};
const connectedStudioDescription = connect(
export default connect(
state => ({
description: selectStudioDescription(state),
canEditInfo: selectCanEditInfo(state),
@ -108,5 +107,3 @@ const connectedStudioDescription = connect(
handleUpdate: mutateStudioDescription
}
)(StudioDescription);
export default onClickOutside(connectedStudioDescription, clickOutsideConfig);

View file

@ -1,5 +1,5 @@
/* eslint-disable react/jsx-no-bind */
import React, {useState} from 'react';
import React, {useState, useRef} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {FormattedMessage} from 'react-intl';
@ -32,7 +32,7 @@ const StudioFollow = ({
});
const [hideValidationMessage, setHideValidationMessage] = useState(false);
const ref = React.useRef(null);
const ref = useRef(null);
useOnClickOutside(ref, () => {
setHideValidationMessage(true);

View file

@ -1,10 +1,10 @@
/* eslint-disable react/jsx-no-bind */
import React, {useState} from 'react';
import React, {useRef, useState} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import onClickOutside from 'react-onclickoutside';
import useOnClickOutside from 'use-onclickoutside';
import {selectStudioImage, selectIsFetchingInfo} from '../../redux/studio';
import {selectCanEditInfo, selectShowEditMuteError} from '../../redux/studio-permissions';
@ -46,9 +46,12 @@ const StudioImage = ({
const [showMuteMessage, setShowMuteMessage] = useState(false);
const [hideValidationMessage, setHideValidationMessage] = useState(false);
StudioImage.handleClickOutside = () => {
const ref = useRef(null);
useOnClickOutside(ref, () => {
setHideValidationMessage(true);
};
});
return (
<div
className={fieldClassName}
@ -99,10 +102,6 @@ const StudioImage = ({
);
};
const clickOutsideConfig = {
handleClickOutside: () => StudioImage.handleClickOutside
};
StudioImage.propTypes = {
imageError: PropTypes.string,
canEditInfo: PropTypes.bool,
@ -113,7 +112,7 @@ StudioImage.propTypes = {
handleUpdate: PropTypes.func
};
const connectedStudioImage = connect(
export default connect(
state => ({
image: selectStudioImage(state),
canEditInfo: selectCanEditInfo(state),
@ -126,5 +125,3 @@ const connectedStudioImage = connect(
handleUpdate: mutateStudioImage
}
)(StudioImage);
export default onClickOutside(connectedStudioImage, clickOutsideConfig);

View file

@ -1,7 +1,7 @@
import React from 'react';
import React, {useCallback} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {useRouteMatch, NavLink} from 'react-router-dom';
import {useParams, NavLink} from 'react-router-dom';
import {FormattedMessage} from 'react-intl';
import SubNavigation from '../../components/subnavigation/subnavigation.jsx';
@ -40,67 +40,67 @@ const countLimits = {
};
const StudioTabNav = ({isFetchingInfo, commentCount, projectCount}) => {
const {params: {studioPath, studioId}} = useRouteMatch();
const {studioPath, studioId} = useParams();
const base = `/${studioPath}/${studioId}`;
const classes = useCallback(({isActive}) => `nav-link ${isActive ? 'activated' : ''}`);
return (
<SubNavigation
align="left"
className="studio-tab-nav"
>
<NavLink
activeClassName="active"
className="nav_link"
className={classes}
to={base}
exact
>
<li><img
src={projectsIcon}
/><FormattedMessage
id={isFetchingInfo ? 'studio.tabNavProjects' : 'studio.tabNavProjectsWithCount'}
values={{
projectCount: (
<span className="tab-count">
({limitCount(projectCount, countLimits.projects)})
</span>
)
}}
/></li>
<li>
<img src={projectsIcon} />
<FormattedMessage
id={isFetchingInfo ? 'studio.tabNavProjects' : 'studio.tabNavProjectsWithCount'}
values={{
projectCount: (
<span className="tab-count">
({limitCount(projectCount, countLimits.projects)})
</span>
)
}}
/>
</li>
</NavLink>
<NavLink
activeClassName="active"
className="nav_link"
className={classes}
to={`${base}/comments`}
>
<li><img
src={commentsIcon}
/><FormattedMessage
id={isFetchingInfo ? 'studio.tabNavComments' : 'studio.tabNavCommentsWithCount'}
values={{
commentCount: (
<span className="tab-count">
({limitCount(commentCount, countLimits.comments)})
</span>
)
}}
/></li>
<li>
<img src={commentsIcon} />
<FormattedMessage
id={isFetchingInfo ? 'studio.tabNavComments' : 'studio.tabNavCommentsWithCount'}
values={{
commentCount: (
<span className="tab-count">
({limitCount(commentCount, countLimits.comments)})
</span>
)
}}
/>
</li>
</NavLink>
<NavLink
activeClassName="active"
className="nav_link"
className={classes}
to={`${base}/curators`}
>
<li><img
src={curatorsIcon}
/><FormattedMessage id="studio.tabNavCurators" /></li>
<li>
<img src={curatorsIcon} />
<FormattedMessage id="studio.tabNavCurators" />
</li>
</NavLink>
<NavLink
activeClassName="active"
className="nav_link"
className={classes}
to={`${base}/activity`}
>
<li><img
src={activityIcon}
/><FormattedMessage id="studio.tabNavActivity" /></li>
<li>
<img src={activityIcon} />
<FormattedMessage id="studio.tabNavActivity" />
</li>
</NavLink>
</SubNavigation>
);

View file

@ -1,10 +1,10 @@
/* eslint-disable react/jsx-no-bind */
import React, {useState} from 'react';
import React, {useRef, useState} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import onClickOutside from 'react-onclickoutside';
import useOnClickOutside from 'use-onclickoutside';
import '../../components/forms/inplace-input.scss';
import {selectStudioTitle, selectIsFetchingInfo} from '../../redux/studio';
@ -38,9 +38,11 @@ const StudioTitle = ({
const [showMuteMessage, setShowMuteMessage] = useState(false);
const [hideValidationMessage, setHideValidationMessage] = useState(false);
StudioTitle.handleClickOutside = () => {
const ref = useRef(null);
useOnClickOutside(ref, () => {
setHideValidationMessage(true);
};
});
return (
<div
@ -74,10 +76,6 @@ const StudioTitle = ({
);
};
const clickOutsideConfig = {
handleClickOutside: () => StudioTitle.handleClickOutside
};
StudioTitle.propTypes = {
titleError: PropTypes.string,
canEditInfo: PropTypes.bool,
@ -88,7 +86,7 @@ StudioTitle.propTypes = {
handleUpdate: PropTypes.func
};
const connectedStudioTitle = connect(
export default connect(
state => ({
title: selectStudioTitle(state),
canEditInfo: selectCanEditInfo(state),
@ -101,5 +99,3 @@ const connectedStudioTitle = connect(
handleUpdate: mutateStudioTitle
}
)(StudioTitle);
export default onClickOutside(connectedStudioTitle, clickOutsideConfig);

View file

@ -1,10 +1,10 @@
import React, {useEffect} from 'react';
import {
BrowserRouter as Router,
Switch,
Routes,
Route,
Redirect,
useRouteMatch
Navigate,
useParams
} from 'react-router-dom';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
@ -38,7 +38,7 @@ import {FormattedMessage} from 'react-intl';
import {selectShowCuratorMuteError} from '../../redux/studio-permissions.js';
const StudioShell = ({isAdmin, showCuratorMuteError, muteExpiresAtMs, studioLoadFailed, onLoadInfo}) => {
const match = useRouteMatch();
const {studioPath, studioId} = useParams();
useEffect(() => {
onLoadInfo();
@ -56,41 +56,52 @@ const StudioShell = ({isAdmin, showCuratorMuteError, muteExpiresAtMs, studioLoad
<div className="studio-tabs">
<StudioTabNav />
<div>
<Switch>
<Route path={`${match.path}/curators`}>
<StudioCuratorInvite />
{showCuratorMuteError &&
<CommentingStatus>
<p>
<div>
<FormattedMessage
id="studio.mutedCurators"
values={{
inDuration: formatRelativeTime(muteExpiresAtMs, window._locale)
}}
/>
</div>
<div><FormattedMessage id="studio.mutedPaused" /></div>
</p>
</CommentingStatus>
<Routes>
<Route
path="/curators"
element={
<>
<StudioCuratorInvite />
{showCuratorMuteError &&
<CommentingStatus>
<p>
<div>
<FormattedMessage
id="studio.mutedCurators"
values={{
inDuration: formatRelativeTime(
muteExpiresAtMs,
window._locale
)
}}
/>
</div>
<div><FormattedMessage id="studio.mutedPaused" /></div>
</p>
</CommentingStatus>
}
<StudioManagers />
<StudioCurators />
</>
}
<StudioManagers />
<StudioCurators />
</Route>
<Route path={`${match.path}/comments`}>
<StudioComments />
</Route>
<Route path={`${match.path}/activity`}>
<StudioActivity />
</Route>
<Route path={`${match.path}/projects`}>
{/* We can force /projects back to / this way */}
<Redirect to={match.url} />
</Route>
<Route path={match.path}>
<StudioProjects />
</Route>
</Switch>
/>
<Route
path="/comments"
element={<StudioComments />}
/>
<Route
path="/activity"
element={<StudioActivity />}
/>
<Route
path="/projects"
element={<Navigate to={`/${studioPath}/${studioId}`} />}
/>
<Route
path="/"
element={<StudioProjects />}
/>
</Routes>
</div>
</div>
</div>
@ -121,12 +132,13 @@ render(
<Page className="studio-page">
<StudioAdminPanel />
<Router>
<Switch>
<Routes>
{/* Use variable studioPath to support /studio-playground/ or future route */}
<Route path="/:studioPath/:studioId">
<ConnectedStudioShell />
</Route>
</Switch>
<Route
path="/:studioPath/:studioId/*"
element={<ConnectedStudioShell />}
/>
</Routes>
</Router>
</Page>,
document.getElementById('app'),

View file

@ -1,52 +1,49 @@
/*
* Helpers for using enzyme and react-test-renderer with react-intl
*/
import React from 'react';
import renderer from 'react-test-renderer';
import {createIntl, IntlProvider} from 'react-intl';
import {mount, shallow} from 'enzyme';
import intlShape from '../../src/lib/intl-shape';
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';
const shallowWithIntl = (node, {context} = {}) => shallow(
node,
{
context: Object.assign({}, context),
wrappingComponent: IntlProvider,
wrappingComponentProps: {
locale: 'en',
messages: {}
// 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;
}
}
).dive();
}
const mountWithIntl = (node, {context, childContextTypes} = {}) => {
const intl = createIntl({locale: 'en', messages: {}});
return mount(
node,
{
context: Object.assign({}, context, {intl}),
childContextTypes: Object.assign({}, {intl: intlShape}, childContextTypes),
wrappingComponent: IntlProvider,
wrappingComponentProps: {
locale: 'en',
messages: {}
}
}
);
};
views.map(view => defaultLocales[view]).reduce((acc, curr) => merge(acc, curr), generalLocales);
// react-test-renderer component for use with snapshot testing
const componentWithIntl = (children, props = {locale: 'en'}) => renderer.create(
<IntlProvider
textComponent="span"
{...props}
>
{children}
</IntlProvider>
);
const renderWithIntl = ui => ({
...render(
<IntlProvider
locale="en"
messages={generalLocales.en}
>
{ui}
</IntlProvider>
)
});
export {
componentWithIntl,
shallowWithIntl,
mountWithIntl
};
export {renderWithIntl};

View file

@ -0,0 +1,68 @@
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,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Captcha test Captcha renders the div google wants 1`] = `
<div
class="g-recaptcha"
data-badge="bottomright"
data-size="invisible"
/>
`;

View file

@ -0,0 +1,71 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CommentingStatus Basic render 1`] = `
<div
class="commenting-status"
>
<div
class="commenting-status-inner-content"
>
<div
class="flex-row comment-status-img"
>
<img
class="comment-status-icon"
src="/svgs/project/comment-status.svg"
/>
</div>
<div
class="flex-row"
/>
</div>
</div>
`;
exports[`CommentingStatus Children added 1`] = `
<div
class="commenting-status"
>
<div
class="commenting-status-inner-content"
>
<div
class="flex-row comment-status-img"
>
<img
class="comment-status-icon"
src="/svgs/project/comment-status.svg"
/>
</div>
<div
class="flex-row"
>
<img
class="myChildDiv"
/>
</div>
</div>
</div>
`;
exports[`CommentingStatus ClassNames added 1`] = `
<div
class="commenting-status class1"
>
<div
class="commenting-status-inner-content class2"
>
<div
class="flex-row comment-status-img"
>
<img
class="comment-status-icon"
src="/svgs/project/comment-status.svg"
/>
</div>
<div
class="flex-row"
/>
</div>
</div>
`;

View file

@ -1,9 +1,10 @@
/* eslint-disable max-len */
const React = require('react');
const {mountWithIntl} = require('../../helpers/intl-helpers.jsx');
const {renderWithIntl} = require('../../helpers/intl-helpers.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';
jest.mock('react-dom', () => ({
render: jest.fn()
@ -24,10 +25,10 @@ describe('BecomeAScratcherPage', () => {
}
}
});
const component = mountWithIntl(
const {container} = renderWithIntl(
<BecomeAScratcherPage />, {context: {store: NotLoggedInUserStore}}
);
expect(component.find('div.not-available-outer').exists()).toBeTruthy();
expect(container.querySelector('div.not-available-outer')).toBeInTheDocument();
});
test('Display No Invitation when user is not invited', () => {
@ -45,10 +46,10 @@ describe('BecomeAScratcherPage', () => {
}
}
});
const component = mountWithIntl(
const {container} = renderWithIntl(
<BecomeAScratcherPage />, {context: {store: NotInvitedUserStore}}
);
expect(component.find('div.no-invitation').exists()).toBeTruthy();
expect(container.querySelector('div.no-invitation').exists()).toBeInTheDocument();
});
test('Display Onboarding when user is invited', () => {
@ -67,10 +68,10 @@ describe('BecomeAScratcherPage', () => {
}
}
});
const component = mountWithIntl(
const {container} = renderWithIntl(
<BecomeAScratcherPage />, {context: {store: InvitedUserStore}}
);
expect(component.find('div.congratulations-page').exists()).toBeTruthy();
expect(container.querySelector('div.congratulations-page').exists()).toBeInTheDocument();
});
test('Display celebration page when user is already a scratcher', () => {
@ -89,9 +90,9 @@ describe('BecomeAScratcherPage', () => {
}
}
});
const component = mountWithIntl(
const {container} = renderWithIntl(
<BecomeAScratcherPage />, {context: {store: AlreadyScratcherStore}}
);
expect(component.find('div.hooray-screen').exists()).toBeTruthy();
expect(container.querySelector('div.hooray-screen').exists()).toBeInTheDocument();
});
});

View file

@ -1,8 +1,7 @@
const React = require('react');
const enzyme = require('enzyme');
const Captcha = require('../../../src/components/captcha/captcha.jsx');
const {render} = require('@testing-library/react');
describe('Captcha test', () => {
global.grecaptcha = {
@ -14,10 +13,10 @@ describe('Captcha test', () => {
const props = {
onCaptchaLoad: jest.fn()
};
const wrapper = enzyme.shallow(<Captcha
render(<Captcha
{...props}
/>);
wrapper.instance().onCaptchaLoad();
expect(global.grecaptcha.render).toHaveBeenCalled();
expect(props.onCaptchaLoad).toHaveBeenCalled();
});
@ -26,10 +25,10 @@ describe('Captcha test', () => {
const props = {
onCaptchaLoad: jest.fn()
};
const wrapper = enzyme.shallow(<Captcha
const {container} = (<Captcha
{...props}
/>);
wrapper.instance().executeCaptcha();
container.executeCaptcha();
expect(global.grecaptcha.execute).toHaveBeenCalled();
});
@ -37,9 +36,9 @@ describe('Captcha test', () => {
const props = {
onCaptchaLoad: jest.fn()
};
const wrapper = enzyme.mount(<Captcha
const {container} = render(<Captcha
{...props}
/>);
expect(wrapper.find('div.g-recaptcha')).toHaveLength(1);
expect(container.firstChild).toMatchSnapshot();
});
});

View file

@ -1,33 +1,35 @@
const React = require('react');
const {shallow} = require('enzyme');
const {render} = require('@testing-library/react');
require('@testing-library/jest-dom');
const CommentingStatus = require('../../../src/components/commenting-status/commenting-status.jsx');
describe('CommentingStatus', () => {
test('Basic render', () => {
const component = shallow(
const {container} = render(
<CommentingStatus />
);
expect(component.find('div.commenting-status').exists()).toBe(true);
expect(component.find('img.comment-status-icon').exists()).toBe(true);
expect(container.firstChild).toMatchSnapshot();
});
test('ClassNames added', () => {
const component = shallow(
const {container} = render(
<CommentingStatus
className="class1"
innerClassName="class2"
/>
);
expect(component.find('div.class1').exists()).toBe(true);
expect(component.find('div.class2').exists()).toBe(true);
expect(container.firstChild).toMatchSnapshot();
});
test('Children added', () => {
const component = shallow(
const {container} = render(
<CommentingStatus>
<img className="myChildDiv" />
</CommentingStatus>
);
expect(component.find('img.myChildDiv').exists()).toBe(true);
expect(container.firstChild).toMatchSnapshot();
});
});

View file

@ -1,8 +1,10 @@
const React = require('react');
const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx');
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import React, {act} from 'react';
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 {screen} from '@testing-library/react';
describe('Compose Comment test', () => {
const mockStore = configureStore();
@ -40,13 +42,14 @@ describe('Compose Comment test', () => {
if (!store) {
store = defaultStore;
}
const wrapper = shallowWithIntl(
<ComposeComment
{...defaultProps()}
{...props}
/>
, {context: {store}}
const wrapper = renderWithIntl(
<Provider store={store}>
<ComposeComment
{...defaultProps()}
{...props}
/>
</Provider>,
'ComposeComment'
);
return wrapper;
};
@ -82,57 +85,65 @@ describe('Compose Comment test', () => {
});
test('Modal & Comment status do not show', () => {
const component = getComposeCommentWrapper({});
const {container} = getComposeCommentWrapper({});
// Comment compsoe box is there
expect(component.find('FlexRow.compose-comment').exists()).toEqual(true);
expect(container.querySelector('.compose-comment')).toBeInTheDocument();
// No error message
expect(component.find('FlexRow.compose-error-row').exists()).toEqual(false);
expect(component.find('MuteModal').exists()).toEqual(false);
expect(component.find('CommentingStatus').exists()).toEqual(false);
expect(container.querySelector('.compose-error-row')).not.toBeInTheDocument();
expect(container.querySelector('.modal-mute')).not.toBeInTheDocument();
expect(container.querySelector('.commenting-status')).not.toBeInTheDocument();
// Buttons start enabled
expect(component.find('Button.compose-post').props().disabled).toBe(false);
expect(component.find('Button.compose-cancel').props().disabled).toBe(false);
expect(container.querySelector('.compose-post')).not.toBeDisabled();
expect(container.querySelector('.compose-cancel')).not.toBeDisabled();
});
test('Error messages shows when comment rejected', () => {
const component = getComposeCommentWrapper({});
const commentInstance = component.instance();
commentInstance.setState({
error: 'isFlood',
status: 'REJECTED'
test('Error messages shows when comment rejected', async () => {
const {container, instance} = getComposeCommentWrapper({});
const commentInstance = instance();
await act(() => {
commentInstance.setState({
error: 'isFlood',
status: 'REJECTED'
});
});
component.update();
expect(component.find('FlexRow.compose-error-row').exists()).toEqual(true);
expect(container.querySelector('.compose-error-row')).toBeInTheDocument();
// Buttons stay enabled when comment rejected for non-mute reasons
expect(component.find('Button.compose-post').props().disabled).toBe(false);
expect(component.find('Button.compose-cancel').props().disabled).toBe(false);
expect(container.querySelector('.compose-post')).not.toBeDisabled();
expect(container.querySelector('.compose-cancel')).not.toBeDisabled();
});
test('No error message shows when comment rejected because user is already muted', () => {
const component = getComposeCommentWrapper({});
const commentInstance = component.instance();
commentInstance.setState({
error: 'isMuted',
status: 'COMPOSE_DISALLOWED'
test('No error message shows when comment rejected because user is already muted', async () => {
const {container, instance} = getComposeCommentWrapper({});
const commentInstance = instance();
await act(() => {
commentInstance.setState({
error: 'isMuted',
status: 'COMPOSE_DISALLOWED'
});
});
component.update();
expect(component.find('FlexRow.compose-error-row').exists()).toEqual(false);
expect(container.querySelector('.compose-error-row')).not.toBeInTheDocument();
});
test('Comment Status shows but compose box does not when you load the page and you are already muted', () => {
const realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0;
const component = getComposeCommentWrapper({});
const commentInstance = component.instance();
commentInstance.setState({muteExpiresAtMs: 100, status: 'COMPOSE_DISALLOWED'});
component.update();
const {container, instance} = getComposeCommentWrapper({});
const commentInstance = instance();
act(() => {
commentInstance.setState({muteExpiresAtMs: 100, status: 'COMPOSE_DISALLOWED'});
});
// Compose box should be hidden if muted unless they got muted due to a comment they just posted.
expect(component.find('FlexRow.compose-comment').exists()).toEqual(false);
expect(component.find('MuteModal').exists()).toEqual(false);
expect(component.find('CommentingStatus').exists()).toEqual(true);
global.Date.now = realDateNow;
expect(container.querySelector('.compose-comment')).not.toBeInTheDocument();
expect(container.querySelector('.modal-mute')).not.toBeInTheDocument();
expect(container.querySelector('.commenting-status')).toBeInTheDocument();
act(() => {
global.Date.now = realDateNow;
});
});
test('Comment Status and compose box do not show on replies when muted, but mute modal does', () => {
@ -152,26 +163,20 @@ describe('Compose Comment test', () => {
}
}
});
const component = mountWithIntl(
<ComposeComment
{...defaultProps()}
isReply
/>
, {context: {store}}
);
expect(component.find('FlexRow.compose-comment').exists()).toEqual(false);
expect(component.find('MuteModal').exists()).toBe(true);
expect(component.find('MuteModal').props().startStep).toBe(1);
expect(component.find('CommentingStatus').exists()).toEqual(false);
const {container, findByComponentName} = getComposeCommentWrapper({isReply: true}, store);
expect(container.querySelector('.compose-comment')).not.toBeInTheDocument();
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(findByComponentName('MuteModal').props.startStep).toBe(1);
expect(container.querySelector('.commenting-status')).not.toBeInTheDocument();
global.Date.now = realDateNow;
});
test('Comment Status and compose box show on replies when not muted', () => {
const realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0;
const component = getComposeCommentWrapper({isReply: true});
expect(component.find('FlexRow.compose-comment').exists()).toEqual(true);
expect(component.find('CommentingStatus').exists()).toEqual(false);
const {container} = getComposeCommentWrapper({isReply: true});
expect(container.querySelector('.compose-comment')).toBeInTheDocument();
expect(container.querySelector('.commenting-status')).not.toBeInTheDocument();
global.Date.now = realDateNow;
});
@ -194,89 +199,93 @@ describe('Compose Comment test', () => {
}
}
});
const component = getComposeCommentWrapper({}, mutedStore);
const commentInstance = component.instance();
const {container, instance} = getComposeCommentWrapper({}, mutedStore);
const commentInstance = instance();
// Check conversion to ms from seconds is done at init time.
expect(commentInstance.state.muteExpiresAtMs).toEqual(5 * 1000);
// Check we setup a timeout to expire the widget when timeout reached.
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 5 * 1000);
expect(commentInstance.state.showWarning).toBe(true);
// Compose box should be hidden if muted unless they got muted due to a comment they just posted.
expect(component.find('FlexRow.compose-comment').exists()).toEqual(false);
expect(component.find('MuteModal').exists()).toEqual(false);
expect(component.find('CommentingStatus').exists()).toEqual(true);
expect(container.querySelector('FlexRow.compose-comment')).not.toBeInTheDocument();
expect(container.querySelector('.modal-mute')).not.toBeInTheDocument();
expect(container.querySelector('.commenting-status')).toBeInTheDocument();
global.Date.now = realDateNow;
});
test('Comment Status shows when user just submitted a reply comment that got them muted', () => {
const realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0;
const component = getComposeCommentWrapper({isReply: true});
const commentInstance = component.instance();
commentInstance.setState({
status: 'REJECTED_MUTE',
muteExpiresAtMs: 100
const {container, instance} = getComposeCommentWrapper({isReply: true});
const commentInstance = instance();
act(() => {
commentInstance.setState({
status: 'REJECTED_MUTE',
muteExpiresAtMs: 100
});
});
component.update();
expect(component.find('FlexRow.compose-comment').exists()).toEqual(true);
expect(component.find('MuteModal').exists()).toEqual(false);
expect(component.find('CommentingStatus').exists()).toEqual(true);
expect(container.querySelector('.compose-comment')).toBeInTheDocument();
expect(container.querySelector('.modal-mute')).not.toBeInTheDocument();
expect(container.querySelector('.commenting-status')).toBeInTheDocument();
// Compose box exists but is disabled
expect(component.find('InplaceInput.compose-input').exists()).toEqual(true);
expect(component.find('InplaceInput.compose-input').props().disabled).toBe(true);
expect(component.find('Button.compose-post').props().disabled).toBe(true);
expect(component.find('Button.compose-cancel').props().disabled).toBe(true);
expect(container.querySelector('.compose-input')).toBeInTheDocument();
expect(container.querySelector('.inplace-textarea')).toBeDisabled();
expect(container.querySelector('.compose-post')).toBeDisabled();
expect(container.querySelector('.compose-cancel')).toBeDisabled();
global.Date.now = realDateNow;
});
test('Comment Status shows when user just submitted a comment that got them muted', () => {
const realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0;
const component = getComposeCommentWrapper({});
const commentInstance = component.instance();
commentInstance.setState({
status: 'REJECTED_MUTE',
muteExpiresAtMs: 100
const {container, instance} = getComposeCommentWrapper({});
const commentInstance = instance();
act(() => {
commentInstance.setState({
status: 'REJECTED_MUTE',
muteExpiresAtMs: 100
});
});
component.update();
expect(component.find('FlexRow.compose-comment').exists()).toEqual(true);
expect(component.find('MuteModal').exists()).toEqual(false);
expect(component.find('CommentingStatus').exists()).toEqual(true);
expect(container.querySelector('.compose-comment')).toBeInTheDocument();
expect(container.querySelector('.modal-mute')).not.toBeInTheDocument();
expect(container.querySelector('.commenting-status')).toBeInTheDocument();
// Compose box exists but is disabled
expect(component.find('InplaceInput.compose-input').exists()).toEqual(true);
expect(component.find('InplaceInput.compose-input').props().disabled).toBe(true);
expect(component.find('Button.compose-post').props().disabled).toBe(true);
expect(component.find('Button.compose-cancel').props().disabled).toBe(true);
expect(container.querySelector('.compose-input')).toBeInTheDocument();
expect(container.querySelector('.inplace-textarea')).toBeDisabled();
expect(container.querySelector('.compose-post')).toBeDisabled();
expect(container.querySelector('.compose-cancel')).toBeDisabled();
global.Date.now = realDateNow;
});
test('Comment Error does not show for mutes', () => {
const realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0;
const component = getComposeCommentWrapper({});
const commentInstance = component.instance();
commentInstance.setState({
status: 'REJECTED_MUTE',
error: 'a mute error'
const {container, instance} = getComposeCommentWrapper({});
const commentInstance = instance();
act(() => {
commentInstance.setState({
status: 'REJECTED_MUTE',
error: 'a mute error'
});
});
component.update();
expect(component.find('FlexRow.compose-error-row').exists()).toEqual(false);
expect(component.find('FlexRow.compose-comment').exists()).toEqual(true);
expect(container.querySelector('.compose-error-row')).not.toBeInTheDocument();
expect(container.querySelector('.compose-comment')).toBeInTheDocument();
global.Date.now = realDateNow;
});
test('Comment Error does show for non-mute errors', () => {
const component = getComposeCommentWrapper({});
const commentInstance = component.instance();
commentInstance.setState({
error: 'some error',
status: 'REJECTED'
const {container, instance} = getComposeCommentWrapper({});
const commentInstance = instance();
act(() => {
commentInstance.setState({
error: 'some error',
status: 'REJECTED'
});
});
component.update();
expect(component.find('FlexRow.compose-error-row').exists()).toEqual(true);
expect(component.find('FlexRow.compose-comment').exists()).toEqual(true);
expect(component.find('InplaceInput.compose-input').exists()).toEqual(true);
expect(component.find('InplaceInput.compose-input').props().disabled).toBe(false);
expect(component.find('Button.compose-post').props().disabled).toBe(false);
expect(component.find('Button.compose-cancel').props().disabled).toBe(false);
expect(container.querySelector('.compose-error-row')).toBeInTheDocument();
expect(container.querySelector('.compose-comment')).toBeInTheDocument();
expect(container.querySelector('.compose-input')).toBeInTheDocument();
expect(container.querySelector('.inplace-textarea')).not.toBeDisabled();
expect(container.querySelector('.compose-post')).not.toBeDisabled();
expect(container.querySelector('.compose-cancel')).not.toBeDisabled();
});
test('Mute Modal shows when muteOpen is true', () => {
@ -292,19 +301,15 @@ describe('Compose Comment test', () => {
}
}
});
const component = mountWithIntl(
<ComposeComment
{...defaultProps()}
/>
, {context: {store}}
);
const {instance, findByComponentName} = getComposeCommentWrapper({}, store);
// set state on the ComposeComment component, not the wrapper
const commentInstance = component.find('ComposeComment').instance();
commentInstance.setState({muteOpen: true});
component.update();
expect(component.find('MuteModal').exists()).toEqual(true);
expect(component.find('MuteModal').props().startStep).toEqual(0);
expect(component.find('MuteModal').props().showWarning).toBe(false);
const commentInstance = instance();
act(() => {
commentInstance.setState({muteOpen: true});
});
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(findByComponentName('MuteModal').props.startStep).toEqual(0);
expect(findByComponentName('MuteModal').props.showWarning).toBe(false);
global.Date.now = realDateNow;
});
@ -319,24 +324,21 @@ describe('Compose Comment test', () => {
}
}
});
const component = mountWithIntl(
<ComposeComment
{...defaultProps()}
/>
, {context: {store}}
);
const {findByComponentName, instance} = getComposeCommentWrapper({}, store);
// set state on the ComposeComment component, not the wrapper
const commentInstance = component.find('ComposeComment').instance();
commentInstance.setState({muteOpen: true});
component.update();
expect(component.find('MuteModal').exists()).toEqual(true);
expect(component.find('MuteModal').props().showWarning).toBe(false);
commentInstance.setState({
muteOpen: true,
showWarning: true
const commentInstance = instance();
act(() => {
commentInstance.setState({muteOpen: true});
});
component.update();
expect(component.find('MuteModal').props().showWarning).toBe(true);
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(findByComponentName('MuteModal').props.showWarning).toBe(false);
act(() => {
commentInstance.setState({
muteOpen: true,
showWarning: true
});
});
expect(findByComponentName('MuteModal').props.showWarning).toBe(true);
});
test('Mute Modal gets showFeedback props from state', () => {
@ -351,46 +353,45 @@ describe('Compose Comment test', () => {
}
});
const component = mountWithIntl(
<ComposeComment
{...defaultProps()}
/>
, {context: {store}}
);
const {instance, findByComponentName} = getComposeCommentWrapper({}, store);
const commentInstance = component.find('ComposeComment').instance();
commentInstance.setState({
status: 'REJECTED_MUTE',
error: 'isBad',
muteOpen: true
const commentInstance = instance();
act(() => {
commentInstance.setState({
status: 'REJECTED_MUTE',
error: 'isBad',
muteOpen: true
});
});
component.update();
expect(component.find('MuteModal').exists()).toEqual(true);
expect(component.find('MuteModal').props().showFeedback).toBe(true);
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(findByComponentName('MuteModal').props.showFeedback).toBe(true);
commentInstance.setState({
status: 'COMPOSE_DISALLOWED',
error: 'isMute',
showWarning: true,
muteOpen: true
act(() => {
commentInstance.setState({
status: 'COMPOSE_DISALLOWED',
error: 'isMute',
showWarning: true,
muteOpen: true
});
});
component.update();
expect(component.find('MuteModal').exists()).toEqual(true);
expect(component.find('MuteModal').props().showFeedback).toBe(false);
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(findByComponentName('MuteModal').props.showFeedback).toBe(false);
commentInstance.setState({
status: 'REJECTED',
error: 'isBad',
showWarning: true,
muteOpen: true
act(() => {
commentInstance.setState({
status: 'REJECTED',
error: 'isBad',
showWarning: true,
muteOpen: true
});
});
component.update();
expect(component.find('MuteModal').exists()).toEqual(true);
expect(component.find('MuteModal').props().showFeedback).toBe(false);
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(findByComponentName('MuteModal').props.showFeedback).toBe(false);
});
test('shouldShowMuteModal is false when muteStatus is undefined', () => {
const commentInstance = getComposeCommentWrapper({}).instance();
expect(commentInstance.shouldShowMuteModal()).toBe(false);
@ -513,16 +514,20 @@ describe('Compose Comment test', () => {
test('getMuteModalStartStep: A reply that got them muted', () => {
const commentInstance = getComposeCommentWrapper({isReply: true}).instance();
commentInstance.setState({
status: 'REJECTED_MUTE'
act(() => {
commentInstance.setState({
status: 'REJECTED_MUTE'
});
});
expect(commentInstance.getMuteModalStartStep()).toBe(0);
});
test('getMuteModalStartStep: A reply click when already muted', () => {
const commentInstance = getComposeCommentWrapper({isReply: true}).instance();
commentInstance.setState({
status: 'COMPOSE_DISALLOWED'
act(() => {
commentInstance.setState({
status: 'COMPOSE_DISALLOWED'
});
});
expect(commentInstance.getMuteModalStartStep()).toBe(1);
});
@ -532,7 +537,9 @@ describe('Compose Comment test', () => {
global.Date.now = () => 0; // Set "now" to 0 for easier testing.
const commentInstance = getComposeCommentWrapper({}).instance();
commentInstance.setState({muteExpiresAtMs: 100});
act(() => {
commentInstance.setState({muteExpiresAtMs: 100});
});
expect(commentInstance.isMuted()).toBe(true);
global.Date.now = realDateNow;
});
@ -542,7 +549,9 @@ describe('Compose Comment test', () => {
global.Date.now = () => 0;
const commentInstance = getComposeCommentWrapper({}).instance();
commentInstance.setState({muteExpiresAtMs: -100});
act(() => {
commentInstance.setState({muteExpiresAtMs: -100});
});
expect(commentInstance.isMuted()).toBe(false);
global.Date.now = realDateNow;
});
@ -559,7 +568,9 @@ describe('Compose Comment test', () => {
test('getMuteMessageInfo: muteType set and just got muted', () => {
const justMuted = true;
const commentInstance = getComposeCommentWrapper({}).instance();
commentInstance.setState({muteType: 'unconstructive'});
act(() => {
commentInstance.setState({muteType: 'unconstructive'});
});
expect(commentInstance.getMuteMessageInfo(justMuted).commentType).toBe('comment.type.unconstructive');
expect(commentInstance.getMuteMessageInfo(justMuted)
.muteStepContent[0]).toBe('comment.unconstructive.content1');
@ -568,12 +579,16 @@ describe('Compose Comment test', () => {
test('getMuteMessageInfo: muteType set and already muted', () => {
const justMuted = false;
const commentInstance = getComposeCommentWrapper({}).instance();
commentInstance.setState({muteType: 'pii'});
act(() => {
commentInstance.setState({muteType: 'pii'});
});
expect(commentInstance.getMuteMessageInfo(justMuted).commentType).toBe('comment.type.pii.past');
// PII has the same content1 regardless of whether you were just muted
expect(commentInstance.getMuteMessageInfo(justMuted).muteStepContent[0]).toBe('comment.pii.content1');
commentInstance.setState({muteType: 'vulgarity'});
act(() => {
commentInstance.setState({muteType: 'vulgarity'});
});
expect(commentInstance.getMuteMessageInfo(justMuted).commentType).toBe('comment.type.vulgarity.past');
expect(commentInstance.getMuteMessageInfo(justMuted).muteStepContent[0]).toBe('comment.type.vulgarity.past');
});
@ -595,14 +610,18 @@ describe('Compose Comment test', () => {
test('getMuteMessageInfo: muteType set to something we don\'t have messages for and just got muted', () => {
const justMuted = true;
const commentInstance = getComposeCommentWrapper({}).instance();
commentInstance.setState({muteType: 'spaghetti'});
act(() => {
commentInstance.setState({muteType: 'spaghetti'});
});
expect(commentInstance.getMuteMessageInfo(justMuted).commentType).toBe('comment.type.general');
});
test('getMuteMessageInfo: muteType set to something we don\'t have messages for and already muted', () => {
const justMuted = false;
const commentInstance = getComposeCommentWrapper({}).instance();
commentInstance.setState({muteType: 'spaghetti'});
act(() => {
commentInstance.setState({muteType: 'spaghetti'});
});
expect(commentInstance.getMuteMessageInfo(justMuted).commentType).toBe('comment.type.general.past');
});
});

View file

@ -1,6 +1,7 @@
import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import {renderWithIntl} from '../../helpers/intl-helpers.jsx';
import DonateTopBanner from '../../../src/views/splash/donate/donate-banner';
import '@testing-library/jest-dom';
describe('DonateBannerTest', () => {
let realDateNow;
@ -13,25 +14,27 @@ describe('DonateBannerTest', () => {
});
test('Testing 2024 EOY campaign message', () => {
global.Date.now = () => new Date(2024, 11, 16).getTime();
const component = mountWithIntl(
const {container} = renderWithIntl(
<DonateTopBanner />
);
expect(component.find('div.donate-banner').exists()).toEqual(true);
expect(component.find('p.donate-text').exists()).toEqual(true);
expect(component.find('FormattedMessage[id="donatebanner.eoyCampaign"]').exists()).toEqual(true);
expect(component.find('FormattedMessage[id="donatebanner.askSupport"]').exists()).toEqual(false);
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');
});
test('testing default message comes back after January 9, 2025', () => {
// Date after Scratch week
global.Date.now = () => new Date(2025, 0, 10).getTime();
const component = mountWithIntl(
const {container} = renderWithIntl(
<DonateTopBanner />
);
expect(component.find('div.donate-banner').exists()).toEqual(true);
expect(component.find('p.donate-text').exists()).toEqual(true);
expect(component.find('FormattedMessage[id="donatebanner.askSupport"]').exists()).toEqual(true);
expect(component.find('FormattedMessage[id="donatebanner.eoyCampaign"]').exists()).toEqual(false);
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');
});
});

View file

@ -1,35 +1,37 @@
const React = require('react');
const {mountWithIntl} = require('../../helpers/intl-helpers.jsx');
const {renderWithIntl} = require('../../helpers/intl-helpers.jsx');
const EmailConfirmationBanner = require('../../../src/components/dropdown-banner/email-confirmation/banner.jsx');
const {fireEvent} = require('@testing-library/react');
require('@testing-library/jest-dom');
jest.mock('../../../src/components/modal/email-confirmation/modal.jsx', () => () => 'MockEmailConfirmationModal');
describe('EmailConfirmationBanner', () => {
test('Clicking "Confirm your email" opens the email confirmation modal', () => {
const component = mountWithIntl(
const {container} = renderWithIntl(
<EmailConfirmationBanner />
);
expect(component.text()).not.toContain('MockEmailConfirmationModal');
const confirmWrapper = component.find({id: 'emailConfirmationBanner.confirm'});
const confirmLink = mountWithIntl(confirmWrapper.props().values.confirmLink);
confirmLink.simulate('click');
component.update();
expect(container).not.toHaveTextContent('MockEmailConfirmationModal');
expect(component.text()).toContain('MockEmailConfirmationModal');
const confirmLink = container.querySelector('a.showEmailConfirmationModalLink');
fireEvent.click(confirmLink);
expect(container).toHaveTextContent('MockEmailConfirmationModal');
});
test('Clicking X calls onRequestDismiss', () => {
const requestDismissMock = jest.fn();
const component = mountWithIntl(
const {container} = renderWithIntl(
<EmailConfirmationBanner onRequestDismiss={requestDismissMock} />
);
component.find('a.close').simulate('click', {preventDefault () {}});
const closeButton = container.querySelector('a.close');
fireEvent.click(closeButton);
expect(requestDismissMock).toHaveBeenCalled();
});
});

View file

@ -1,7 +1,10 @@
const React = require('react');
const {mountWithIntl} = require('../../helpers/intl-helpers.jsx');
const {renderWithIntl} = require('../../helpers/intl-helpers.jsx');
const EmailConfirmationModal = require('../../../src/components/modal/email-confirmation/modal.jsx');
import {Provider} from 'react-redux';
import configureStore from 'redux-mock-store';
import '@testing-library/jest-dom';
import {fireEvent, screen} from '@testing-library/react';
describe('Modal', () => {
@ -23,33 +26,43 @@ describe('Modal', () => {
});
test('Display email prop correctly', () => {
const component = mountWithIntl(
<EmailConfirmationModal
isOpen
/>, {context: {store: defaultStore}}
renderWithIntl(
<Provider store={defaultStore}>
<EmailConfirmationModal
isOpen
/>
</Provider>
);
expect(component.find('div.modal-text-content').text()).toContain(testEmail);
expect(screen.getByText(testEmail)).toBeInTheDocument();
});
test('Clicking on Text changes to tips page', () => {
const component = mountWithIntl(
<EmailConfirmationModal
isOpen
/>, {email: testEmail, context: {store: defaultStore}}
renderWithIntl(
<Provider
store={defaultStore}
>
<EmailConfirmationModal
isOpen
/>
</Provider>
);
const tipsLinkWrapper = component.find({id: 'emailConfirmationModal.havingTrouble'});
const tipsLink = mountWithIntl(tipsLinkWrapper.props().values.tipsLink);
tipsLink.simulate('click');
expect(component.text()).toContain('emailConfirmationModal.confirmingTips');
const tipsLink = screen.getByText('Check out these tips');
fireEvent.click(tipsLink);
expect(screen.getByText('Tips for confirming your email address')).toBeInTheDocument();
});
test('Close button shows correctly', () => {
const component = mountWithIntl(
<EmailConfirmationModal isOpen />, {context: {store: defaultStore}}
test('Close button shows correctly', async () => {
renderWithIntl(
<Provider store={defaultStore}>
<EmailConfirmationModal
isOpen
/>
</Provider>
);
expect(component.find('div.modal-content-close').exists()).toBe(true);
expect(component.find('img.modal-content-close-img').exists()).toBe(true);
expect(await screen.findByRole('dialog')).toBeInTheDocument();
expect(screen.getByRole('img', {name: /close-icon/i})).toBeInTheDocument();
});
});