function renderWithStore({ id, store = createErrorStore(), decorator = withErrorHandler, customProps = {}, ShallowTarget = SomeComponentBase, ...options } = {}) { const ComponentWithErrorHandling = compose( translate(), decorator({ id, name: 'SomeComponent', ...options, }), )(SomeComponentBase); const props = { i18n: fakeI18n(), store, ...customProps, }; return shallowUntilTarget( <ComponentWithErrorHandling {...props} />, ShallowTarget, ); }
it('returns the wrapped instance when using withRef', () => { const Component = translate({ withRef: true })(InnerComponent); const root = render({ Component }); const wrappedComponent = findRenderedComponentWithType(root, Component); const component = wrappedComponent.getWrappedInstance(); expect(component).toBeInstanceOf(InnerComponent); });
it('throws an exception calling getWrappedInstance without withRef', () => { const Component = translate()(InnerComponent); const root = render({ Component }); const wrappedComponent = findRenderedComponentWithType(root, Component); expect(() => { wrappedComponent.getWrappedInstance(); }).toThrowError('To access the wrapped instance'); });
function renderMastHead({ ...props }) { const MyMastHead = translate({ withRef: true })(MastHeadBase); const initialState = { api: { clientApp: 'android', lang: 'en-GB' } }; return findRenderedComponentWithType(renderIntoDocument( <Provider store={createStore(initialState)}> <MyMastHead i18n={getFakeI18nInst()} {...props} /> </Provider> ), MyMastHead).getWrappedInstance(); }
function render({ Component = translate()(InnerComponent), i18n = getFakeI18nInst(), componentProps = {}, } = {}) { return renderIntoDocument( <I18nProvider i18n={i18n}> <OuterComponent> <Component {...componentProps} /> </OuterComponent> </I18nProvider> ); }
const render = ({ Component = translate()(InnerComponent), i18n = fakeI18n(), componentProps = {}, } = {}) => { return mount( <I18nProvider i18n={i18n}> <OuterComponent> <Component {...componentProps} /> </OuterComponent> </I18nProvider>, InnerComponent, ); };
function render({ ...customProps } = {}) { const props = { errorHandler: sinon.stub(), i18n: getFakeI18nInst(), apiState: signedInApiState, review: defaultReview, router: {}, updateReviewText: () => {}, ...customProps, }; const AddonReview = translate({ withRef: true })(AddonReviewBase); const root = findRenderedComponentWithType(renderIntoDocument( <AddonReview {...props} /> ), AddonReview); return root.getWrappedInstance(); }
it('creates a unique handler ID per component instance', () => { const SomeComponent = translate()(SomeComponentBase); const ComponentWithErrorHandling = withErrorHandler({ name: 'SomeComponent', })(SomeComponent); const getRenderedComponent = () => { return shallowUntilTarget( <ComponentWithErrorHandling store={createErrorStore()} />, SomeComponent, ); }; const component1 = getRenderedComponent(); const component2 = getRenderedComponent(); expect(component1.instance().props.errorHandler.id).not.toEqual( component2.instance().props.errorHandler.id, ); });
function render({ ...customProps } = {}) { const props = { addon: fakeAddon, apiState: signedInApiState, errorHandler: sinon.stub(), version: fakeAddon.current_version, i18n: getFakeI18nInst(), userId: 91234, submitReview: () => {}, loadSavedReview: () => {}, router: {}, ...customProps, }; const RatingManager = translate({ withRef: true })(RatingManagerBase); const root = findRenderedComponentWithType(renderIntoDocument( <RatingManager {...props} /> ), RatingManager); return root.getWrappedInstance(); }
<hr /> <section className="screenshots"> <h2>{i18n.gettext('Screenshots')}</h2> <ScreenShots /> </section> <hr /> <section className="about"> <h2>{i18n.gettext('About this extension')}</h2> <div dangerouslySetInnerHTML={sanitizeHTML(nl2br(addon.description), allowedDescriptionTags)} /> </section> <hr /> <section className="overall-rating"> <h2>{i18n.gettext('Rate your experience')}</h2> <OverallRating addonName={addon.name} addonId={addon.id} version={addon.current_version} /> </section> </div> ); } } export default translate({ withRef: true })(AddonDetail);
} } render() { const browsertheme = JSON.stringify(getThemeData(this.props)); const { handleChange, slug, status, ...otherProps } = this.props; if (!validStates.includes(status)) { throw new Error(`Invalid add-on status ${status}`); } const isChecked = [INSTALLED, INSTALLING, ENABLING, ENABLED].includes(status); const isDisabled = status === UNKNOWN; const isSuccess = [ENABLED, INSTALLED].includes(status); return ( <div data-browsertheme={browsertheme} ref={(el) => { this.themeData = el; }}> <Switch {...otherProps} checked={isChecked} disabled={isDisabled} progress={this.getDownloadProgress()} name={slug} success={isSuccess} label={this.getLabel()} onChange={handleChange} onClick={this.handleClick} ref={(el) => { this.switchEl = el; }} /> </div> ); } } export default translate()(InstallSwitchBase);
endForumLink: '</a>', }, ), ['a'], )} /> <p // eslint-disable-next-line react/no-danger dangerouslySetInnerHTML={sanitizeHTML( i18n.sprintf( i18n.gettext( `%(startLink)sInformation about how to contact Mozilla's add-ons team can be found here%(endLink)s.`, ), { startLink: '<a href="https://wiki.mozilla.org/Add-ons#Getting_in_touch">', endLink: '</a>', }, ), ['a'], )} /> </section> </div> </Card> ); } } export default translate()(AboutBase);
<div className="CategoryHeader-contents"> <h1 className="CategoryHeader-name"> {category ? category.name : <LoadingText />} </h1> <div className="CategoryHeader-description"> {category ? ( <p className="CategoryHeader-paragraph"> {category.description} </p> ) : ( <p className="CategoryHeader-paragraph"> <LoadingText /> <br /> <LoadingText /> </p> )} </div> </div> {icon && ( <div className="CategoryHeader-icon"> <CategoryIcon name={icon} color={color} /> </div> )} </div> </Card> ); } } export default compose(translate())(CategoryHeaderBase);
type="hostPermission" description={this.getPermissionString({ messageType: allUrlsMessageType, })} key="allUrls" />, ); } else { hostPermissions.push( ...this.generateHostPermissions({ permissions: uniqueWildcards, messageType: domainMessageType, }), ); hostPermissions.push( ...this.generateHostPermissions({ permissions: uniqueSites, messageType: siteMessageType, }), ); } return <React.Fragment>{hostPermissions}</React.Fragment>; } } const HostPermissions: React.ComponentType<Props> = compose(translate())( HostPermissionsBase, ); export default HostPermissions;
import React, { PropTypes } from 'react'; import translate from 'core/i18n/translate'; import 'amo/css/SearchBox.scss'; export class SearchBox extends React.Component { static propTypes = { i18n: PropTypes.object, } render() { const { i18n } = this.props; // This is just a placeholder. return ( <div className="SearchBox"> <span className="visually-hidden">{i18n.gettext('Search')}</span> </div> ); } } export default translate({ withRef: true })(SearchBox);
import React, { PropTypes } from 'react'; import Helmet from 'react-helmet'; import 'disco/css/App.scss'; import translate from 'core/i18n/translate'; export class AppBase extends React.Component { static propTypes = { children: PropTypes.node, i18n: PropTypes.object.isRequired, } render() { const { children, i18n } = this.props; return ( <div className="disco-pane"> <Helmet defaultTitle={i18n.gettext('Discover Add-ons')} meta={[ { name: 'robots', content: 'noindex' }, ]} /> {children} </div> ); } } export default translate({ withRef: true })(AppBase);
invariant(authorId, 'authorId is required'); invariant(name, 'name is required'); invariant(slug, 'slug is required'); invariant( numberOfAddons !== undefined && Number.isInteger(numberOfAddons), 'numberOfAddons must be a number', ); linkProps.to = `/collections/${authorId}/${slug}/`; numberText = i18n.sprintf( i18n.ngettext('%(total)s add-on', '%(total)s add-ons', numberOfAddons), { total: i18n.formatNumber(numberOfAddons) }, ); } return ( <li className="UserCollection" key={id}> <Link className="UserCollection-link" {...linkProps}> <h2 className="UserCollection-name">{name || <LoadingText />}</h2> <p className="UserCollection-number">{numberText || <LoadingText />}</p> </Link> </li> ); }; const UserCollection: React.ComponentType<Props> = compose(translate())( UserCollectionBase, ); export default UserCollection;
</h3> ) : ( <h3 className="SearchResult-rating visually-hidden"> {i18n.gettext('No ratings')} </h3> ); return ( <li className="SearchResult"> <Link to={`/${lang}/firefox/addon/${result.slug}/`} className="SearchResult-link" ref={(el) => { this.name = el; }}> <section className="SearchResult-main"> <img className="SearchResult-icon" src={result.icon_url} alt="" /> <h2 className="SearchResult-heading">{result.name}</h2> {rating} <h3 className="SearchResult-author">{result.authors[0].name}</h3> <h3 className="SearchResult-users">{i18n.sprintf( i18n.ngettext('%(users)s user', '%(users)s users', result.average_daily_users), { users: result.average_daily_users } )} </h3> </section> </Link> </li> ); } } export default translate({ withRef: true })(SearchResult);
return getDiscoveryAddons({ api: state.api }) .then(({ entities, result }) => { dispatch(loadEntities(entities)); dispatch(discoResults(result.results.map((r) => entities.discoResults[r]))); }); } export function mapStateToProps(state) { return { results: loadedAddons(state), showInfoDialog: state.infoDialog.show, infoDialogData: state.infoDialog.data, }; } export function mapDispatchToProps(dispatch, { _config = config } = {}) { if (_config.get('server')) { return {}; } return { handleGlobalEvent(payload) { dispatch({ type: INSTALL_STATE, payload }); }, }; } export default asyncConnect([{ key: 'DiscoPane', promise: loadDataIfNeeded, }])(connect(mapStateToProps, mapDispatchToProps)(translate()(DiscoPaneBase)));
} const isChecked = [INSTALLED, INSTALLING, ENABLING, ENABLED].includes(status); const isDisabled = status === UNKNOWN; const isDownloading = status === DOWNLOADING; const switchClasses = `switch ${status.toLowerCase()}`; const identifier = `install-button-${slug}`; return ( <div className={switchClasses} onClick={this.handleClick} data-download-progress={isDownloading ? downloadProgress : 0}> <input id={identifier} className="visually-hidden" checked={isChecked} disabled={isDisabled} onChange={this.props.handleChange} data-browsertheme={JSON.stringify(getThemeData(this.props))} ref={(ref) => { this.themeData = ref; }} type="checkbox" /> <label htmlFor={identifier}> {isDownloading ? <div className="progress" /> : null} <span className="visually-hidden">{this.getLabel()}</span> </label> </div> ); } } export default translate()(InstallButtonBase);
// Messages in the disco pane are a bit less specific as we don't care about // non-Firefox clients and the copy space is limited. export class AddonCompatibilityErrorBase extends React.Component<InternalProps> { render() { const { i18n, reason } = this.props; let message; if (reason === INCOMPATIBLE_FIREFOX_FOR_IOS) { message = i18n.gettext( 'Firefox for iOS does not currently support add-ons.', ); } else if (reason === INCOMPATIBLE_UNDER_MIN_VERSION) { message = i18n.gettext( 'This add-on does not support your version of Firefox.', ); } else { // Unknown reasons are fine on the Disco Pane because we don't // care about non-FF clients. message = i18n.gettext('This add-on does not support your browser.'); } return <div className="AddonCompatibilityError">{message}</div>; } } const AddonCompatibilityError: React.ComponentType<Props> = translate()( AddonCompatibilityErrorBase, ); export default AddonCompatibilityError;
href="%(url)s">filing an issue</a>. Tell us where you came from and what you were looking for, and we'll get it sorted.`), { url: 'https://github.com/mozilla/addons-frontend/issues/new/', }, ), ['a'], )} /> <p className="ErrorPage-paragraph-with-links"> {linkParts.first.beforeLinkText} <Link to={`/${visibleAddonType(ADDON_TYPE_EXTENSION)}/`}> {linkParts.first.innerLinkText} </Link> {linkParts.second.beforeLinkText} <Link to={`/${visibleAddonType(ADDON_TYPE_THEME)}/`}> {linkParts.second.innerLinkText} </Link> {linkParts.second.afterLinkText} </p> </Card> </NestedStatus> ); } } const NotFound: React.ComponentType<Props> = compose(translate())(NotFoundBase); export default NotFound;
const { addon, addonInstallSource } = this.props; const result = this.renderResult(); const resultClassnames = makeClassName('SearchResult', { 'SearchResult--theme': addon && isTheme(addon.type), 'SearchResult--persona': addon && addon.type === ADDON_TYPE_THEME, }); let item = result; if (addon) { let linkTo = `/addon/${addon.slug}/`; if (addonInstallSource) { linkTo = addQueryParams(linkTo, { src: addonInstallSource }); } item = ( <Link to={linkTo} className="SearchResult-link"> {result} </Link> ); } return <li className={resultClassnames}>{item}</li>; } } const SearchResult: React.ComponentType<Props> = compose(translate())( SearchResultBase, ); export default SearchResult;
export class SuggestedPagesBase extends React.Component<InternalProps> { render() { const { i18n } = this.props; return ( <section className="SuggestedPages"> <h2>{i18n.gettext('Suggested Pages')}</h2> <ul> <li> <Link to={`/${visibleAddonType(ADDON_TYPE_EXTENSION)}/`}> {i18n.gettext('Browse all extensions')} </Link> </li> <li className="SuggestedPages-link-themes"> <Link to={`/${visibleAddonType(ADDON_TYPE_THEME)}/`}> {i18n.gettext('Browse all themes')} </Link> </li> <li> <Link to="/">{i18n.gettext('Add-ons Home Page')}</Link> </li> </ul> </section> ); } } export default compose(translate())(SuggestedPagesBase);
styleName="small" /> </div> { addon.authors && addon.authors.length ? ( <h3 className="SearchResult-author SearchResult--meta-section"> {addon.authors[0].name} </h3> ) : null } </div> </div> <h3 className="SearchResult-users SearchResult--meta-section"> <Icon className="SearchResult-users-icon" name="user-fill" /> <span className="SearchResult-users-text"> {i18n.sprintf(i18n.ngettext( '%(total)s user', '%(total)s users', averageDailyUsers), { total: i18n.formatNumber(averageDailyUsers) }, )} </span> </h3> </Link> </li> ); /* eslint-enable react/no-danger */ } } export default compose( translate({ withRef: true }), )(SearchResultBase);
} return ( <ul className={makeClassName('ErrorList', className)}> {items.map((item, index) => { return ( <li className="ErrorList-item" // We don't have message IDs but it's safe to rely on // array indices since they are returned from the API // in a predictable order. // eslint-disable-next-line react/no-array-index-key key={`erroritem-${index}`} > <Notice type="error" actionOnClick={action} actionText={actionText} > {item} </Notice> </li> ); })} </ul> ); } } export default compose(translate())(ErrorListBase);
let deletingReview = false; let editingReview = false; let replyingToReview = false; let submittingReply = false; if (ownProps.review) { const view = state.reviews.view[ownProps.review.id]; if (view) { deletingReview = view.deletingReview; editingReview = view.editingReview; replyingToReview = view.replyingToReview; submittingReply = view.submittingReply; } } return { deletingReview, editingReview, replyingToReview, siteUser: getCurrentUser(state.users), siteUserHasReplyPerm: hasPermission(state, ADDONS_EDIT), submittingReply, }; } const AddonReviewCard: React.ComponentType<Props> = compose( connect(mapStateToProps), withErrorHandler({ name: 'AddonReviewCard' }), translate(), )(AddonReviewCardBase); export default AddonReviewCard;
if (addon && isTheme(addon.type)) { const label = i18n.sprintf(i18n.gettext('Preview of %(title)s'), { title: addon.name, }); let previewURL = getPreviewImage(addon, { useStandardSize }); if (!previewURL && addon.type === ADDON_TYPE_THEME) { invariant(addon.themeData, 'themeData is required'); previewURL = addon.themeData.previewURL; } return ( <div className={makeClassName('ThemeImage', { 'ThemeImage--rounded-corners': roundedCorners, })} role="presentation" > <img alt={label} className="ThemeImage-image" src={previewURL} /> </div> ); } return null; }; const ThemeImage: React.ComponentType<Props> = translate()(ThemeImageBase); export default ThemeImage;
// TODO: Offer a sign in link/button inside the error page. /* eslint-disable react/no-danger */ return ( <NestedStatus code={401}> <Card className="ErrorPage NotAuthorized" header={i18n.gettext('Not Authorized')} > <p> {i18n.gettext(` Sorry, but you aren't authorized to access this page. If you aren't signed in, try signing in using the link at the top of the page.`)} </p> <SuggestedPages /> <p dangerouslySetInnerHTML={sanitizeHTML(fileAnIssueText, ['a'])} /> </Card> </NestedStatus> ); /* eslint-enable react/no-danger */ } } const NotAuthorized: React.ComponentType<Props> = compose(translate())( NotAuthorizedBase, ); export default NotAuthorized;
import './styles.scss'; export class FooterBase extends React.Component { static propTypes = { i18n: PropTypes.object.isRequired, }; render() { const { i18n } = this.props; return ( <footer className="Footer"> <a className="Footer-privacy-link" href={`https://www.mozilla.org/privacy/websites/${makeQueryStringWithUTM( { utm_content: 'privacy-policy-link', }, )}`} rel="noopener noreferrer" target="_blank" > {i18n.gettext('Privacy Policy')} </a> </footer> ); } } export default compose(translate())(FooterBase);