it('dispatches when the send button is clicked if textarea has text', () => {
    const addon = { ...fakeAddon, slug: 'which-browser' };
    const fakeEvent = createFakeEvent();
    const { store } = dispatchClientMetadata();
    const dispatchSpy = sinon.spy(store, 'dispatch');
    const root = renderMount({ addon, store });

    // This simulates entering text into the textarea.
    const textarea = root.find('.ReportAbuseButton-textarea > textarea');
    textarea.instance().value = 'Opera did it first!';
    textarea.simulate('change');

    root
      .find('button.ReportAbuseButton-send-report')
      .simulate('click', fakeEvent);
    sinon.assert.calledWith(
      dispatchSpy,
      sendAddonAbuseReport({
        addonSlug: addon.slug,
        errorHandlerId: 'create-stub-error-handler-id',
        message: 'Opera did it first!',
      }),
    );
    sinon.assert.called(fakeEvent.preventDefault);
  });
    it('shows an empty notes form when the leave a note button is clicked', () => {
      const { store } = dispatchClientMetadata();
      const root = render({ store });

      expect(root.find('.EditableCollectionAddon-notes')).toHaveLength(0);

      root
        .find('.EditableCollectionAddon-leaveNote-button')
        .simulate('click', createFakeEvent());
      applyUIStateChanges({ root, store });

      expect(root.find('.EditableCollectionAddon-notes')).toHaveLength(1);

      const notesForm = root.find('.EditableCollectionAddon-notes-form');
      expect(notesForm).toHaveLength(1);
      expect(notesForm).toHaveProp('microButtons', true);
      expect(notesForm).toHaveProp('onDelete', null);
      expect(notesForm).toHaveProp(
        'onDismiss',
        root.instance().onDismissNoteForm,
      );
      expect(notesForm).toHaveProp('onSubmit', root.instance().onSaveNote);
      expect(notesForm).toHaveProp(
        'placeholder',
        'Add a comment about this add-on.',
      );
      expect(notesForm).toHaveProp('submitButtonText', 'Save');
      expect(notesForm).toHaveProp('text', null);

      // The read-only portion should not be shown.
      expect(
        root.find('.EditableCollectionAddon-notes-read-only'),
      ).toHaveLength(0);
    });
  it('passes review=null if no user is signed in', () => {
    const { store } = dispatchClientMetadata();
    const root = render({ store });

    // This does not trigger a loading state.
    expect(root.find(UserRating)).toHaveProp('review', null);
  });
Example #4
0
    it('calls the report add-on abuse API', async () => {
      const apiState = dispatchClientMetadata().store.getState().api;
      const message = 'I do not like this!';
      const user = createUserAccountResponse({ id: 5001 });

      mockApi
        .expects('callApi')
        .withArgs({
          auth: true,
          endpoint: 'abuse/report/user',
          method: 'POST',
          body: { message, user: '5001' },
          apiState,
        })
        .once()
        .returns(mockResponse({ message, user }));

      await reportUser({
        api: apiState,
        message,
        userId: user.id,
      });

      mockApi.verify();
    });
    it('does not fetch user collections when signed out', () => {
      dispatchClientMetadata({ store });
      const dispatchSpy = sinon.spy(store, 'dispatch');
      render();

      sinon.assert.notCalled(dispatchSpy);
    });
Example #6
0
    it('loads collections', () => {
      const { store } = dispatchClientMetadata();

      _loadHomeAddons({
        store,
        collections: [
          createFakeCollectionAddonsListResponse({
            addons: Array(10).fill(createFakeCollectionAddon()),
          }),
        ],
      });

      const homeState = store.getState().home;

      expect(homeState.resultsLoaded).toEqual(true);
      expect(homeState.collections).toHaveLength(1);
      expect(homeState.collections[0]).toHaveLength(
        LANDING_PAGE_EXTENSION_COUNT,
      );
      expect(homeState.collections[0]).toEqual(
        Array(LANDING_PAGE_EXTENSION_COUNT).fill(
          createInternalAddon(fakeAddon),
        ),
      );
    });
Example #7
0
    it('calls the report add-on abuse API', async () => {
      const apiState = dispatchClientMetadata().store.getState().api;
      const message = 'I do not like this!';

      mockApi
        .expects('callApi')
        .withArgs({
          auth: true,
          endpoint: 'abuse/report/addon',
          method: 'POST',
          body: { addon: 'cool-addon', message },
          apiState,
        })
        .once()
        .returns(
          mockResponse({
            addon: { ...fakeAddon, slug: 'cool-addon' },
            message,
          }),
        );

      await reportAddon({
        addonSlug: 'cool-addon',
        api: apiState,
        message,
      });

      mockApi.verify();
    });
  it('dispatches fetchCurrentCollection when slug param has changed', () => {
    const errorHandler = createStubErrorHandler();
    const { store } = dispatchClientMetadata();
    const fakeDispatch = sinon.spy(store, 'dispatch');
    const page = 123;
    const sort = COLLECTION_SORT_NAME;

    _loadCurrentCollection({ store });

    const wrapper = renderComponent({
      errorHandler,
      location: createFakeLocation({ query: { page, collection_sort: sort } }),
      store,
    });
    fakeDispatch.resetHistory();

    const newParams = {
      slug: 'some-other-collection-slug',
      username: defaultUser,
    };
    wrapper.setProps({ match: { params: newParams } });

    sinon.assert.callCount(fakeDispatch, 1);
    sinon.assert.calledWith(
      fakeDispatch,
      fetchCurrentCollection({
        errorHandlerId: errorHandler.id,
        filters: { page, collectionSort: sort },
        ...newParams,
      }),
    );
  });
  it('renders a collection', () => {
    const slug = 'some-slug';
    const username = 'some-username';
    const page = 2;
    const sort = COLLECTION_SORT_NAME;
    const queryParams = { page, collection_sort: sort };

    const { store } = dispatchClientMetadata();

    const detail = createFakeCollectionDetail({
      authorUsername: username,
      count: 10,
      slug,
    });

    _loadCurrentCollection({ store, detail });

    const wrapper = renderComponent({
      location: createFakeLocation({ query: queryParams }),
      match: { params: { username, slug } },
      store,
    });

    expect(wrapper.find('.Collection-wrapper')).toHaveLength(1);
    expect(wrapper.find(AddonsCard)).toHaveProp('editing', false);
  });
  it('passes filters from the query string to fetchCurrentCollection', () => {
    const { store } = dispatchClientMetadata();
    const fakeDispatch = sinon.spy(store, 'dispatch');

    const errorHandler = createStubErrorHandler();
    const slug = 'collection-slug';
    const username = 'some-user';
    const page = 123;
    const sort = COLLECTION_SORT_NAME;

    renderComponent({
      errorHandler,
      location: createFakeLocation({ query: { page, collection_sort: sort } }),
      match: { params: { slug, username } },
      store,
    });

    sinon.assert.callCount(fakeDispatch, 1);
    sinon.assert.calledWith(
      fakeDispatch,
      fetchCurrentCollection({
        errorHandlerId: errorHandler.id,
        filters: { page, collectionSort: sort },
        slug,
        username,
      }),
    );
  });
  it('dispatches fetchCurrentCollectionPage when page has changed', () => {
    const { store } = dispatchClientMetadata();
    const fakeDispatch = sinon.spy(store, 'dispatch');
    const page = 123;
    const sort = COLLECTION_SORT_NAME;

    _loadCurrentCollection({ store });

    const errorHandler = createStubErrorHandler();

    const wrapper = renderComponent({
      errorHandler,
      location: createFakeLocation({ query: { page, collection_sort: sort } }),
      store,
    });
    fakeDispatch.resetHistory();

    const newFilters = { collectionSort: sort, page: 999 };

    // This will trigger the componentDidUpdate() method.
    wrapper.setProps({ filters: newFilters });

    sinon.assert.callCount(fakeDispatch, 1);
    sinon.assert.calledWith(
      fakeDispatch,
      fetchCurrentCollectionPage({
        errorHandlerId: errorHandler.id,
        filters: newFilters,
        username: defaultUser,
        slug: defaultSlug,
      }),
    );
  });
  it('dispatches fetchCurrentCollection on mount', () => {
    const { store } = dispatchClientMetadata();
    const fakeDispatch = sinon.spy(store, 'dispatch');

    const errorHandler = createStubErrorHandler();
    const slug = 'collection-slug';
    const username = 'some-user';
    const params = { slug, username };

    renderComponent({ errorHandler, match: { params }, store });

    // These are the expected default values for filters.
    const filters = {
      page: '1',
      collectionSort: COLLECTION_SORT_DATE_ADDED_DESCENDING,
    };

    sinon.assert.callCount(fakeDispatch, 1);
    sinon.assert.calledWith(
      fakeDispatch,
      fetchCurrentCollection({
        errorHandlerId: errorHandler.id,
        filters,
        slug,
        username,
      }),
    );
  });
  it('renders a CollectionDetailsCard', () => {
    const creating = false;
    const editing = false;
    const addons = createFakeCollectionAddons();
    const detail = createFakeCollectionDetail();
    const pageSize = DEFAULT_API_PAGE_SIZE;
    const collection = createInternalCollection({
      detail,
      items: addons,
      pageSize,
    });
    const page = 1;
    const sort = COLLECTION_SORT_NAME;
    const queryParams = { page, collection_sort: sort };
    const { store } = dispatchClientMetadata();

    _loadCurrentCollection({ addons, detail, pageSize, store });

    const wrapper = renderComponent({
      creating,
      editing,
      location: createFakeLocation({ query: queryParams }),
      store,
    });

    const detailsCard = wrapper.find(CollectionDetailsCard);
    expect(detailsCard).toHaveProp('collection', collection);
    expect(detailsCard).toHaveProp('creating', creating);
    expect(detailsCard).toHaveProp('editing', editing);
    expect(detailsCard).toHaveProp('filters', { page, collectionSort: sort });
  });
  it('does not allow dispatch if there is no content in the textarea', () => {
    const addon = { ...fakeAddon, slug: 'this-should-not-happen' };
    const fakeEvent = createFakeEvent();
    const { store } = dispatchClientMetadata();
    const dispatchSpy = sinon.spy(store, 'dispatch');

    // We enable the button with an empty textarea; this never happens
    // normally but we can force it here for testing.
    store.dispatch(enableAbuseButtonUI({ addon }));
    dispatchSpy.resetHistory();
    fakeEvent.preventDefault.resetHistory();

    const root = renderMount({ addon, store });

    // Make sure the button isn't disabled.
    expect(
      root
        .find('.ReportAbuseButton-send-report')
        .first()
        .prop('disabled'),
    ).toEqual(false);
    root
      .find('button.ReportAbuseButton-send-report')
      .simulate('click', fakeEvent);

    sinon.assert.notCalled(dispatchSpy);
    // Make sure preventDefault was called; we then know the sendReport()
    // method was called.
    sinon.assert.called(fakeEvent.preventDefault);
  });
    it('returns an array of languages', () => {
      const language = createFakeLanguageTool();
      const { store } = dispatchClientMetadata();
      store.dispatch(loadLanguageTools({ languageTools: [language] }));

      expect(getAllLanguageTools(store.getState())).toEqual([language]);
    });
  it('updates add-on placeholder count with collection add-ons', () => {
    const { store } = dispatchClientMetadata();
    const { detail, addons } = createCollectionWithTwoAddons();

    const wrapper = renderComponent({
      params: { username: detail.author.username, slug: detail.slug },
      store,
    });

    _loadCurrentCollection({
      store,
      detail,
      addons,
    });
    simulateReduxStateChange({ wrapper, store });

    // Since the placeholder calculation happens in
    // componentDidUpdate(), we need to re-render to see the effect.
    wrapper.setProps();

    expect(wrapper.find(AddonsCard)).toHaveProp(
      'placeholderCount',
      addons.length,
    );
  });
Example #17
0
    it('loads the the correct amount of theme add-ons in a collection to display on homepage', () => {
      const { store } = dispatchClientMetadata();

      _loadHomeAddons({
        store,
        collections: [
          createFakeCollectionAddonsListResponse({
            addons: Array(10).fill({
              ...createFakeCollectionAddon({
                addon: {
                  ...fakeAddon,
                  type: ADDON_TYPE_THEME,
                },
              }),
            }),
          }),
        ],
      });

      const homeState = store.getState().home;

      expect(homeState.resultsLoaded).toEqual(true);
      expect(homeState.collections).toHaveLength(1);

      expect(homeState.collections[0]).toEqual(
        Array(LANDING_PAGE_THEME_COUNT).fill(
          createInternalAddon({
            ...fakeAddon,
            type: ADDON_TYPE_THEME,
          }),
        ),
      );
    });
Example #18
0
    it('does not fetchLatestUserReview without a user', () => {
      const { store } = dispatchClientMetadata();
      const dispatchSpy = sinon.spy(store, 'dispatch');
      renderWithoutUser({ store });

      sinon.assert.notCalled(dispatchSpy);
    });
  it('disables the buttons while sending the abuse report', () => {
    const addon = { ...fakeAddon, slug: 'bank-machine-skimmer' };
    const fakeEvent = createFakeEvent();
    const { store } = dispatchClientMetadata();
    store.dispatch(
      sendAddonAbuseReport({
        addonSlug: addon.slug,
        errorHandlerId: 'my-error',
        message: 'All my money is gone',
      }),
    );
    const root = renderMount({ addon, store });

    // Expand the view so we can test that it wasn't contracted when
    // clicking on the disabled "dismiss" link.
    root
      .find('button.ReportAbuseButton-show-more')
      .simulate('click', fakeEvent);
    expect(root.find('.ReportAbuseButton--is-expanded')).toHaveLength(1);

    const dismissButton = root.find('.ReportAbuseButton-dismiss-report');
    const sendButton = root.find('button.ReportAbuseButton-send-report');

    expect(dismissButton).toHaveClassName(
      'ReportAbuseButton-dismiss-report--disabled',
    );
    expect(sendButton.prop('disabled')).toEqual(true);
    expect(sendButton.prop('children')).toEqual('Sending abuse report');

    dismissButton.simulate('click', fakeEvent);
    sinon.assert.called(fakeEvent.preventDefault);
    expect(root.find('.ReportAbuseButton--is-expanded')).toHaveLength(1);
  });
 const _dispatchClientMetadata = (params = {}) => {
   return dispatchClientMetadata({
     store,
     userAgent: userAgentsByPlatform.mac.firefox57,
     ...params,
   });
 };
Example #21
0
  it('dispatches an action to fetch the add-ons to display on update', () => {
    const includeFeaturedThemes = false;
    const includeTrendingExtensions = false;
    const { store } = dispatchClientMetadata();

    const fakeDispatch = sinon.stub(store, 'dispatch');

    const root = render({
      includeFeaturedThemes,
      includeTrendingExtensions,
      store,
    });
    fakeDispatch.resetHistory();

    // We simulate an update to trigger `componentDidUpdate()`.
    root.setProps();

    sinon.assert.callCount(fakeDispatch, 2);
    sinon.assert.calledWith(fakeDispatch, setViewContext(VIEW_CONTEXT_HOME));
    sinon.assert.calledWith(
      fakeDispatch,
      fetchHomeAddons({
        errorHandlerId: root.instance().props.errorHandler.id,
        collectionsToFetch: FEATURED_COLLECTIONS,
        includeFeaturedThemes,
        includeTrendingExtensions,
      }),
    );
  });
Example #22
0
    it('allows abuse reports for multiple add-ons', () => {
      const { store } = dispatchClientMetadata();

      store.dispatch(
        loadAddonAbuseReport(
          createFakeAddonAbuseReport({
            addon: { ...fakeAddon, slug: 'some-addon' },
            message: 'This add-on is malwaré.',
            reporter: null,
          }),
        ),
      );
      store.dispatch(
        loadAddonAbuseReport(
          createFakeAddonAbuseReport({
            addon: { ...fakeAddon, slug: 'another-addon' },
            message: 'The add-on is boring',
            reporter: null,
          }),
        ),
      );

      expect(store.getState().abuse).toMatchObject({
        bySlug: {
          'another-addon': {
            message: 'The add-on is boring',
          },
          'some-addon': {
            message: 'This add-on is malwaré.',
          },
        },
      });
    });
Example #23
0
  it('dispatches an action to fetch the add-ons to display', () => {
    const includeFeaturedThemes = false;
    const includeTrendingExtensions = false;
    const errorHandler = createStubErrorHandler();
    const { store } = dispatchClientMetadata();

    const fakeDispatch = sinon.stub(store, 'dispatch');

    render({
      errorHandler,
      includeFeaturedThemes,
      includeTrendingExtensions,
      store,
    });

    sinon.assert.callCount(fakeDispatch, 2);
    sinon.assert.calledWith(fakeDispatch, setViewContext(VIEW_CONTEXT_HOME));
    sinon.assert.calledWith(
      fakeDispatch,
      fetchHomeAddons({
        errorHandlerId: errorHandler.id,
        collectionsToFetch: FEATURED_COLLECTIONS,
        includeFeaturedThemes,
        includeTrendingExtensions,
      }),
    );
  });
Example #24
0
  it('renders alternate links for aliased locales', () => {
    const baseURL = 'https://example.org';
    const clientApp = CLIENT_APP_FIREFOX;
    const lang = 'de';
    const to = '/some-url';

    const _hrefLangs = ['x-default'];
    const aliasKey = 'x-default';
    const aliasValue = 'en-US';
    const hrefLangsMap = {
      [aliasKey]: aliasValue,
    };

    const _config = getFakeConfig({ baseURL, hrefLangsMap });
    const { store } = dispatchClientMetadata({
      clientApp,
      lang,
      pathname: `/${lang}/${clientApp}${to}`,
    });

    const root = render({ _config, _hrefLangs, store });

    expect(root.find('link[rel="alternate"]')).toHaveLength(_hrefLangs.length);
    expect(root.find('link[rel="alternate"]').at(0)).toHaveProp(
      'hrefLang',
      aliasKey,
    );
    expect(root.find('link[rel="alternate"]').at(0)).toHaveProp(
      'href',
      `${baseURL}/${aliasValue}/${clientApp}${to}`,
    );
  });
Example #25
0
    (clientApp) => {
      const baseURL = 'https://example.org';
      const lang = 'fr';
      const to = '/some-url';

      const _hrefLangs = ['fr', 'en-US'];
      const _config = getFakeConfig({ baseURL });
      const { store } = dispatchClientMetadata({
        clientApp,
        lang,
        pathname: `/${lang}/${clientApp}${to}`,
      });

      const root = render({ _config, _hrefLangs, store });

      expect(root.find('link[rel="alternate"]')).toHaveLength(
        _hrefLangs.length,
      );
      _hrefLangs.forEach((locale, index) => {
        expect(root.find('link[rel="alternate"]').at(index)).toHaveProp(
          'hrefLang',
          locale,
        );
        expect(root.find('link[rel="alternate"]').at(index)).toHaveProp(
          'href',
          `${baseURL}/${locale}/${clientApp}${to}`,
        );
      });
    },
Example #26
0
  it('accepts a queryString prop that is added to the URLs', () => {
    const baseURL = 'https://example.org';
    const clientApp = CLIENT_APP_FIREFOX;
    const lang = 'de';
    const to = '/some-canonical-url';
    const queryString = '?foo=bar';

    const _config = getFakeConfig({ baseURL });
    const { store } = dispatchClientMetadata({
      clientApp,
      lang,
      pathname: `/${lang}/${clientApp}${to}`,
      search: queryString,
    });

    const root = render({ _config, store, queryString });

    expect(root.find('link[rel="canonical"]')).toHaveLength(1);
    expect(root.find('link[rel="canonical"]')).toHaveProp(
      'href',
      `${baseURL}/${lang}/${clientApp}${to}${queryString}`,
    );
    expect(root.find(`link[hrefLang="${lang}"]`)).toHaveProp(
      'href',
      `${baseURL}/${lang}/${clientApp}${to}${queryString}`,
    );
  });
  it('renders a GuidesAddonCard', () => {
    const { store } = dispatchClientMetadata();
    const iconURL = 'https://addons.cdn.mozilla.net/foo.jpg';
    const addonName = 'special-addon';
    const addonCustomText = 'Everyone needs this cool addon.';
    const addon = createInternalAddon({
      ...fakeAddon,
      icon_url: iconURL,
      name: addonName,
    });

    const root = render({
      addon,
      addonCustomText,
      store,
    });

    const image = root.find('.GuidesAddonCard-content-icon');
    expect(image).toHaveLength(1);
    expect(image.prop('src')).toEqual(iconURL);
    expect(image.prop('alt')).toEqual(addonName);

    expect(root.find('.GuidesAddonCard-content-description')).toHaveText(
      addonCustomText,
    );
    expect(root.find(AddonTitle)).toHaveLength(1);
    expect(root.find(InstallButtonWrapper)).toHaveLength(1);
  });
Example #28
0
 const getProps = ({ ...props } = {}) => {
   return {
     className: 'OverlayCard',
     id: 'OverlayCard',
     store: dispatchClientMetadata().store,
     ...props,
   };
 };
Example #29
0
 it('hides when you click the background', () => {
   const { store } = dispatchClientMetadata();
   const root = render({ visibleOnLoad: true, store });
   const btn = root.find('.Overlay-background');
   btn.simulate('click', createFakeEvent());
   applyUIStateChanges({ root, store });
   expect(root).not.toHaveClassName('Overlay--visible');
 });
  it('passes the addon to InstallButtonWrapper', () => {
    const { store } = dispatchClientMetadata();
    const addon = createInternalAddon(fakeAddon);

    const root = render({ addon, store });

    expect(root.find(InstallButtonWrapper)).toHaveProp('addon', addon);
  });