Merge branch 'gatsby-hooks' into 'gatsby'

Gatsby hooks

See merge request javallone/regexper-static!43
This commit is contained in:
Jeff Avallone 2019-07-12 07:17:42 +00:00
commit 5ffeb90616
37 changed files with 406 additions and 815 deletions

View File

@ -1,6 +1,8 @@
const reactI18next = jest.requireActual('react-i18next'); const reactI18next = jest.requireActual('react-i18next');
const i18n = require('i18n');
module.exports = { module.exports = {
...reactI18next, ...reactI18next,
Trans: require('__mocks__/component-mock').buildMock(reactI18next.Trans) Trans: require('__mocks__/component-mock').buildMock(reactI18next.Trans),
useTranslation: () => ({ i18n, t: i18n.mockT })
}; };

View File

@ -3,7 +3,7 @@
exports[`App removing rendered expression 1`] = ` exports[`App removing rendered expression 1`] = `
<DocumentFragment> <DocumentFragment>
<span <span
data-component="withI18nextTranslation(Form)" data-component="Form"
data-props="{ data-props="{
\\"syntax\\": \\"js\\", \\"syntax\\": \\"js\\",
\\"expr\\": \\"test expression\\", \\"expr\\": \\"test expression\\",
@ -20,7 +20,7 @@ exports[`App removing rendered expression 1`] = `
}" }"
> >
<span <span
data-component="withI18nextTranslation(FormActions)" data-component="FormActions"
data-props="{}" data-props="{}"
/> />
</span> </span>
@ -36,7 +36,7 @@ exports[`App removing rendered expression 1`] = `
exports[`App removing rendered expression 2`] = ` exports[`App removing rendered expression 2`] = `
<DocumentFragment> <DocumentFragment>
<span <span
data-component="withI18nextTranslation(Form)" data-component="Form"
data-props="{ data-props="{
\\"syntax\\": \\"js\\", \\"syntax\\": \\"js\\",
\\"expr\\": \\"\\", \\"expr\\": \\"\\",
@ -58,7 +58,7 @@ exports[`App removing rendered expression 2`] = `
exports[`App rendering 1`] = ` exports[`App rendering 1`] = `
<DocumentFragment> <DocumentFragment>
<span <span
data-component="withI18nextTranslation(Form)" data-component="Form"
data-props="{ data-props="{
\\"syntax\\": \\"js\\", \\"syntax\\": \\"js\\",
\\"expr\\": \\"\\", \\"expr\\": \\"\\",
@ -80,7 +80,7 @@ exports[`App rendering 1`] = `
exports[`App rendering an expression 1`] = ` exports[`App rendering an expression 1`] = `
<DocumentFragment> <DocumentFragment>
<span <span
data-component="withI18nextTranslation(Form)" data-component="Form"
data-props="{ data-props="{
\\"syntax\\": \\"js\\", \\"syntax\\": \\"js\\",
\\"expr\\": \\"\\", \\"expr\\": \\"\\",
@ -102,7 +102,7 @@ exports[`App rendering an expression 1`] = `
exports[`App rendering an expression 2`] = ` exports[`App rendering an expression 2`] = `
<DocumentFragment> <DocumentFragment>
<span <span
data-component="withI18nextTranslation(Form)" data-component="Form"
data-props="{ data-props="{
\\"syntax\\": \\"js\\", \\"syntax\\": \\"js\\",
\\"expr\\": \\"test expression\\", \\"expr\\": \\"test expression\\",
@ -119,7 +119,7 @@ exports[`App rendering an expression 2`] = `
}" }"
/> />
<span <span
data-component="withI18nextTranslation(Loader)" data-component="Loader"
data-props="{}" data-props="{}"
/> />
</DocumentFragment> </DocumentFragment>
@ -128,7 +128,7 @@ exports[`App rendering an expression 2`] = `
exports[`App rendering an expression 3`] = ` exports[`App rendering an expression 3`] = `
<DocumentFragment> <DocumentFragment>
<span <span
data-component="withI18nextTranslation(Form)" data-component="Form"
data-props="{ data-props="{
\\"syntax\\": \\"js\\", \\"syntax\\": \\"js\\",
\\"expr\\": \\"test expression\\", \\"expr\\": \\"test expression\\",
@ -145,7 +145,7 @@ exports[`App rendering an expression 3`] = `
}" }"
> >
<span <span
data-component="withI18nextTranslation(FormActions)" data-component="FormActions"
data-props="{}" data-props="{}"
/> />
</span> </span>
@ -161,7 +161,7 @@ exports[`App rendering an expression 3`] = `
exports[`App rendering with an invalid syntax 1`] = ` exports[`App rendering with an invalid syntax 1`] = `
<DocumentFragment> <DocumentFragment>
<span <span
data-component="withI18nextTranslation(Form)" data-component="Form"
data-props="{ data-props="{
\\"syntax\\": \\"invalid\\", \\"syntax\\": \\"invalid\\",
\\"expr\\": \\"\\", \\"expr\\": \\"\\",
@ -183,7 +183,7 @@ exports[`App rendering with an invalid syntax 1`] = `
exports[`App rendering with an invalid syntax 2`] = ` exports[`App rendering with an invalid syntax 2`] = `
<DocumentFragment> <DocumentFragment>
<span <span
data-component="withI18nextTranslation(Form)" data-component="Form"
data-props="{ data-props="{
\\"syntax\\": \\"invalid\\", \\"syntax\\": \\"invalid\\",
\\"expr\\": \\"test expression\\", \\"expr\\": \\"test expression\\",
@ -200,7 +200,7 @@ exports[`App rendering with an invalid syntax 2`] = `
}" }"
/> />
<span <span
data-component="withI18nextTranslation(Loader)" data-component="Loader"
data-props="{}" data-props="{}"
/> />
</DocumentFragment> </DocumentFragment>
@ -209,7 +209,7 @@ exports[`App rendering with an invalid syntax 2`] = `
exports[`App rendering with an invalid syntax 3`] = ` exports[`App rendering with an invalid syntax 3`] = `
<DocumentFragment> <DocumentFragment>
<span <span
data-component="withI18nextTranslation(Form)" data-component="Form"
data-props="{ data-props="{
\\"syntax\\": \\"invalid\\", \\"syntax\\": \\"invalid\\",
\\"expr\\": \\"test expression\\", \\"expr\\": \\"test expression\\",

View File

@ -1,13 +1,15 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withTranslation, Trans } from 'react-i18next'; import { useTranslation, Trans } from 'react-i18next';
import ccLogo from './cc-by.svg'; import ccLogo from './cc-by.svg';
import style from './style.module.css'; import style from './style.module.css';
export const Footer = ({ t, buildId }) => ( export const Footer = ({ buildId }) => {
<footer className={ style.footer }> const { t } = useTranslation();
return <footer className={ style.footer }>
<ul className={ style.list }> <ul className={ style.list }>
<li> <li>
<Trans>Created by <a <Trans>Created by <a
@ -26,12 +28,11 @@ export const Footer = ({ t, buildId }) => (
<div className={ style.buildId }> <div className={ style.buildId }>
{ buildId } { buildId }
</div> </div>
</footer> </footer>;
); };
Footer.propTypes = { Footer.propTypes = {
t: PropTypes.func.isRequired,
buildId: PropTypes.string.isRequired buildId: PropTypes.string.isRequired
}; };
export default withTranslation()(Footer); export default Footer;

View File

@ -1,13 +1,12 @@
import React from 'react'; import React from 'react';
import { render } from 'react-testing-library'; import { render } from 'react-testing-library';
import { mockT } from 'i18n'; import Footer from 'components/Footer';
import { Footer } from 'components/Footer';
describe('Footer', () => { describe('Footer', () => {
test('rendering', () => { test('rendering', () => {
const { asFragment } = render( const { asFragment } = render(
<Footer buildId="abc-123" t={ mockT } /> <Footer buildId="abc-123" />
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });

View File

@ -1,86 +1,73 @@
import React from 'react'; import React, { useState, useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withTranslation, Trans } from 'react-i18next'; import { useTranslation, Trans } from 'react-i18next';
import ExpandIcon from 'react-feather/dist/icons/chevrons-down'; import ExpandIcon from 'react-feather/dist/icons/chevrons-down';
import style from './style.module.css'; import style from './style.module.css';
class Form extends React.PureComponent { const Form = ({ syntaxList, children, onSubmit, ...props }) => {
static propTypes = { const { t } = useTranslation();
expr: PropTypes.string, const [ expr, exprUpdate ] = useState(props.expr);
syntax: PropTypes.string, const [ syntax, syntaxUpdate ] = useState(props.syntax);
syntaxList: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
label: PropTypes.string
})),
onSubmit: PropTypes.func.isRequired,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]),
t: PropTypes.func.isRequired
}
state = { const handleExprChange = useCallback(event => {
expr: this.props.expr, exprUpdate(event.target.value);
syntax: this.props.syntax }, [exprUpdate]);
} const handleSyntaxChange = useCallback(event => {
syntaxUpdate(event.target.value);
handleSubmit = event => { }, [syntaxUpdate]);
const handleSubmit = useCallback(event => {
event.preventDefault(); event.preventDefault();
const { expr, syntax } = this.state; onSubmit({ expr, syntax });
}, [expr, syntax, onSubmit]);
this.props.onSubmit({ expr, syntax }); const handleKeyPress = useCallback(event => {
}
handleKeyPress = event => {
if (event.charCode === 13 && event.shiftKey) { if (event.charCode === 13 && event.shiftKey) {
this.handleSubmit(event); handleSubmit(event);
} }
} }, [handleSubmit]);
handleChange = event => this.setState({ return <div className={ style.form } data-requires-js>
[event.target.name]: event.target.value <form data-testid="form" onSubmit={ handleSubmit }>
}) <textarea
data-testid="expr-input"
name="expr"
value={ expr }
onKeyPress={ handleKeyPress }
onChange={ handleExprChange }
autoFocus
placeholder={ t('Enter regular expression to display') }></textarea>
<button type="submit"><Trans>Display</Trans></button>
<div className={ style.select }>
<select
data-testid="syntax-select"
name="syntax"
value={ syntax }
onChange={ handleSyntaxChange } >
{ syntaxList.map(({ id, label }) => (
<option value={ id } key={ id }>{ t(label) }</option>
)) }
</select>
<ExpandIcon />
</div>
{ children }
</form>
</div>;
};
render() { Form.propTypes = {
const { expr: PropTypes.string,
syntaxList, syntax: PropTypes.string,
children, syntaxList: PropTypes.arrayOf(PropTypes.shape({
t id: PropTypes.string,
} = this.props; label: PropTypes.string
const { expr, syntax } = this.state; })),
onSubmit: PropTypes.func.isRequired,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
])
};
return <div className={ style.form } data-requires-js> export default Form;
<form data-testid="form" onSubmit={ this.handleSubmit }>
<textarea
data-testid="expr-input"
name="expr"
value={ expr }
onKeyPress={ this.handleKeyPress }
onChange={ this.handleChange }
autoFocus
placeholder={ t('Enter regular expression to display') }></textarea>
<button type="submit"><Trans>Display</Trans></button>
<div className={ style.select }>
<select
data-testid="syntax-select"
name="syntax"
value={ syntax }
onChange={ this.handleChange } >
{ syntaxList.map(({ id, label }) => (
<option value={ id } key={ id }>{ t(label) }</option>
)) }
</select>
<ExpandIcon />
</div>
{ children }
</form>
</div>;
}
}
export { Form };
export default withTranslation()(Form);

View File

@ -5,14 +5,13 @@ jest.mock('react-feather/dist/icons/chevrons-down', () =>
import React from 'react'; import React from 'react';
import { render, fireEvent } from 'react-testing-library'; import { render, fireEvent } from 'react-testing-library';
import { mockT } from 'i18n'; import Form from 'components/Form';
import { Form } from 'components/Form';
const syntaxList = [ const syntaxList = [
{ id: 'testJS', label: 'Testing JS' }, { id: 'testJS', label: 'Testing JS' },
{ id: 'other', label: 'Other' } { id: 'other', label: 'Other' }
]; ];
const commonProps = { syntaxList, t: mockT }; const commonProps = { syntaxList };
describe('Form', () => { describe('Form', () => {
test('rendering', () => { test('rendering', () => {

View File

@ -8,168 +8,6 @@ exports[`FormActions rendering 1`] = `
</DocumentFragment> </DocumentFragment>
`; `;
exports[`FormActions rendering download links 1`] = `
<DocumentFragment>
<ul
class="actions"
>
<li>
<a
download="image.png"
href="http://example.com/image.png"
type="image/png"
>
<span
data-component="Download"
data-props="{}"
/>
TRANSLATE(Example PNG Link)
</a>
</li>
<li>
<a
download="image.svg"
href="http://example.com/image.svg"
type="image/svg+xml"
>
<span
data-component="Download"
data-props="{}"
/>
TRANSLATE(Example SVG Link)
</a>
</li>
</ul>
</DocumentFragment>
`;
exports[`FormActions rendering download links with data after mounting 1`] = `
<DocumentFragment>
<ul
class="actions"
>
<li>
<a
href="http://example.com"
>
<span
data-component="Link"
data-props="{}"
/>
<span
data-component="Trans"
data-props="{}"
>
Permalink
</span>
</a>
</li>
</ul>
</DocumentFragment>
`;
exports[`FormActions rendering download links with data after mounting 2`] = `
<DocumentFragment>
<ul
class="actions"
>
<li>
<a
download="image.png"
href="http://example.com/image.png"
type="image/png"
>
<span
data-component="Download"
data-props="{}"
/>
TRANSLATE(Example PNG Link)
</a>
</li>
<li>
<a
download="image.svg"
href="http://example.com/image.svg"
type="image/svg+xml"
>
<span
data-component="Download"
data-props="{}"
/>
TRANSLATE(Example SVG Link)
</a>
</li>
<li>
<a
href="http://example.com"
>
<span
data-component="Link"
data-props="{}"
/>
<span
data-component="Trans"
data-props="{}"
>
Permalink
</span>
</a>
</li>
</ul>
</DocumentFragment>
`;
exports[`FormActions rendering download links with data after mounting 3`] = `
<DocumentFragment>
<ul
class="actions"
>
<li>
<a
download="image.png"
href="http://example.com/image.png"
type="image/png"
>
<span
data-component="Download"
data-props="{}"
/>
TRANSLATE(Example PNG Link)
</a>
</li>
<li>
<a
download="image.svg"
href="http://example.com/image.svg"
type="image/svg+xml"
>
<span
data-component="Download"
data-props="{}"
/>
TRANSLATE(Example SVG Link)
</a>
</li>
<li>
<a
href="http://example.com"
>
<span
data-component="Link"
data-props="{}"
/>
<span
data-component="Trans"
data-props="{}"
>
Permalink
</span>
</a>
</li>
</ul>
</DocumentFragment>
`;
exports[`FormActions rendering with a permalink 1`] = ` exports[`FormActions rendering with a permalink 1`] = `
<DocumentFragment> <DocumentFragment>
<ul <ul

View File

@ -1,6 +1,6 @@
import React from 'react'; import React, { useState, useCallback, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withTranslation, Trans } from 'react-i18next'; import { useTranslation, Trans } from 'react-i18next';
import DownloadIcon from 'react-feather/dist/icons/download'; import DownloadIcon from 'react-feather/dist/icons/download';
import LinkIcon from 'react-feather/dist/icons/link'; import LinkIcon from 'react-feather/dist/icons/link';
@ -9,89 +9,52 @@ import style from './style.module.css';
import { createPngLink, createSvgLink } from './links'; import { createPngLink, createSvgLink } from './links';
class FormActions extends React.PureComponent { const downloadLink = (link, t) => {
static propTypes = { const { url, filename, type, label } = link;
permalinkUrl: PropTypes.string, return <li>
imageDetails: PropTypes.shape({ <a href={ url } download={ filename } type={ type }>
svg: PropTypes.string, <DownloadIcon />{ t(label) }
width: PropTypes.number, </a>
height: PropTypes.number </li>;
}), };
t: PropTypes.func.isRequired
}
state = { const FormActions = ({
svgLink: null, permalinkUrl,
pngLink: null imageDetails
} }) => {
const { t } = useTranslation();
const [svgLink, setSvgLink] = useState(null);
const [pngLink, setPngLink] = useState(null);
componentDidMount() { const generateDownloadLinks = useCallback(async () => {
const { imageDetails } = this.props; const { svg, width, height } = imageDetails;
setSvgLink(await createSvgLink({ svg }));
setPngLink(await createPngLink({ svg, width, height }));
}, [setSvgLink, setPngLink, imageDetails]);
useEffect(() => {
if (imageDetails && imageDetails.svg) { if (imageDetails && imageDetails.svg) {
this.generateDownloadLinks(); generateDownloadLinks();
} }
} }, [imageDetails]);
componentDidUpdate(prevProps) { return <ul className={ style.actions }>
const { imageDetails } = this.props; { pngLink && downloadLink(pngLink, t) }
const { imageDetails: prevImageDetails } = prevProps; { svgLink && downloadLink(svgLink, t) }
{ permalinkUrl && <li>
<a href={ permalinkUrl }><LinkIcon /><Trans>Permalink</Trans></a>
</li> }
</ul>;
};
if (!imageDetails) { FormActions.propTypes = {
this.setState({ svgLink: null, pngLink: null }); permalinkUrl: PropTypes.string,
return; imageDetails: PropTypes.shape({
} svg: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number
})
};
if (!prevImageDetails) { export default FormActions;
this.generateDownloadLinks();
return;
}
if (imageDetails.svg !== prevImageDetails.svg
|| imageDetails.width !== prevImageDetails.width
|| imageDetails.height !== prevImageDetails.height) {
this.generateDownloadLinks();
return;
}
}
async generateDownloadLinks() {
const { imageDetails: { svg, width, height } } = this.props;
this.setState({
svgLink: await createSvgLink({ svg }),
pngLink: await createPngLink({ svg, width, height })
});
}
downloadLink({ url, filename, type, label }) {
const { t } = this.props;
return <li>
<a href={ url } download={ filename } type={ type }>
<DownloadIcon />{ t(label) }
</a>
</li>;
}
render() {
const {
permalinkUrl
} = this.props;
const {
svgLink,
pngLink
} = this.state;
return <ul className={ style.actions }>
{ pngLink && this.downloadLink(pngLink) }
{ svgLink && this.downloadLink(svgLink) }
{ permalinkUrl && <li>
<a href={ permalinkUrl }><LinkIcon /><Trans>Permalink</Trans></a>
</li> }
</ul>;
}
}
export { FormActions };
export default withTranslation()(FormActions);

View File

@ -9,8 +9,7 @@ jest.mock('react-feather/dist/icons/link', () =>
import React from 'react'; import React from 'react';
import { render } from 'react-testing-library'; import { render } from 'react-testing-library';
import { mockT } from 'i18n'; import FormActions from 'components/FormActions';
import { FormActions } from 'components/FormActions';
import { createPngLink, createSvgLink } from './links'; import { createPngLink, createSvgLink } from './links';
createPngLink.mockResolvedValue({ createPngLink.mockResolvedValue({
@ -29,68 +28,15 @@ createSvgLink.mockResolvedValue({
describe('FormActions', () => { describe('FormActions', () => {
test('rendering', () => { test('rendering', () => {
const { asFragment } = render( const { asFragment } = render(
<FormActions t={ mockT } /> <FormActions/>
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
test('rendering with a permalink', () => { test('rendering with a permalink', () => {
const { asFragment } = render( const { asFragment } = render(
<FormActions permalinkUrl="http://example.com" t={ mockT } /> <FormActions permalinkUrl="http://example.com" />
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
test('rendering download links', async () => {
const imageDetails = {
svg: 'test image',
width: 10,
height: 20
};
const { asFragment } = render(
<FormActions imageDetails={ imageDetails } t={ mockT } />
);
// Give a beat for mocked promises to resolve
await new Promise(resolve => setTimeout(resolve));
expect(asFragment()).toMatchSnapshot();
});
test('rendering download links with data after mounting', async () => {
const { asFragment, rerender } = render(
<FormActions t={ mockT } />
);
rerender(
<FormActions permalinkUrl="http://example.com" t={ mockT } />
);
expect(asFragment()).toMatchSnapshot();
rerender(
<FormActions
permalinkUrl="http://example.com"
imageDetails={ { svg: 'test-image' } }
t={ mockT } />
);
// Give a beat for mocked promises to resolve
await new Promise(resolve => setTimeout(resolve));
expect(asFragment()).toMatchSnapshot();
rerender(
<FormActions
permalinkUrl="http://example.com"
imageDetails={ { svg: 'test-image', width: 10, height: 20 } }
t={ mockT } />
);
// Give a beat for mocked promises to resolve
await new Promise(resolve => setTimeout(resolve));
expect(asFragment()).toMatchSnapshot();
});
}); });

View File

@ -9,7 +9,7 @@ exports[`Header opening the Privacy Policy modal 1`] = `
}" }"
> >
<span <span
data-component="withI18nextTranslation(PrivacyPolicy)" data-component="PrivacyPolicy"
data-props="{}" data-props="{}"
/> />
</span> </span>
@ -59,7 +59,7 @@ exports[`Header opening the Privacy Policy modal 1`] = `
</li> </li>
<li> <li>
<span <span
data-component="withI18nextTranslation(InstallPrompt)" data-component="InstallPrompt"
data-props="{}" data-props="{}"
/> />
</li> </li>
@ -67,7 +67,7 @@ exports[`Header opening the Privacy Policy modal 1`] = `
data-requires-js="true" data-requires-js="true"
> >
<span <span
data-component="withI18nextTranslation(LocaleSwitcher)" data-component="LocaleSwitcher"
data-props="{}" data-props="{}"
/> />
</li> </li>
@ -85,7 +85,7 @@ exports[`Header opening the Privacy Policy modal while holding alt key 1`] = `
}" }"
> >
<span <span
data-component="withI18nextTranslation(PrivacyPolicy)" data-component="PrivacyPolicy"
data-props="{}" data-props="{}"
/> />
</span> </span>
@ -135,7 +135,7 @@ exports[`Header opening the Privacy Policy modal while holding alt key 1`] = `
</li> </li>
<li> <li>
<span <span
data-component="withI18nextTranslation(InstallPrompt)" data-component="InstallPrompt"
data-props="{}" data-props="{}"
/> />
</li> </li>
@ -143,7 +143,7 @@ exports[`Header opening the Privacy Policy modal while holding alt key 1`] = `
data-requires-js="true" data-requires-js="true"
> >
<span <span
data-component="withI18nextTranslation(LocaleSwitcher)" data-component="LocaleSwitcher"
data-props="{}" data-props="{}"
/> />
</li> </li>
@ -161,7 +161,7 @@ exports[`Header opening the Privacy Policy modal while holding ctrl key 1`] = `
}" }"
> >
<span <span
data-component="withI18nextTranslation(PrivacyPolicy)" data-component="PrivacyPolicy"
data-props="{}" data-props="{}"
/> />
</span> </span>
@ -211,7 +211,7 @@ exports[`Header opening the Privacy Policy modal while holding ctrl key 1`] = `
</li> </li>
<li> <li>
<span <span
data-component="withI18nextTranslation(InstallPrompt)" data-component="InstallPrompt"
data-props="{}" data-props="{}"
/> />
</li> </li>
@ -219,7 +219,7 @@ exports[`Header opening the Privacy Policy modal while holding ctrl key 1`] = `
data-requires-js="true" data-requires-js="true"
> >
<span <span
data-component="withI18nextTranslation(LocaleSwitcher)" data-component="LocaleSwitcher"
data-props="{}" data-props="{}"
/> />
</li> </li>
@ -237,7 +237,7 @@ exports[`Header opening the Privacy Policy modal while holding meta key 1`] = `
}" }"
> >
<span <span
data-component="withI18nextTranslation(PrivacyPolicy)" data-component="PrivacyPolicy"
data-props="{}" data-props="{}"
/> />
</span> </span>
@ -287,7 +287,7 @@ exports[`Header opening the Privacy Policy modal while holding meta key 1`] = `
</li> </li>
<li> <li>
<span <span
data-component="withI18nextTranslation(InstallPrompt)" data-component="InstallPrompt"
data-props="{}" data-props="{}"
/> />
</li> </li>
@ -295,7 +295,7 @@ exports[`Header opening the Privacy Policy modal while holding meta key 1`] = `
data-requires-js="true" data-requires-js="true"
> >
<span <span
data-component="withI18nextTranslation(LocaleSwitcher)" data-component="LocaleSwitcher"
data-props="{}" data-props="{}"
/> />
</li> </li>
@ -313,7 +313,7 @@ exports[`Header opening the Privacy Policy modal while holding shift key 1`] = `
}" }"
> >
<span <span
data-component="withI18nextTranslation(PrivacyPolicy)" data-component="PrivacyPolicy"
data-props="{}" data-props="{}"
/> />
</span> </span>
@ -363,7 +363,7 @@ exports[`Header opening the Privacy Policy modal while holding shift key 1`] = `
</li> </li>
<li> <li>
<span <span
data-component="withI18nextTranslation(InstallPrompt)" data-component="InstallPrompt"
data-props="{}" data-props="{}"
/> />
</li> </li>
@ -371,7 +371,7 @@ exports[`Header opening the Privacy Policy modal while holding shift key 1`] = `
data-requires-js="true" data-requires-js="true"
> >
<span <span
data-component="withI18nextTranslation(LocaleSwitcher)" data-component="LocaleSwitcher"
data-props="{}" data-props="{}"
/> />
</li> </li>
@ -389,7 +389,7 @@ exports[`Header rendering 1`] = `
}" }"
> >
<span <span
data-component="withI18nextTranslation(PrivacyPolicy)" data-component="PrivacyPolicy"
data-props="{}" data-props="{}"
/> />
</span> </span>
@ -440,7 +440,7 @@ exports[`Header rendering 1`] = `
</li> </li>
<li> <li>
<span <span
data-component="withI18nextTranslation(InstallPrompt)" data-component="InstallPrompt"
data-props="{}" data-props="{}"
/> />
</li> </li>
@ -448,7 +448,7 @@ exports[`Header rendering 1`] = `
data-requires-js="true" data-requires-js="true"
> >
<span <span
data-component="withI18nextTranslation(LocaleSwitcher)" data-component="LocaleSwitcher"
data-props="{}" data-props="{}"
/> />
</li> </li>
@ -466,7 +466,7 @@ exports[`Header rendering with no banner 1`] = `
}" }"
> >
<span <span
data-component="withI18nextTranslation(PrivacyPolicy)" data-component="PrivacyPolicy"
data-props="{}" data-props="{}"
/> />
</span> </span>
@ -516,7 +516,7 @@ exports[`Header rendering with no banner 1`] = `
</li> </li>
<li> <li>
<span <span
data-component="withI18nextTranslation(InstallPrompt)" data-component="InstallPrompt"
data-props="{}" data-props="{}"
/> />
</li> </li>
@ -524,7 +524,7 @@ exports[`Header rendering with no banner 1`] = `
data-requires-js="true" data-requires-js="true"
> >
<span <span
data-component="withI18nextTranslation(LocaleSwitcher)" data-component="LocaleSwitcher"
data-props="{}" data-props="{}"
/> />
</li> </li>

View File

@ -1,8 +1,8 @@
import React from 'react'; import React, { useState, useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Modal from 'react-modal'; import Modal from 'react-modal';
import { Link } from 'gatsby'; import { Link } from 'gatsby';
import { withTranslation, Trans } from 'react-i18next'; import { Trans } from 'react-i18next';
import GitlabIcon from 'react-feather/dist/icons/gitlab'; import GitlabIcon from 'react-feather/dist/icons/gitlab';
@ -12,76 +12,66 @@ import PrivacyPolicy from 'components/PrivacyPolicy';
import style from './style.module.css'; import style from './style.module.css';
class Header extends React.PureComponent { const Header = ({ banner }) => {
state = { const [ showModal, updateShowModal] = useState(false);
showModal: false const handleClose = useCallback(() => {
} updateShowModal(false);
}, [updateShowModal]);
static propTypes = { const handleOpen = useCallback(event => {
banner: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.string
]).isRequired
}
handleOpen = event => {
if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) { if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) {
return; return;
} }
event.preventDefault(); event.preventDefault();
this.setState({ showModal: true }); updateShowModal(true);
} }, [updateShowModal]);
handleClose = () => { return <>
this.setState({ showModal: false }); <Modal
} isOpen={ showModal }
onRequestClose={ handleClose }>
<PrivacyPolicy onClose={ handleClose } />
</Modal>
<header
className={ style.header }
data-banner={ banner || null }>
<h1>
<Link to="/">Regexper</Link>
</h1>
render() { <ul className={ style.list }>
const { banner } = this.props; <li>
const { showModal } = this.state; <a href="https://gitlab.com/javallone/regexper-static"
rel="external noopener noreferrer"
target="_blank">
<GitlabIcon />
<Trans>Source on GitLab</Trans>
</a>
</li>
<li>
<Link to="/privacy"
data-testid="privacy-link"
onClick={ handleOpen }
>
<Trans>Privacy Policy</Trans>
</Link>
</li>
<li>
<InstallPrompt />
</li>
<li data-requires-js>
<LocaleSwitcher />
</li>
</ul>
</header>
</>;
};
return <> Header.propTypes = {
<Modal banner: PropTypes.oneOfType([
isOpen={ showModal } PropTypes.bool,
onRequestClose={ this.handleClose }> PropTypes.string
<PrivacyPolicy onClose={ this.handleClose } /> ]).isRequired
</Modal> };
<header
className={ style.header }
data-banner={ banner || null }>
<h1>
<Link to="/">Regexper</Link>
</h1>
<ul className={ style.list }> export default Header;
<li>
<a href="https://gitlab.com/javallone/regexper-static"
rel="external noopener noreferrer"
target="_blank">
<GitlabIcon />
<Trans>Source on GitLab</Trans>
</a>
</li>
<li>
<Link to="/privacy"
data-testid="privacy-link"
onClick={ this.handleOpen }
>
<Trans>Privacy Policy</Trans>
</Link>
</li>
<li>
<InstallPrompt />
</li>
<li data-requires-js>
<LocaleSwitcher />
</li>
</ul>
</header>
</>;
}
}
export { Header };
export default withTranslation()(Header);

View File

@ -12,7 +12,7 @@ jest.mock('components/PrivacyPolicy', () =>
import React from 'react'; import React from 'react';
import { render, fireEvent } from 'react-testing-library'; import { render, fireEvent } from 'react-testing-library';
import { Header } from 'components/Header'; import Header from 'components/Header';
describe('Header', () => { describe('Header', () => {
test('rendering', () => { test('rendering', () => {

View File

@ -1,41 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`InstallPrompt accepting install prompt 1`] = `
<DocumentFragment>
<a
data-testid="install"
href="#install"
>
<span
data-component="Trans"
data-props="{}"
>
Add to Home Screen
</span>
</a>
</DocumentFragment>
`;
exports[`InstallPrompt accepting install prompt 2`] = `<DocumentFragment />`;
exports[`InstallPrompt rejecting install prompt 1`] = `
<DocumentFragment>
<a
data-testid="install"
href="#install"
>
<span
data-component="Trans"
data-props="{}"
>
Add to Home Screen
</span>
</a>
</DocumentFragment>
`;
exports[`InstallPrompt rejecting install prompt 2`] = `<DocumentFragment />`;
exports[`InstallPrompt rendering 1`] = `<DocumentFragment />`; exports[`InstallPrompt rendering 1`] = `<DocumentFragment />`;
exports[`InstallPrompt rendering after an install prompt has been requested 1`] = `<DocumentFragment />`; exports[`InstallPrompt rendering after an install prompt has been requested 1`] = `<DocumentFragment />`;

View File

@ -1,30 +1,12 @@
import React from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { withTranslation, Trans } from 'react-i18next'; import { Trans } from 'react-i18next';
class InstallPrompt extends React.PureComponent { const InstallPrompt = () => {
state = { const [ installPrompt, updateInstallPrompt ] = useState(null);
installPrompt: null
}
componentDidMount() { const handleInstall = useCallback(async event => {
window.addEventListener('beforeinstallprompt', this.handleInstallPrompt);
}
componentWillUnmount() {
window.removeEventListener('beforeinstallprompt', this.handleInstallPrompt);
}
handleInstallPrompt = event => {
this.setState({
installPrompt: event
});
}
handleInstall = async event => {
event.preventDefault(); event.preventDefault();
const { installPrompt } = this.state;
try { try {
installPrompt.prompt(); installPrompt.prompt();
await installPrompt.userChoice; await installPrompt.userChoice;
@ -33,24 +15,27 @@ class InstallPrompt extends React.PureComponent {
// User cancelled install // User cancelled install
} }
this.setState({ installPrompt: null }); updateInstallPrompt(null);
}, [installPrompt, updateInstallPrompt]);
useEffect(() => {
window.addEventListener('beforeinstallprompt', updateInstallPrompt);
return () => {
window.removeEventListener('beforeinstallprompt', updateInstallPrompt);
};
});
if (!installPrompt) {
return null;
} }
render() { return <a href="#install"
const { installPrompt } = this.state; data-testid="install"
onClick={ handleInstall }
>
<Trans>Add to Home Screen</Trans>
</a>;
};
if (!installPrompt) { export default InstallPrompt;
return null;
}
return <a href="#install"
data-testid="install"
onClick={ this.handleInstall }
>
<Trans>Add to Home Screen</Trans>
</a>;
}
}
export { InstallPrompt };
export default withTranslation()(InstallPrompt);

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { render, fireEvent } from 'react-testing-library'; import { render, fireEvent } from 'react-testing-library';
import { InstallPrompt } from 'components/InstallPrompt'; import InstallPrompt from 'components/InstallPrompt';
describe('InstallPrompt', () => { describe('InstallPrompt', () => {
test('rendering', () => { test('rendering', () => {
@ -39,48 +39,4 @@ describe('InstallPrompt', () => {
'beforeinstallprompt', 'beforeinstallprompt',
expect.any(Function)); expect.any(Function));
}); });
test('accepting install prompt', async () => {
const { asFragment, getByTestId } = render(
<InstallPrompt />
);
const promptEvent = new Event('beforeinstallprompt');
promptEvent.prompt = jest.fn();
promptEvent.userChoice = Promise.resolve();
const clickEvent = new MouseEvent('click', { bubbles: true });
jest.spyOn(clickEvent, 'preventDefault');
fireEvent(window, promptEvent);
expect(asFragment()).toMatchSnapshot();
fireEvent(getByTestId('install'), clickEvent);
// Allow async code to run
await new Promise(resolve => setTimeout(resolve));
expect(clickEvent.preventDefault).toHaveBeenCalled();
expect(promptEvent.prompt).toHaveBeenCalled();
expect(asFragment()).toMatchSnapshot();
});
test('rejecting install prompt', async () => {
const { asFragment, getByTestId } = render(
<InstallPrompt />
);
const promptEvent = new Event('beforeinstallprompt');
promptEvent.prompt = jest.fn();
promptEvent.userChoice = Promise.reject();
const clickEvent = new MouseEvent('click', { bubbles: true });
jest.spyOn(clickEvent, 'preventDefault');
fireEvent(window, promptEvent);
expect(asFragment()).toMatchSnapshot();
fireEvent(getByTestId('install'), clickEvent);
// Allow async code to run
await new Promise(resolve => setTimeout(resolve));
expect(clickEvent.preventDefault).toHaveBeenCalled();
expect(promptEvent.prompt).toHaveBeenCalled();
expect(asFragment()).toMatchSnapshot();
});
}); });

View File

@ -8,7 +8,7 @@ exports[`Layout rendering 1`] = `
> >
<noscript /> <noscript />
<span <span
data-component="withI18nextTranslation(Header)" data-component="Header"
data-props="{ data-props="{
\\"banner\\": \\"Test Banner\\" \\"banner\\": \\"Test Banner\\"
}" }"
@ -20,7 +20,7 @@ exports[`Layout rendering 1`] = `
Example content Example content
</span> </span>
<span <span
data-component="withI18nextTranslation(Footer)" data-component="Footer"
data-props="{ data-props="{
\\"buildId\\": \\"test-buildid\\" \\"buildId\\": \\"test-buildid\\"
}" }"

View File

@ -1,21 +1,17 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next';
import { withTranslation } from 'react-i18next';
import LoaderIcon from 'react-feather/dist/icons/loader'; import LoaderIcon from 'react-feather/dist/icons/loader';
import style from './style.module.css'; import style from './style.module.css';
const Loader = ({ t }) => ( const Loader = () => {
<div className={ style.loader }> const { t } = useTranslation();
return <div className={ style.loader }>
<LoaderIcon /> <LoaderIcon />
<div className={ style.message }>{ t('Loading...') }</div> <div className={ style.message }>{ t('Loading...') }</div>
</div> </div>;
);
Loader.propTypes = {
t: PropTypes.func.isRequired
}; };
export { Loader }; export default Loader;
export default withTranslation()(Loader);

View File

@ -1,15 +1,14 @@
import React from 'react'; import React from 'react';
import { render } from 'react-testing-library'; import { render } from 'react-testing-library';
import { mockT } from 'i18n'; import Loader from 'components/Loader';
import { Loader } from 'components/Loader';
describe('Loader', () => { describe('Loader', () => {
test('rendering', () => { test('rendering', () => {
// Using full rendering here since styles for this depend on the structure // Using full rendering here since styles for this depend on the structure
// of the SVG. // of the SVG.
const { asFragment } = render( const { asFragment } = render(
<Loader t={ mockT } /> <Loader/>
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });

View File

@ -1,5 +1,5 @@
import React from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { withTranslation, Trans } from 'react-i18next'; import { Trans } from 'react-i18next';
import ExpandIcon from 'react-feather/dist/icons/chevrons-down'; import ExpandIcon from 'react-feather/dist/icons/chevrons-down';
@ -8,50 +8,40 @@ import i18n, { locales } from 'i18n';
import localeToAvailable from './locale-to-available'; import localeToAvailable from './locale-to-available';
import style from './style.module.css'; import style from './style.module.css';
export class LocaleSwitcher extends React.PureComponent { const LocaleSwitcher = () => {
state = { const [ current, updateCurrent ] = useState(localeToAvailable(
current: localeToAvailable( i18n.language || '',
i18n.language || '', locales.map(l => l.code),
locales.map(l => l.code), 'en'));
'en')
}
componentDidMount() { useEffect(() => {
i18n.on('languageChanged', this.handleLanguageChange); i18n.on('languageChanged', updateCurrent);
}
componentWillUnmount() { return () => {
i18n.off('languageChanged', this.handleLanguageChange); i18n.off('languageChanged', updateCurrent);
} };
});
handleSelectChange = ({ target }) => { const handleSelectChange = useCallback(({ target }) => {
i18n.changeLanguage(target.value); i18n.changeLanguage(target.value);
} });
handleLanguageChange = lang => { return <label>
this.setState({ current: lang }); <Trans>Language</Trans>
} <div className={ style.switcher }>
<select data-testid="language-select"
value={ current }
onChange={ handleSelectChange }
>
{ locales.map(locale => (
<option value={ locale.code } key={ locale.code }>
{ locale.name }
</option>
)) }
</select>
<ExpandIcon />
</div>
</label>;
};
render() { export default LocaleSwitcher;
const { current } = this.state;
return <label>
<Trans>Language</Trans>
<div className={ style.switcher }>
<select data-testid="language-select"
value={ current }
onChange={ this.handleSelectChange }
>
{ locales.map(locale => (
<option value={ locale.code } key={ locale.code }>
{ locale.name }
</option>
)) }
</select>
<ExpandIcon />
</div>
</label>;
}
}
export default withTranslation()(LocaleSwitcher);

View File

@ -3,10 +3,10 @@ jest.mock('react-feather/dist/icons/chevrons-down', () =>
'react-feather/dist/icons/chevrons-down')); 'react-feather/dist/icons/chevrons-down'));
import React from 'react'; import React from 'react';
import { render, fireEvent } from 'react-testing-library'; import { render, fireEvent, act } from 'react-testing-library';
import i18n from 'i18n'; import i18n from 'i18n';
import { LocaleSwitcher } from 'components/LocaleSwitcher'; import LocaleSwitcher from 'components/LocaleSwitcher';
// Ensure initial locale is always "en" during tests // Ensure initial locale is always "en" during tests
jest.mock('./locale-to-available', () => jest.fn(() => 'en')); jest.mock('./locale-to-available', () => jest.fn(() => 'en'));
@ -40,7 +40,11 @@ describe('LocaleSwitcher', () => {
); );
expect(getByTestId('language-select').value).toEqual('en'); expect(getByTestId('language-select').value).toEqual('en');
i18n.emit('languageChanged', 'other');
act(() => {
i18n.emit('languageChanged', 'other');
});
expect(getByTestId('language-select').value).toEqual('other'); expect(getByTestId('language-select').value).toEqual('other');
}); });

View File

@ -5,15 +5,15 @@ exports[`Metadata rendering 1`] = `
<span <span
data-component="HelmetWrapper" data-component="HelmetWrapper"
data-props="{ data-props="{
\\"htmlAttributes\\": { \\"title\\": \\"Regexper\\",
\\"lang\\": \\"test-lang\\" \\"htmlAttributes\\": {},
} \\"meta\\": [
{
\\"name\\": \\"description\\"
}
]
}" }"
> />
<title>
Regexper
</title>
</span>
</DocumentFragment> </DocumentFragment>
`; `;
@ -22,18 +22,15 @@ exports[`Metadata rendering with a title and description 1`] = `
<span <span
data-component="HelmetWrapper" data-component="HelmetWrapper"
data-props="{ data-props="{
\\"htmlAttributes\\": { \\"title\\": \\"Regexper - Testing\\",
\\"lang\\": \\"test-lang\\" \\"htmlAttributes\\": {},
} \\"meta\\": [
{
\\"name\\": \\"description\\",
\\"content\\": \\"Test description\\"
}
]
}" }"
> />
<title>
Regexper - Testing
</title>
<meta
content="Test description"
name="description"
/>
</span>
</DocumentFragment> </DocumentFragment>
`; `;

View File

@ -1,29 +1,29 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
class Metadata extends React.PureComponent { const Metadata = ({ title, description }) => {
static propTypes = { const { i18n } = useTranslation();
title: PropTypes.string, const helmetProps = {
description: PropTypes.string, title: title ? `Regexper - ${ title }` : 'Regexper',
i18n: PropTypes.shape({ htmlAttributes: {
language: PropTypes.string.isRequired
}).isRequired
}
render() {
const { title, description, i18n } = this.props;
const htmlAttributes = {
lang: i18n.language lang: i18n.language
}; },
meta: [
{
name: 'description',
content: description
}
]
};
return <Helmet htmlAttributes={ htmlAttributes }> return <Helmet { ...helmetProps }></Helmet>;
<title>{ title ? `Regexper - ${ title }` : 'Regexper' }</title> };
{ description && <meta name="description" content={ description } /> }
</Helmet>;
}
}
export { Metadata }; Metadata.propTypes = {
export default withTranslation()(Metadata); title: PropTypes.string,
description: PropTypes.string
};
export default Metadata;

View File

@ -9,16 +9,12 @@ jest.mock('react-helmet', () => {
import React from 'react'; import React from 'react';
import { render } from 'react-testing-library'; import { render } from 'react-testing-library';
import { Metadata } from 'components/Metadata'; import Metadata from 'components/Metadata';
const commonProps = {
i18n: { language: 'test-lang' }
};
describe('Metadata', () => { describe('Metadata', () => {
test('rendering', () => { test('rendering', () => {
const { asFragment } = render( const { asFragment } = render(
<Metadata { ...commonProps } /> <Metadata/>
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
@ -27,8 +23,7 @@ describe('Metadata', () => {
const { asFragment } = render( const { asFragment } = render(
<Metadata <Metadata
title="Testing" title="Testing"
description="Test description" description="Test description" />
{ ...commonProps } />
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });

View File

@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { useTranslation, Trans } from 'react-i18next';
import { withTranslation, Trans } from 'react-i18next';
import Message from 'components/Message'; import Message from 'components/Message';
export const PrivacyPolicy = ({ t, ...props }) => ( export const PrivacyPolicy = props => {
<Message type="info" heading={ t('Privacy Policy') } { ...props }> const { t } = useTranslation();
return <Message type="info" heading={ t('Privacy Policy') } { ...props }>
<Trans i18nKey="Privacy policy copy"> <Trans i18nKey="Privacy policy copy">
<p> <p>
Regexper and the tools used to create it are all open source. If you are Regexper and the tools used to create it are all open source. If you are
@ -44,11 +45,7 @@ export const PrivacyPolicy = ({ t, ...props }) => (
Regexper is not supported by ad revenue or sales of any kind. Regexper is not supported by ad revenue or sales of any kind.
</p> </p>
</Trans> </Trans>
</Message> </Message>;
);
PrivacyPolicy.propTypes = {
t: PropTypes.func.isRequired
}; };
export default withTranslation()(PrivacyPolicy); export default PrivacyPolicy;

View File

@ -4,13 +4,12 @@ jest.mock('components/Message', () =>
import React from 'react'; import React from 'react';
import { render } from 'react-testing-library'; import { render } from 'react-testing-library';
import { mockT } from 'i18n'; import PrivacyPolicy from 'components/PrivacyPolicy';
import { PrivacyPolicy } from 'components/PrivacyPolicy';
describe('PrivacyPolicy', () => { describe('PrivacyPolicy', () => {
test('rendering', () => { test('rendering', () => {
const { asFragment } = render( const { asFragment } = render(
<PrivacyPolicy onClose={ jest.fn() } t={ mockT } /> <PrivacyPolicy onClose={ jest.fn() } />
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useRef, useCallback, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import nodeTypes from 'rendering/types'; import nodeTypes from 'rendering/types';
@ -40,48 +40,40 @@ const render = (data, key) => {
</React.Fragment>; </React.Fragment>;
}; };
class Render extends React.PureComponent { const Render = ({ data, onRender }) => {
static propTypes = { const svgContainer = useRef();
data: PropTypes.object.isRequired,
onRender: PropTypes.func.isRequired
}
svgContainer = React.createRef() const provideSVGData = useCallback(() => {
if (!svgContainer.current) {
componentDidMount() {
this.provideSVGData();
}
componentDidUpdate() {
this.provideSVGData();
}
provideSVGData() {
if (!this.svgContainer.current) {
return; return;
} }
const svg = this.svgContainer.current.querySelector('svg'); const svg = svgContainer.current.querySelector('svg');
this.props.onRender({ onRender({
svg: svg.outerHTML, svg: svg.outerHTML,
width: Number(svg.getAttribute('width')), width: Number(svg.getAttribute('width')),
height: Number(svg.getAttribute('height')) height: Number(svg.getAttribute('height'))
}); });
} }, [svgContainer, onRender]);
render() { useEffect(() => {
const { data } = this.props; provideSVGData();
}, [provideSVGData]);
return <div className={ style.render } ref={ this.svgContainer }> return <div className={ style.render } ref={ svgContainer }>
{ render({ { render({
...data, ...data,
props: { props: {
...data.props, ...data.props,
onReflow: this.provideSVGData onReflow: provideSVGData
} }
}) } }) }
</div>; </div>;
} };
}
Render.propTypes = {
data: PropTypes.object.isRequired,
onRender: PropTypes.func.isRequired
};
export default Render; export default Render;

View File

@ -9,7 +9,7 @@ exports[`SentryBoundary error handling 1`] = `
exports[`SentryBoundary error handling 2`] = ` exports[`SentryBoundary error handling 2`] = `
<DocumentFragment> <DocumentFragment>
<span <span
data-component="withI18nextTranslation(SentryError)" data-component="SentryError"
data-props="{}" data-props="{}"
/> />
</DocumentFragment> </DocumentFragment>

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { withTranslation, Trans } from 'react-i18next'; import { useTranslation, Trans } from 'react-i18next';
import Message from 'components/Message'; import Message from 'components/Message';
@ -13,19 +12,17 @@ const reportError = event => {
} }
}; };
export const SentryError = ({ t }) => ( export const SentryError = () => {
<Message type="error" heading={ t('An error has occurred') }> const { t } = useTranslation();
return <Message type="error" heading={ t('An error has occurred') }>
<p> <p>
<Trans>This error has been logged. You may also <a <Trans>This error has been logged. You may also <a
href="#error-report" href="#error-report"
data-testid="error-report" data-testid="error-report"
onClick={ reportError }>fill out a report</a>.</Trans> onClick={ reportError }>fill out a report</a>.</Trans>
</p> </p>
</Message> </Message>;
);
SentryError.propTypes = {
t: PropTypes.func.isRequired
}; };
export default withTranslation()(SentryError); export default SentryError;

View File

@ -7,13 +7,12 @@ import React from 'react';
import { render, fireEvent } from 'react-testing-library'; import { render, fireEvent } from 'react-testing-library';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { mockT } from 'i18n'; import SentryError from 'components/SentryError';
import { SentryError } from 'components/SentryError';
describe('SentryError', () => { describe('SentryError', () => {
test('rendering', () => { test('rendering', () => {
const { asFragment } = render( const { asFragment } = render(
<SentryError t={ mockT }/> <SentryError/>
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
@ -22,7 +21,7 @@ describe('SentryError', () => {
test('fill out a report when an event has been logged', () => { test('fill out a report when an event has been logged', () => {
Sentry.lastEventId.mockReturnValue(1); Sentry.lastEventId.mockReturnValue(1);
const { getByTestId } = render( const { getByTestId } = render(
<SentryError t={ mockT } /> <SentryError/>
); );
const event = new MouseEvent('click', { bubbles: true }); const event = new MouseEvent('click', { bubbles: true });
jest.spyOn(event, 'preventDefault'); jest.spyOn(event, 'preventDefault');
@ -35,7 +34,7 @@ describe('SentryError', () => {
test('fill out a report when an event has not been logged', () => { test('fill out a report when an event has not been logged', () => {
Sentry.lastEventId.mockReturnValue(false); Sentry.lastEventId.mockReturnValue(false);
const { getByTestId } = render( const { getByTestId } = render(
<SentryError t={ mockT } /> <SentryError/>
); );
const event = new MouseEvent('click', { bubbles: true }); const event = new MouseEvent('click', { bubbles: true });
jest.spyOn(event, 'preventDefault'); jest.spyOn(event, 'preventDefault');

View File

@ -1,19 +1,18 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { useTranslation, Trans } from 'react-i18next';
import { withTranslation, Trans } from 'react-i18next';
import Metadata from 'components/Metadata'; import Metadata from 'components/Metadata';
import Message from 'components/Message'; import Message from 'components/Message';
export const ErrorPage = ({ t }) => <> export const ErrorPage = () => {
<Metadata title={ t('Page Not Found') } /> const { t } = useTranslation();
<Message type="error" heading={ t('404 Page Not Found') }>
<p><Trans>The page you have requested could not be found.</Trans></p>
</Message>
</>;
ErrorPage.propTypes = { return <>
t: PropTypes.func.isRequired <Metadata title={ t('Page Not Found') } />
<Message type="error" heading={ t('404 Page Not Found') }>
<p><Trans>The page you have requested could not be found.</Trans></p>
</Message>
</>;
}; };
export default withTranslation()(ErrorPage); export default ErrorPage;

View File

@ -6,13 +6,12 @@ jest.mock('components/Message', () =>
import React from 'react'; import React from 'react';
import { render } from 'react-testing-library'; import { render } from 'react-testing-library';
import { mockT } from 'i18n'; import ErrorPage from 'pages/404';
import { ErrorPage } from 'pages/404';
describe('Error Page', () => { describe('Error Page', () => {
test('rendering', () => { test('rendering', () => {
const { asFragment } = render( const { asFragment } = render(
<ErrorPage t={ mockT } /> <ErrorPage/>
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });

View File

@ -3,7 +3,7 @@
exports[`Error Page rendering 1`] = ` exports[`Error Page rendering 1`] = `
<DocumentFragment> <DocumentFragment>
<span <span
data-component="withI18nextTranslation(Metadata)" data-component="Metadata"
data-props="{ data-props="{
\\"title\\": \\"TRANSLATE(Page Not Found)\\" \\"title\\": \\"TRANSLATE(Page Not Found)\\"
}" }"

View File

@ -3,7 +3,7 @@
exports[`Index Page rendering 1`] = ` exports[`Index Page rendering 1`] = `
<DocumentFragment> <DocumentFragment>
<span <span
data-component="withI18nextTranslation(Metadata)" data-component="Metadata"
data-props="{ data-props="{
\\"description\\": \\"Test description\\" \\"description\\": \\"Test description\\"
}" }"
@ -33,7 +33,7 @@ exports[`Index Page rendering 1`] = `
exports[`Index Page rendering with an expression on the URL 1`] = ` exports[`Index Page rendering with an expression on the URL 1`] = `
<DocumentFragment> <DocumentFragment>
<span <span
data-component="withI18nextTranslation(Metadata)" data-component="Metadata"
data-props="{ data-props="{
\\"description\\": \\"Test description\\" \\"description\\": \\"Test description\\"
}" }"

View File

@ -3,13 +3,13 @@
exports[`Privacy Page rendering 1`] = ` exports[`Privacy Page rendering 1`] = `
<DocumentFragment> <DocumentFragment>
<span <span
data-component="withI18nextTranslation(Metadata)" data-component="Metadata"
data-props="{ data-props="{
\\"title\\": \\"TRANSLATE(Privacy Policy)\\" \\"title\\": \\"TRANSLATE(Privacy Policy)\\"
}" }"
/> />
<span <span
data-component="withI18nextTranslation(PrivacyPolicy)" data-component="PrivacyPolicy"
data-props="{}" data-props="{}"
/> />
</DocumentFragment> </DocumentFragment>

View File

@ -8,7 +8,7 @@ jest.mock('components/App', () =>
import React from 'react'; import React from 'react';
import { render } from 'react-testing-library'; import { render } from 'react-testing-library';
import { IndexPage } from 'pages/index'; import IndexPage from 'pages/index';
const queryResult = { const queryResult = {
site: { site: {

View File

@ -1,17 +1,16 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next';
import { withTranslation } from 'react-i18next';
import Metadata from 'components/Metadata'; import Metadata from 'components/Metadata';
import PrivacyPolicy from 'components/PrivacyPolicy'; import PrivacyPolicy from 'components/PrivacyPolicy';
export const PrivacyPage = ({ t }) => <> export const PrivacyPage = () => {
<Metadata title={ t('Privacy Policy') } /> const { t } = useTranslation();
<PrivacyPolicy />
</>;
PrivacyPage.propTypes = { return <>
t: PropTypes.func.isRequired <Metadata title={ t('Privacy Policy') } />
<PrivacyPolicy />
</>;
}; };
export default withTranslation()(PrivacyPage); export default PrivacyPage;

View File

@ -6,13 +6,12 @@ jest.mock('components/PrivacyPolicy', () =>
import React from 'react'; import React from 'react';
import { render } from 'react-testing-library'; import { render } from 'react-testing-library';
import { mockT } from 'i18n'; import PrivacyPage from 'pages/privacy';
import { PrivacyPage } from 'pages/privacy';
describe('Privacy Page', () => { describe('Privacy Page', () => {
test('rendering', () => { test('rendering', () => {
const { asFragment } = render( const { asFragment } = render(
<PrivacyPage t={ mockT } /> <PrivacyPage/>
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });