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);
Example #11
0
    }
  }

  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);
Example #12
0
                    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);
Example #14
0
          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;
Example #15
0
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);
Example #16
0
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);
Example #17
0
    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)));
Example #20
0
    }

    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);
Example #21
0
// 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;
Example #22
0
                  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;
Example #23
0
    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;
Example #24
0
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);
Example #25
0
                  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;
Example #28
0
  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);