diff --git a/src/disco/components/Addon.js b/src/disco/components/Addon.js
index 8bd873fea2d..d57fc671452 100644
--- a/src/disco/components/Addon.js
+++ b/src/disco/components/Addon.js
@@ -1,20 +1,29 @@
import classNames from 'classnames';
import { sprintf } from 'jed';
import React, { PropTypes } from 'react';
+import { connect } from 'react-redux';
import translate from 'core/i18n/translate';
import purify from 'core/purify';
+import config from 'config';
import themeAction, { getThemeData } from 'disco/themePreview';
+import tracking from 'core/tracking';
+import { AddonManager } from 'disco/addonManager';
-import InstallButton from 'disco/containers/InstallButton';
+import InstallButton from 'disco/components/InstallButton';
import {
validAddonTypes,
validInstallStates,
ERROR,
EXTENSION_TYPE,
+ INSTALL_CATEGORY,
+ INSTALLED,
+ THEME_INSTALL,
THEME_TYPE,
THEME_PREVIEW,
THEME_RESET_PREVIEW,
+ UNINSTALL_CATEGORY,
+ UNINSTALLED,
} from 'disco/constants';
import 'disco/css/Addon.scss';
@@ -40,8 +49,10 @@ export class Addon extends React.Component {
i18n: PropTypes.string.isRequired,
iconUrl: PropTypes.string,
id: PropTypes.string.isRequired,
+ installURL: PropTypes.string,
previewURL: PropTypes.string,
name: PropTypes.string.isRequired,
+ setInitialStatus: PropTypes.func.isRequired,
status: PropTypes.oneOf(validInstallStates).isRequired,
textcolor: PropTypes.string,
themeAction: PropTypes.func,
@@ -53,6 +64,11 @@ export class Addon extends React.Component {
themeAction,
}
+ componentDidMount() {
+ const { guid, installURL, setInitialStatus } = this.props;
+ setInitialStatus({guid, installURL});
+ }
+
getBrowserThemeData() {
return JSON.stringify(getThemeData(this.props));
}
@@ -117,7 +133,7 @@ export class Addon extends React.Component {
}
render() {
- const { guid, heading, type } = this.props;
+ const { heading, type } = this.props;
if (!validAddonTypes.includes(type)) {
throw new Error(`Invalid addon type "${type}"`);
@@ -141,7 +157,7 @@ export class Addon extends React.Component {
{this.getDescription()}
-
+
@@ -149,4 +165,75 @@ export class Addon extends React.Component {
}
}
-export default translate({withRef: true})(Addon);
+export function mapStateToProps(state, ownProps) {
+ const installation = state.installations[ownProps.guid] || {};
+ const addon = state.addons[ownProps.guid] || {};
+ return {...installation, ...addon};
+}
+
+export function makeProgressHandler(dispatch, guid) {
+ return (addonInstall) => {
+ if (addonInstall.state === 'STATE_DOWNLOADING') {
+ const downloadProgress = parseInt(
+ 100 * addonInstall.progress / addonInstall.maxProgress, 10);
+ dispatch({type: 'DOWNLOAD_PROGRESS', payload: {guid, downloadProgress}});
+ } else if (addonInstall.state === 'STATE_INSTALLING') {
+ dispatch({type: 'START_INSTALL', payload: {guid}});
+ } else if (addonInstall.state === 'STATE_INSTALLED') {
+ dispatch({type: 'INSTALL_COMPLETE', payload: {guid}});
+ }
+ };
+}
+
+export function mapDispatchToProps(dispatch, { _tracking = tracking } = {}) {
+ if (config.get('server')) {
+ return {};
+ }
+ return {
+ setInitialStatus({ guid, installURL }) {
+ const addonManager = new AddonManager(guid, installURL);
+ const payload = {guid, url: installURL};
+ return addonManager.getAddon()
+ .then(
+ (addon) => {
+ const status = addon.type === THEME_TYPE && !addon.isEnabled ? UNINSTALLED : INSTALLED;
+ dispatch({type: 'INSTALL_STATE', payload: {...payload, status}});
+ },
+ () => dispatch({type: 'INSTALL_STATE', payload: {...payload, status: UNINSTALLED}}));
+ },
+
+ install({ guid, installURL, name }) {
+ const addonManager = new AddonManager(guid, installURL, makeProgressHandler(dispatch, guid));
+ dispatch({type: 'START_DOWNLOAD', payload: {guid}});
+ _tracking.sendEvent({action: 'addon', category: INSTALL_CATEGORY, label: name});
+ return addonManager.install();
+ },
+
+ installTheme(node, guid, name, _themeAction = themeAction) {
+ _themeAction(node, THEME_INSTALL);
+ _tracking.sendEvent({action: 'theme', category: INSTALL_CATEGORY, label: name});
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ dispatch({type: 'INSTALL_STATE', payload: {guid, status: INSTALLED}});
+ resolve();
+ }, 250);
+ });
+ },
+
+ uninstall({ guid, installURL, name, type }) {
+ const addonManager = new AddonManager(guid, installURL);
+ dispatch({type: 'START_UNINSTALL', payload: {guid}});
+ const action = {
+ [EXTENSION_TYPE]: 'addon',
+ [THEME_TYPE]: 'theme',
+ }[type] || 'invalid';
+ _tracking.sendEvent({action, category: UNINSTALL_CATEGORY, label: name});
+ return addonManager.uninstall()
+ .then(() => dispatch({type: 'UNINSTALL_COMPLETE', payload: {guid}}));
+ },
+ };
+}
+
+export default connect(
+ mapStateToProps, mapDispatchToProps, undefined, {withRef: true}
+)(translate({withRef: true})(Addon));
diff --git a/src/disco/components/InstallButton.js b/src/disco/components/InstallButton.js
new file mode 100644
index 00000000000..9c8a67dcc01
--- /dev/null
+++ b/src/disco/components/InstallButton.js
@@ -0,0 +1,85 @@
+import React, { PropTypes } from 'react';
+import translate from 'core/i18n/translate';
+
+import {
+ DOWNLOADING,
+ INSTALLED,
+ THEME_TYPE,
+ UNINSTALLED,
+ UNKNOWN,
+ validAddonTypes,
+ validInstallStates as validStates,
+} from 'disco/constants';
+import { getThemeData } from 'disco/themePreview';
+
+import 'disco/css/InstallButton.scss';
+
+export class InstallButton extends React.Component {
+ static propTypes = {
+ handleChange: PropTypes.func,
+ guid: PropTypes.string.isRequired,
+ install: PropTypes.func.isRequired,
+ installTheme: PropTypes.func.isRequired,
+ i18n: PropTypes.object.isRequired,
+ installURL: PropTypes.string,
+ name: PropTypes.string.isRequired,
+ uninstall: PropTypes.func.isRequired,
+ url: PropTypes.string,
+ downloadProgress: PropTypes.number,
+ slug: PropTypes.string.isRequired,
+ status: PropTypes.oneOf(validStates),
+ type: PropTypes.oneOf(validAddonTypes),
+ }
+
+ static defaultProps = {
+ status: UNKNOWN,
+ downloadProgress: 0,
+ }
+
+ handleClick = (e) => {
+ e.preventDefault();
+ const { guid, install, installURL, name, status, installTheme, type, uninstall } = this.props;
+ if (type === THEME_TYPE && status === UNINSTALLED) {
+ installTheme(this.refs.themeData, guid, name);
+ } else if (status === UNINSTALLED) {
+ install({ guid, installURL, name });
+ } else if (status === INSTALLED) {
+ uninstall({ guid, installURL, name, type });
+ }
+ }
+
+ render() {
+ const { downloadProgress, i18n, slug, status } = this.props;
+
+ if (!validStates.includes(status)) {
+ throw new Error('Invalid add-on status');
+ }
+
+ const isInstalled = status === INSTALLED;
+ const isDisabled = status === UNKNOWN;
+ const isDownloading = status === DOWNLOADING;
+ const switchClasses = `switch ${status}`;
+ const identifier = `install-button-${slug}`;
+
+ return (
+
+
+
+
+ );
+ }
+}
+
+export default translate()(InstallButton);
diff --git a/src/disco/containers/DiscoPane.js b/src/disco/containers/DiscoPane.js
index f8f77e71f27..1cb8af263d3 100644
--- a/src/disco/containers/DiscoPane.js
+++ b/src/disco/containers/DiscoPane.js
@@ -70,7 +70,7 @@ export class DiscoPane extends React.Component {
- {results.map((item, i) => )}
+ {results.map((item) => )}
{i18n.gettext('See more add-ons!')}
diff --git a/src/disco/containers/InstallButton.js b/src/disco/containers/InstallButton.js
deleted file mode 100644
index 09cc58094c6..00000000000
--- a/src/disco/containers/InstallButton.js
+++ /dev/null
@@ -1,168 +0,0 @@
-import React, { PropTypes } from 'react';
-import { connect } from 'react-redux';
-import translate from 'core/i18n/translate';
-import tracking from 'core/tracking';
-
-import config from 'config';
-import { AddonManager } from 'disco/addonManager';
-import {
- DOWNLOADING,
- EXTENSION_TYPE,
- INSTALLED,
- INSTALL_CATEGORY,
- THEME_INSTALL,
- THEME_TYPE,
- UNINSTALL_CATEGORY,
- UNINSTALLED,
- UNKNOWN,
- validAddonTypes,
- validInstallStates as validStates,
-} from 'disco/constants';
-import themeAction, { getThemeData } from 'disco/themePreview';
-
-import 'disco/css/InstallButton.scss';
-
-export class InstallButton extends React.Component {
- static propTypes = {
- handleChange: PropTypes.func,
- guid: PropTypes.string.isRequired,
- install: PropTypes.func.isRequired,
- installTheme: PropTypes.func.isRequired,
- i18n: PropTypes.object.isRequired,
- installURL: PropTypes.string,
- name: PropTypes.string.isRequired,
- uninstall: PropTypes.func.isRequired,
- url: PropTypes.string,
- downloadProgress: PropTypes.number,
- setInitialStatus: PropTypes.func.isRequired,
- slug: PropTypes.string.isRequired,
- status: PropTypes.oneOf(validStates),
- type: PropTypes.oneOf(validAddonTypes),
- }
-
- static defaultProps = {
- status: UNKNOWN,
- downloadProgress: 0,
- }
-
- componentDidMount() {
- const { guid, installURL, setInitialStatus } = this.props;
- setInitialStatus({guid, installURL});
- }
-
- handleClick = (e) => {
- e.preventDefault();
- const { guid, install, installURL, name, status, installTheme, type, uninstall } = this.props;
- if (type === THEME_TYPE && status === UNINSTALLED) {
- installTheme(this.refs.themeData, guid, name);
- } else if (status === UNINSTALLED) {
- install({ guid, installURL, name });
- } else if (status === INSTALLED) {
- uninstall({ guid, installURL, name, type });
- }
- }
-
- render() {
- const { downloadProgress, i18n, slug, status } = this.props;
-
- if (!validStates.includes(status)) {
- throw new Error('Invalid add-on status');
- }
-
- const isInstalled = status === INSTALLED;
- const isDisabled = status === UNKNOWN;
- const isDownloading = status === DOWNLOADING;
- const switchClasses = `switch ${status}`;
- const identifier = `install-button-${slug}`;
-
- return (
-
-
-
-
- );
- }
-}
-
-export function mapStateToProps(state, ownProps) {
- const installation = state.installations[ownProps.guid] || {};
- const addon = state.addons[ownProps.guid] || {};
- return {...installation, ...addon};
-}
-
-export function makeProgressHandler(dispatch, guid) {
- return (addonInstall) => {
- if (addonInstall.state === 'STATE_DOWNLOADING') {
- const downloadProgress = parseInt(
- 100 * addonInstall.progress / addonInstall.maxProgress, 10);
- dispatch({type: 'DOWNLOAD_PROGRESS', payload: {guid, downloadProgress}});
- } else if (addonInstall.state === 'STATE_INSTALLING') {
- dispatch({type: 'START_INSTALL', payload: {guid}});
- } else if (addonInstall.state === 'STATE_INSTALLED') {
- dispatch({type: 'INSTALL_COMPLETE', payload: {guid}});
- }
- };
-}
-
-export function mapDispatchToProps(dispatch, { _tracking = tracking } = {}) {
- if (config.get('server')) {
- return {};
- }
- return {
- setInitialStatus({ guid, installURL }) {
- const addonManager = new AddonManager(guid, installURL);
- const payload = {guid, url: installURL};
- return addonManager.getAddon()
- .then(
- (addon) => {
- const status = addon.type === THEME_TYPE && !addon.isEnabled ? UNINSTALLED : INSTALLED;
- dispatch({type: 'INSTALL_STATE', payload: {...payload, status}});
- },
- () => dispatch({type: 'INSTALL_STATE', payload: {...payload, status: UNINSTALLED}}));
- },
-
- install({ guid, installURL, name }) {
- const addonManager = new AddonManager(guid, installURL, makeProgressHandler(dispatch, guid));
- dispatch({type: 'START_DOWNLOAD', payload: {guid}});
- _tracking.sendEvent({action: 'addon', category: INSTALL_CATEGORY, label: name});
- return addonManager.install();
- },
-
- installTheme(node, guid, name, _themeAction = themeAction) {
- _themeAction(node, THEME_INSTALL);
- _tracking.sendEvent({action: 'theme', category: INSTALL_CATEGORY, label: name});
- return new Promise((resolve) => {
- setTimeout(() => {
- dispatch({type: 'INSTALL_STATE', payload: {guid, status: INSTALLED}});
- resolve();
- }, 250);
- });
- },
-
- uninstall({ guid, installURL, name, type }) {
- const addonManager = new AddonManager(guid, installURL);
- dispatch({type: 'START_UNINSTALL', payload: {guid}});
- const action = {
- [EXTENSION_TYPE]: 'addon',
- [THEME_TYPE]: 'theme',
- }[type] || 'invalid';
- _tracking.sendEvent({action, category: UNINSTALL_CATEGORY, label: name});
- return addonManager.uninstall()
- .then(() => dispatch({type: 'UNINSTALL_COMPLETE', payload: {guid}}));
- },
- };
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(translate()(InstallButton));
diff --git a/tests/client/disco/components/TestAddon.js b/tests/client/disco/components/TestAddon.js
index 083a2b003a6..0c887cec2a8 100644
--- a/tests/client/disco/components/TestAddon.js
+++ b/tests/client/disco/components/TestAddon.js
@@ -7,8 +7,24 @@ import {
import { findDOMNode } from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
-import Addon from 'disco/components/Addon';
-import { ERROR, THEME_PREVIEW, THEME_RESET_PREVIEW, THEME_TYPE } from 'disco/constants';
+import config from 'config';
+import Addon, {
+ makeProgressHandler,
+ mapDispatchToProps,
+ mapStateToProps,
+} from 'disco/components/Addon';
+import * as addonManager from 'disco/addonManager';
+import {
+ ERROR,
+ INSTALL_CATEGORY,
+ INSTALLED,
+ THEME_INSTALL,
+ THEME_PREVIEW,
+ THEME_RESET_PREVIEW,
+ THEME_TYPE,
+ UNINSTALL_CATEGORY,
+ UNINSTALLED,
+} from 'disco/constants';
import { stubAddonManager, getFakeI18nInst } from 'tests/client/helpers';
import I18nProvider from 'core/i18n/Provider';
@@ -29,18 +45,15 @@ function renderAddon(data) {
- ), Addon).getWrappedInstance();
+ ), Addon).getWrappedInstance().getWrappedInstance();
}
describe('', () => {
- beforeEach(() => {
- stubAddonManager();
- });
-
describe('', () => {
let root;
beforeEach(() => {
+ stubAddonManager();
root = renderAddon(result);
});
@@ -108,6 +121,7 @@ describe('', () => {
let root;
beforeEach(() => {
+ stubAddonManager();
const data = {...result, type: THEME_TYPE};
root = renderAddon(data);
});
@@ -128,6 +142,7 @@ describe('', () => {
let themeAction;
beforeEach(() => {
+ stubAddonManager();
themeAction = sinon.stub();
const data = {...result, type: THEME_TYPE, themeAction};
root = renderAddon(data);
@@ -160,4 +175,296 @@ describe('', () => {
assert.ok(preventDefault.called);
});
});
+
+ describe('mapStateToProps', () => {
+ it('pulls the installation data from the state', () => {
+ stubAddonManager();
+ const addon = {
+ guid: 'foo@addon',
+ downloadProgress: 75,
+ };
+ assert.deepEqual(
+ mapStateToProps({
+ installations: {foo: {some: 'data'}, 'foo@addon': addon},
+ addons: {'foo@addon': {addonProp: 'addonValue'}},
+ }, {guid: 'foo@addon'}),
+ {guid: 'foo@addon', downloadProgress: 75, addonProp: 'addonValue'});
+ });
+ });
+
+ describe('makeProgressHandler', () => {
+ it('sets the download progress on STATE_DOWNLOADING', () => {
+ const dispatch = sinon.spy();
+ const guid = 'foo@addon';
+ const handler = makeProgressHandler(dispatch, guid);
+ handler({state: 'STATE_DOWNLOADING', progress: 300, maxProgress: 990});
+ assert(dispatch.calledWith({
+ type: 'DOWNLOAD_PROGRESS',
+ payload: {downloadProgress: 30, guid},
+ }));
+ });
+
+ it('sets status to installing on STATE_INSTALLING', () => {
+ const dispatch = sinon.spy();
+ const guid = 'foo@my-addon';
+ const handler = makeProgressHandler(dispatch, guid);
+ handler({state: 'STATE_INSTALLING'});
+ assert(dispatch.calledWith({
+ type: 'START_INSTALL',
+ payload: {guid},
+ }));
+ });
+
+ it('sets status to installed on STATE_INSTALLED', () => {
+ const dispatch = sinon.spy();
+ const guid = '{my-addon}';
+ const handler = makeProgressHandler(dispatch, guid);
+ handler({state: 'STATE_INSTALLED'});
+ assert(dispatch.calledWith({
+ type: 'INSTALL_COMPLETE',
+ payload: {guid},
+ }));
+ });
+ });
+
+ describe('setInitialStatus', () => {
+ it('sets the status to INSTALLED when add-on found', () => {
+ stubAddonManager();
+ const dispatch = sinon.spy();
+ const guid = '@foo';
+ const installURL = 'http://the.url';
+ const { setInitialStatus } = mapDispatchToProps(dispatch);
+ return setInitialStatus({guid, installURL})
+ .then(() => {
+ assert(dispatch.calledWith({
+ type: 'INSTALL_STATE',
+ payload: {guid, status: INSTALLED, url: installURL},
+ }));
+ });
+ });
+
+ it('sets the status to INSTALLED when an installed theme is found', () => {
+ stubAddonManager({getAddon: Promise.resolve({type: THEME_TYPE, isEnabled: true})});
+ const dispatch = sinon.spy();
+ const guid = '@foo';
+ const installURL = 'http://the.url';
+ const { setInitialStatus } = mapDispatchToProps(dispatch);
+ return setInitialStatus({guid, installURL})
+ .then(() => {
+ assert(dispatch.calledWith({
+ type: 'INSTALL_STATE',
+ payload: {guid, status: INSTALLED, url: installURL},
+ }));
+ });
+ });
+
+ it('sets the status to UNINSTALLED when an uninstalled theme is found', () => {
+ stubAddonManager({getAddon: Promise.resolve({type: THEME_TYPE, isEnabled: false})});
+ const dispatch = sinon.spy();
+ const guid = '@foo';
+ const installURL = 'http://the.url';
+ const { setInitialStatus } = mapDispatchToProps(dispatch);
+ return setInitialStatus({guid, installURL})
+ .then(() => {
+ assert(dispatch.calledWith({
+ type: 'INSTALL_STATE',
+ payload: {guid, status: UNINSTALLED, url: installURL},
+ }));
+ });
+ });
+
+ it('sets the status to UNINSTALLED when not found', () => {
+ stubAddonManager({getAddon: Promise.reject()});
+ const dispatch = sinon.spy();
+ const guid = '@foo';
+ const installURL = 'http://the.url';
+ const { setInitialStatus } = mapDispatchToProps(dispatch);
+ return setInitialStatus({guid, installURL})
+ .then(() => {
+ assert(dispatch.calledWith({
+ type: 'INSTALL_STATE',
+ payload: {guid, status: UNINSTALLED, url: installURL},
+ }));
+ });
+ });
+ });
+
+ describe('install', () => {
+ const guid = '@install';
+ const installURL = 'https://mysite.com/download.xpi';
+
+ it('installs the addon on a new AddonManager', () => {
+ stubAddonManager();
+ const dispatch = sinon.spy();
+ const { install } = mapDispatchToProps(dispatch);
+ return install({guid, installURL})
+ .then(() => {
+ assert(addonManager.AddonManager.calledWithNew, 'new AddonManager() called');
+ assert(addonManager.AddonManager.calledWith(guid, installURL, sinon.match.func));
+ });
+ });
+
+ it('tracks an addon inntall', () => {
+ stubAddonManager();
+ const name = 'hai-addon';
+ const type = 'extension';
+ const dispatch = sinon.spy();
+ const fakeTracking = {
+ sendEvent: sinon.spy(),
+ };
+ const { install } = mapDispatchToProps(dispatch, {_tracking: fakeTracking});
+ return install({guid, installURL, name, type})
+ .then(() => {
+ assert(fakeTracking.sendEvent.calledWith({
+ action: 'addon',
+ category: INSTALL_CATEGORY,
+ label: 'hai-addon',
+ }));
+ });
+ });
+
+
+ it('should dispatch START_DOWNLOAD', () => {
+ stubAddonManager();
+ const dispatch = sinon.spy();
+ const { install } = mapDispatchToProps(dispatch);
+ return install({guid, installURL})
+ .then(() => assert(dispatch.calledWith({
+ type: 'START_DOWNLOAD',
+ payload: {guid},
+ })));
+ });
+ });
+
+ describe('uninstall', () => {
+ const guid = '@uninstall';
+ const installURL = 'https://mysite.com/download.xpi';
+
+ it('prepares the addon on a new AddonManager', () => {
+ stubAddonManager();
+ const dispatch = sinon.spy();
+ const { uninstall } = mapDispatchToProps(dispatch);
+ return uninstall({guid, installURL})
+ .then(() => {
+ assert(addonManager.AddonManager.calledWithNew, 'new AddonManager() called');
+ assert(addonManager.AddonManager.calledWith(guid, installURL));
+ });
+ });
+
+ it('tracks an addon uninstall', () => {
+ stubAddonManager();
+ const dispatch = sinon.spy();
+ const name = 'whatevs';
+ const type = 'extension';
+ const fakeTracking = {
+ sendEvent: sinon.spy(),
+ };
+ const { uninstall } = mapDispatchToProps(dispatch, {_tracking: fakeTracking});
+ return uninstall({guid, installURL, name, type})
+ .then(() => {
+ assert.ok(fakeTracking.sendEvent.calledWith({
+ action: 'addon',
+ category: UNINSTALL_CATEGORY,
+ label: 'whatevs',
+ }), 'correctly called');
+ });
+ });
+
+ it('tracks a theme uninstall', () => {
+ stubAddonManager();
+ const dispatch = sinon.spy();
+ const name = 'whatevs';
+ const type = 'persona';
+ const fakeTracking = {
+ sendEvent: sinon.spy(),
+ };
+ const { uninstall } = mapDispatchToProps(dispatch, {_tracking: fakeTracking});
+ return uninstall({guid, installURL, name, type})
+ .then(() => {
+ assert(fakeTracking.sendEvent.calledWith({
+ action: 'theme',
+ category: UNINSTALL_CATEGORY,
+ label: 'whatevs',
+ }));
+ });
+ });
+
+ it('tracks a unknown type uninstall', () => {
+ stubAddonManager();
+ const dispatch = sinon.spy();
+ const name = 'whatevs';
+ const type = 'foo';
+ const fakeTracking = {
+ sendEvent: sinon.spy(),
+ };
+ const { uninstall } = mapDispatchToProps(dispatch, {_tracking: fakeTracking});
+ return uninstall({guid, installURL, name, type})
+ .then(() => {
+ assert(fakeTracking.sendEvent.calledWith({
+ action: 'invalid',
+ category: UNINSTALL_CATEGORY,
+ label: 'whatevs',
+ }));
+ });
+ });
+
+ it('should dispatch START_UNINSTALL', () => {
+ stubAddonManager();
+ const dispatch = sinon.spy();
+ const { uninstall } = mapDispatchToProps(dispatch);
+ return uninstall({guid, installURL})
+ .then(() => assert(dispatch.calledWith({
+ type: 'START_UNINSTALL',
+ payload: {guid},
+ })));
+ });
+ });
+
+ describe('installTheme', () => {
+ it('installs the theme', () => {
+ const name = 'hai-theme';
+ const guid = '{install-theme}';
+ const node = sinon.stub();
+ const spyThemeAction = sinon.spy();
+ const dispatch = sinon.spy();
+ const { installTheme } = mapDispatchToProps(dispatch);
+ return installTheme(node, guid, name, spyThemeAction)
+ .then(() => {
+ assert(spyThemeAction.calledWith(node, THEME_INSTALL));
+ assert(dispatch.calledWith({
+ type: 'INSTALL_STATE',
+ payload: {guid, status: INSTALLED},
+ }));
+ });
+ });
+
+ it('tracks a theme install', () => {
+ const name = 'hai-theme';
+ const guid = '{install-theme}';
+ const node = sinon.stub();
+ const dispatch = sinon.spy();
+ const spyThemeAction = sinon.spy();
+ const fakeTracking = {
+ sendEvent: sinon.spy(),
+ };
+ const { installTheme } = mapDispatchToProps(dispatch, {_tracking: fakeTracking});
+ return installTheme(node, guid, name, spyThemeAction)
+ .then(() => {
+ assert(fakeTracking.sendEvent.calledWith({
+ action: 'theme',
+ category: INSTALL_CATEGORY,
+ label: 'hai-theme',
+ }));
+ });
+ });
+ });
+
+ describe('mapDispatchToProps', () => {
+ it('is empty when there is no navigator', () => {
+ const configStub = sinon.stub(config, 'get').returns(true);
+ assert.deepEqual(mapDispatchToProps(sinon.spy()), {});
+ assert(configStub.calledOnce);
+ assert(configStub.calledWith('server'));
+ });
+ });
});
diff --git a/tests/client/disco/components/TestInstallButton.js b/tests/client/disco/components/TestInstallButton.js
new file mode 100644
index 00000000000..22063fbc47b
--- /dev/null
+++ b/tests/client/disco/components/TestInstallButton.js
@@ -0,0 +1,136 @@
+import React from 'react';
+import { Simulate, renderIntoDocument } from 'react-addons-test-utils';
+import { findDOMNode } from 'react-dom';
+
+import {
+ InstallButton,
+} from 'disco/components/InstallButton';
+import {
+ DOWNLOADING,
+ INSTALLED,
+ INSTALLING,
+ THEME_TYPE,
+ UNINSTALLED,
+ UNINSTALLING,
+ UNKNOWN,
+} from 'disco/constants';
+import { getFakeI18nInst } from 'tests/client/helpers';
+
+
+describe('', () => {
+ function renderButton(props = {}) {
+ const renderProps = {
+ dispatch: sinon.spy(),
+ install: sinon.spy(),
+ installTheme: sinon.spy(),
+ uninstall: sinon.spy(),
+ i18n: getFakeI18nInst(),
+ ...props,
+ };
+
+ return renderIntoDocument(
+ );
+ }
+
+ it('should be disabled if isDisabled status is UNKNOWN', () => {
+ const button = renderButton({status: UNKNOWN});
+ const root = findDOMNode(button);
+ const checkbox = root.querySelector('input[type=checkbox]');
+ assert.equal(checkbox.hasAttribute('disabled'), true);
+ assert.ok(root.classList.contains('unknown'));
+ });
+
+ it('should reflect UNINSTALLED status', () => {
+ const button = renderButton({status: UNINSTALLED});
+ const root = findDOMNode(button);
+ const checkbox = root.querySelector('input[type=checkbox]');
+ assert.equal(checkbox.hasAttribute('disabled'), false);
+ assert.ok(root.classList.contains('uninstalled'));
+ });
+
+ it('should reflect INSTALLED status', () => {
+ const button = renderButton({status: INSTALLED});
+ const root = findDOMNode(button);
+ const checkbox = root.querySelector('input[type=checkbox]');
+ assert.equal(checkbox.checked, true, 'checked is true');
+ assert.ok(root.classList.contains('installed'));
+ });
+
+ it('should reflect download downloadProgress', () => {
+ const button = renderButton({status: DOWNLOADING, downloadProgress: 50});
+ const root = findDOMNode(button);
+ assert.ok(root.classList.contains('downloading'));
+ assert.equal(root.getAttribute('data-download-progress'), 50);
+ });
+
+ it('should reflect installation', () => {
+ const button = renderButton({status: INSTALLING});
+ const root = findDOMNode(button);
+ assert.ok(root.classList.contains('installing'));
+ });
+
+ it('should reflect uninstallation', () => {
+ const button = renderButton({status: UNINSTALLING});
+ const root = findDOMNode(button);
+ assert.ok(root.classList.contains('uninstalling'));
+ });
+
+ it('should not call anything on click when neither installed or uninstalled', () => {
+ const install = sinon.stub();
+ const uninstall = sinon.stub();
+ const button = renderButton({status: DOWNLOADING, install, uninstall});
+ const root = findDOMNode(button);
+ Simulate.click(root);
+ assert.ok(!install.called);
+ assert.ok(!uninstall.called);
+ });
+
+ it('should associate the label and input with id and for attributes', () => {
+ const button = renderButton({status: UNINSTALLED, slug: 'foo'});
+ const root = findDOMNode(button);
+ assert.equal(root.querySelector('input').getAttribute('id'),
+ 'install-button-foo', 'id is set');
+ assert.equal(root.querySelector('label').getAttribute('for'),
+ 'install-button-foo', 'for attribute matches id');
+ });
+
+ it('should throw on bogus status', () => {
+ assert.throws(() => {
+ renderButton({status: 'BOGUS'});
+ }, Error, 'Invalid add-on status');
+ });
+
+ it('should call installTheme function on click when uninstalled theme', () => {
+ const installTheme = sinon.spy();
+ const guid = 'test-guid';
+ const name = 'hai';
+ const button = renderButton({installTheme, type: THEME_TYPE, guid, name, status: UNINSTALLED});
+ const themeData = button.refs.themeData;
+ const root = findDOMNode(button);
+ Simulate.click(root);
+ assert(installTheme.calledWith(themeData, guid, name));
+ });
+
+ it('should call install function on click when uninstalled', () => {
+ const guid = '@foo';
+ const name = 'hai';
+ const install = sinon.spy();
+ const installURL = 'https://my.url/download';
+ const button = renderButton({guid, install, installURL, name, status: UNINSTALLED});
+ const root = findDOMNode(button);
+ Simulate.click(root);
+ assert(install.calledWith({guid, installURL, name}));
+ });
+
+ it('should call uninstall function on click when installed', () => {
+ const guid = '@foo';
+ const installURL = 'https://my.url/download';
+ const name = 'hai';
+ const type = 'whatevs';
+ const uninstall = sinon.spy();
+ const button = renderButton({guid, installURL, name, status: INSTALLED, type, uninstall});
+ const root = findDOMNode(button);
+ Simulate.click(root);
+ assert(uninstall.calledWith({guid, installURL, name, type}));
+ });
+});
diff --git a/tests/client/disco/containers/TestInstallButton.js b/tests/client/disco/containers/TestInstallButton.js
deleted file mode 100644
index d1aee96a86e..00000000000
--- a/tests/client/disco/containers/TestInstallButton.js
+++ /dev/null
@@ -1,443 +0,0 @@
-import React from 'react';
-import { Simulate, renderIntoDocument } from 'react-addons-test-utils';
-import { findDOMNode } from 'react-dom';
-
-import * as addonManager from 'disco/addonManager';
-import {
- InstallButton,
- makeProgressHandler,
- mapDispatchToProps,
- mapStateToProps,
-} from 'disco/containers/InstallButton';
-import {
- DOWNLOADING,
- INSTALL_CATEGORY,
- INSTALLED,
- INSTALLING,
- THEME_INSTALL,
- THEME_TYPE,
- UNINSTALLED,
- UNINSTALL_CATEGORY,
- UNINSTALLING,
- UNKNOWN,
-} from 'disco/constants';
-import { stubAddonManager, getFakeI18nInst } from 'tests/client/helpers';
-import config from 'config';
-
-
-describe('', () => {
- function renderButton(props = {}) {
- const renderProps = {
- dispatch: sinon.spy(),
- setInitialStatus: sinon.spy(),
- install: sinon.spy(),
- installTheme: sinon.spy(),
- uninstall: sinon.spy(),
- i18n: getFakeI18nInst(),
- ...props,
- };
-
- return renderIntoDocument(
- );
- }
-
- it('should be disabled if isDisabled status is UNKNOWN', () => {
- const button = renderButton({status: UNKNOWN});
- const root = findDOMNode(button);
- const checkbox = root.querySelector('input[type=checkbox]');
- assert.equal(checkbox.hasAttribute('disabled'), true);
- assert.ok(root.classList.contains('unknown'));
- });
-
- it('should reflect UNINSTALLED status', () => {
- const button = renderButton({status: UNINSTALLED});
- const root = findDOMNode(button);
- const checkbox = root.querySelector('input[type=checkbox]');
- assert.equal(checkbox.hasAttribute('disabled'), false);
- assert.ok(root.classList.contains('uninstalled'));
- });
-
- it('should reflect INSTALLED status', () => {
- const button = renderButton({status: INSTALLED});
- const root = findDOMNode(button);
- const checkbox = root.querySelector('input[type=checkbox]');
- assert.equal(checkbox.checked, true, 'checked is true');
- assert.ok(root.classList.contains('installed'));
- });
-
- it('should reflect download downloadProgress', () => {
- const button = renderButton({status: DOWNLOADING, downloadProgress: 50});
- const root = findDOMNode(button);
- assert.ok(root.classList.contains('downloading'));
- assert.equal(root.getAttribute('data-download-progress'), 50);
- });
-
- it('should reflect installation', () => {
- const button = renderButton({status: INSTALLING});
- const root = findDOMNode(button);
- assert.ok(root.classList.contains('installing'));
- });
-
- it('should reflect uninstallation', () => {
- const button = renderButton({status: UNINSTALLING});
- const root = findDOMNode(button);
- assert.ok(root.classList.contains('uninstalling'));
- });
-
- it('should not call anything on click when neither installed or uninstalled', () => {
- const install = sinon.stub();
- const uninstall = sinon.stub();
- const button = renderButton({status: DOWNLOADING, install, uninstall});
- const root = findDOMNode(button);
- Simulate.click(root);
- assert.ok(!install.called);
- assert.ok(!uninstall.called);
- });
-
- it('should associate the label and input with id and for attributes', () => {
- const button = renderButton({status: UNINSTALLED, slug: 'foo'});
- const root = findDOMNode(button);
- assert.equal(root.querySelector('input').getAttribute('id'),
- 'install-button-foo', 'id is set');
- assert.equal(root.querySelector('label').getAttribute('for'),
- 'install-button-foo', 'for attribute matches id');
- });
-
- it('should throw on bogus status', () => {
- assert.throws(() => {
- renderButton({status: 'BOGUS'});
- }, Error, 'Invalid add-on status');
- });
-
- it('should call installTheme function on click when uninstalled theme', () => {
- const installTheme = sinon.spy();
- const guid = 'test-guid';
- const name = 'hai';
- const button = renderButton({installTheme, type: THEME_TYPE, guid, name, status: UNINSTALLED});
- const themeData = button.refs.themeData;
- const root = findDOMNode(button);
- Simulate.click(root);
- assert(installTheme.calledWith(themeData, guid, name));
- });
-
- it('should call install function on click when uninstalled', () => {
- const guid = '@foo';
- const name = 'hai';
- const install = sinon.spy();
- const installURL = 'https://my.url/download';
- const button = renderButton({guid, install, installURL, name, status: UNINSTALLED});
- const root = findDOMNode(button);
- Simulate.click(root);
- assert(install.calledWith({guid, installURL, name}));
- });
-
- it('should call uninstall function on click when installed', () => {
- const guid = '@foo';
- const installURL = 'https://my.url/download';
- const name = 'hai';
- const type = 'whatevs';
- const uninstall = sinon.spy();
- const button = renderButton({guid, installURL, name, status: INSTALLED, type, uninstall});
- const root = findDOMNode(button);
- Simulate.click(root);
- assert(uninstall.calledWith({guid, installURL, name, type}));
- });
-
- it('should call setInitialStatus in componentDidMount', () => {
- const guid = '@foo';
- const installURL = 'http://the.url';
- const setInitialStatus = sinon.spy();
- renderButton({guid, installURL, setInitialStatus, status: UNKNOWN});
- assert(setInitialStatus.calledWith({guid, installURL}));
- });
-
- describe('mapStateToProps', () => {
- it('pulls the installation data from the state', () => {
- const addon = {
- guid: 'foo@addon',
- downloadProgress: 75,
- };
- assert.deepEqual(
- mapStateToProps({
- installations: {foo: {some: 'data'}, 'foo@addon': addon},
- addons: {'foo@addon': {addonProp: 'addonValue'}},
- }, {guid: 'foo@addon'}),
- {guid: 'foo@addon', downloadProgress: 75, addonProp: 'addonValue'});
- });
- });
-
- describe('makeProgressHandler', () => {
- it('sets the download progress on STATE_DOWNLOADING', () => {
- const dispatch = sinon.spy();
- const guid = 'foo@addon';
- const handler = makeProgressHandler(dispatch, guid);
- handler({state: 'STATE_DOWNLOADING', progress: 300, maxProgress: 990});
- assert(dispatch.calledWith({
- type: 'DOWNLOAD_PROGRESS',
- payload: {downloadProgress: 30, guid},
- }));
- });
-
- it('sets status to installing on STATE_INSTALLING', () => {
- const dispatch = sinon.spy();
- const guid = 'foo@my-addon';
- const handler = makeProgressHandler(dispatch, guid);
- handler({state: 'STATE_INSTALLING'});
- assert(dispatch.calledWith({
- type: 'START_INSTALL',
- payload: {guid},
- }));
- });
-
- it('sets status to installed on STATE_INSTALLED', () => {
- const dispatch = sinon.spy();
- const guid = '{my-addon}';
- const handler = makeProgressHandler(dispatch, guid);
- handler({state: 'STATE_INSTALLED'});
- assert(dispatch.calledWith({
- type: 'INSTALL_COMPLETE',
- payload: {guid},
- }));
- });
- });
-
- describe('setInitialStatus', () => {
- it('sets the status to INSTALLED when add-on found', () => {
- stubAddonManager();
- const dispatch = sinon.spy();
- const guid = '@foo';
- const installURL = 'http://the.url';
- const { setInitialStatus } = mapDispatchToProps(dispatch);
- return setInitialStatus({guid, installURL})
- .then(() => {
- assert(dispatch.calledWith({
- type: 'INSTALL_STATE',
- payload: {guid, status: INSTALLED, url: installURL},
- }));
- });
- });
-
- it('sets the status to INSTALLED when an installed theme is found', () => {
- stubAddonManager({getAddon: Promise.resolve({type: THEME_TYPE, isEnabled: true})});
- const dispatch = sinon.spy();
- const guid = '@foo';
- const installURL = 'http://the.url';
- const { setInitialStatus } = mapDispatchToProps(dispatch);
- return setInitialStatus({guid, installURL})
- .then(() => {
- assert(dispatch.calledWith({
- type: 'INSTALL_STATE',
- payload: {guid, status: INSTALLED, url: installURL},
- }));
- });
- });
-
- it('sets the status to UNINSTALLED when an uninstalled theme is found', () => {
- stubAddonManager({getAddon: Promise.resolve({type: THEME_TYPE, isEnabled: false})});
- const dispatch = sinon.spy();
- const guid = '@foo';
- const installURL = 'http://the.url';
- const { setInitialStatus } = mapDispatchToProps(dispatch);
- return setInitialStatus({guid, installURL})
- .then(() => {
- assert(dispatch.calledWith({
- type: 'INSTALL_STATE',
- payload: {guid, status: UNINSTALLED, url: installURL},
- }));
- });
- });
-
- it('sets the status to UNINSTALLED when not found', () => {
- stubAddonManager({getAddon: Promise.reject()});
- const dispatch = sinon.spy();
- const guid = '@foo';
- const installURL = 'http://the.url';
- const { setInitialStatus } = mapDispatchToProps(dispatch);
- return setInitialStatus({guid, installURL})
- .then(() => {
- assert(dispatch.calledWith({
- type: 'INSTALL_STATE',
- payload: {guid, status: UNINSTALLED, url: installURL},
- }));
- });
- });
- });
-
- describe('install', () => {
- const guid = '@install';
- const installURL = 'https://mysite.com/download.xpi';
-
- it('installs the addon on a new AddonManager', () => {
- stubAddonManager();
- const dispatch = sinon.spy();
- const { install } = mapDispatchToProps(dispatch);
- return install({guid, installURL})
- .then(() => {
- assert(addonManager.AddonManager.calledWithNew, 'new AddonManager() called');
- assert(addonManager.AddonManager.calledWith(guid, installURL, sinon.match.func));
- });
- });
-
- it('tracks an addon inntall', () => {
- stubAddonManager();
- const name = 'hai-addon';
- const type = 'extension';
- const dispatch = sinon.spy();
- const fakeTracking = {
- sendEvent: sinon.spy(),
- };
- const { install } = mapDispatchToProps(dispatch, {_tracking: fakeTracking});
- return install({guid, installURL, name, type})
- .then(() => {
- assert(fakeTracking.sendEvent.calledWith({
- action: 'addon',
- category: INSTALL_CATEGORY,
- label: 'hai-addon',
- }));
- });
- });
-
- it('should dispatch START_DOWNLOAD', () => {
- stubAddonManager();
- const dispatch = sinon.spy();
- const { install } = mapDispatchToProps(dispatch);
- return install({guid, installURL})
- .then(() => assert(dispatch.calledWith({
- type: 'START_DOWNLOAD',
- payload: {guid},
- })));
- });
- });
-
- describe('uninstall', () => {
- const guid = '@uninstall';
- const installURL = 'https://mysite.com/download.xpi';
-
- it('prepares the addon on a new AddonManager', () => {
- stubAddonManager();
- const dispatch = sinon.spy();
- const { uninstall } = mapDispatchToProps(dispatch);
- return uninstall({guid, installURL})
- .then(() => {
- assert(addonManager.AddonManager.calledWithNew, 'new AddonManager() called');
- assert(addonManager.AddonManager.calledWith(guid, installURL));
- });
- });
-
- it('tracks an addon uninstall', () => {
- stubAddonManager();
- const dispatch = sinon.spy();
- const name = 'whatevs';
- const type = 'extension';
- const fakeTracking = {
- sendEvent: sinon.spy(),
- };
- const { uninstall } = mapDispatchToProps(dispatch, {_tracking: fakeTracking});
- return uninstall({guid, installURL, name, type})
- .then(() => {
- assert.ok(fakeTracking.sendEvent.calledWith({
- action: 'addon',
- category: UNINSTALL_CATEGORY,
- label: 'whatevs',
- }), 'correctly called');
- });
- });
-
- it('tracks a theme uninstall', () => {
- stubAddonManager();
- const dispatch = sinon.spy();
- const name = 'whatevs';
- const type = 'persona';
- const fakeTracking = {
- sendEvent: sinon.spy(),
- };
- const { uninstall } = mapDispatchToProps(dispatch, {_tracking: fakeTracking});
- return uninstall({guid, installURL, name, type})
- .then(() => {
- assert(fakeTracking.sendEvent.calledWith({
- action: 'theme',
- category: UNINSTALL_CATEGORY,
- label: 'whatevs',
- }));
- });
- });
-
- it('tracks a unknown type uninstall', () => {
- stubAddonManager();
- const dispatch = sinon.spy();
- const name = 'whatevs';
- const type = 'foo';
- const fakeTracking = {
- sendEvent: sinon.spy(),
- };
- const { uninstall } = mapDispatchToProps(dispatch, {_tracking: fakeTracking});
- return uninstall({guid, installURL, name, type})
- .then(() => {
- assert(fakeTracking.sendEvent.calledWith({
- action: 'invalid',
- category: UNINSTALL_CATEGORY,
- label: 'whatevs',
- }));
- });
- });
-
- it('should dispatch START_UNINSTALL', () => {
- stubAddonManager();
- const dispatch = sinon.spy();
- const { uninstall } = mapDispatchToProps(dispatch);
- return uninstall({guid, installURL})
- .then(() => assert(dispatch.calledWith({
- type: 'START_UNINSTALL',
- payload: {guid},
- })));
- });
- });
-
- describe('installTheme', () => {
- it('installs the theme', () => {
- const name = 'hai-theme';
- const guid = '{install-theme}';
- const node = sinon.stub();
- const spyThemeAction = sinon.spy();
- const dispatch = sinon.spy();
- const { installTheme } = mapDispatchToProps(dispatch);
- return installTheme(node, guid, name, spyThemeAction)
- .then(() => {
- assert(spyThemeAction.calledWith(node, THEME_INSTALL));
- assert(dispatch.calledWith({
- type: 'INSTALL_STATE',
- payload: {guid, status: INSTALLED},
- }));
- });
- });
-
- it('tracks a theme install', () => {
- const name = 'hai-theme';
- const guid = '{install-theme}';
- const node = sinon.stub();
- const dispatch = sinon.spy();
- const spyThemeAction = sinon.spy();
- const fakeTracking = {
- sendEvent: sinon.spy(),
- };
- const { installTheme } = mapDispatchToProps(dispatch, {_tracking: fakeTracking});
- return installTheme(node, guid, name, spyThemeAction)
- .then(() => {
- assert(fakeTracking.sendEvent.calledWith({
- action: 'theme',
- category: INSTALL_CATEGORY,
- label: 'hai-theme',
- }));
- });
- });
- });
-
- describe('mapDispatchToProps', () => {
- it('is empty when there is no navigator', () => {
- const configStub = sinon.stub(config, 'get').returns(true);
- assert.deepEqual(mapDispatchToProps(sinon.spy()), {});
- assert(configStub.calledOnce);
- assert(configStub.calledWith('server'));
- });
- });
-});