Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/disco/components/Addon.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { PropTypes } from 'react';
import { sprintf } from 'sprintf-js';
import translate from 'core/i18n/translate';

import themeAction from 'disco/themePreview';
import themeAction, { getThemeData } from 'disco/themePreview';

import InstallButton from 'disco/containers/InstallButton';
import {
Expand Down Expand Up @@ -47,8 +47,7 @@ export class Addon extends React.Component {
}

getBrowserThemeData() {
const { id, name, headerURL, footerURL, textcolor, accentcolor } = this.props;
return JSON.stringify({id, name, headerURL, footerURL, textcolor, accentcolor});
return JSON.stringify(getThemeData(this.props));
}

getError() {
Expand Down
34 changes: 29 additions & 5 deletions src/disco/containers/InstallButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,28 @@ import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import translate from 'core/i18n/translate';

import 'disco/css/InstallButton.scss';
import config from 'config';
import { AddonManager } from 'disco/addonManager';
import {
DOWNLOADING,
INSTALLED,
THEME_INSTALL,
THEME_TYPE,
UNINSTALLED,
UNKNOWN,
validAddonTypes,
validInstallStates as validStates,
} from 'disco/constants';
import config from 'config';
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,
install: PropTypes.func.isRequired,
installTheme: PropTypes.func.isRequired,
i18n: PropTypes.object.isRequired,
installURL: PropTypes.string,
uninstall: PropTypes.func.isRequired,
Expand All @@ -26,6 +32,7 @@ export class InstallButton extends React.Component {
setInitialStatus: PropTypes.func.isRequired,
slug: PropTypes.string.isRequired,
status: PropTypes.oneOf(validStates),
type: PropTypes.oneOf(validAddonTypes),
}

static defaultProps = {
Expand All @@ -39,8 +46,10 @@ export class InstallButton extends React.Component {
}

handleClick = () => {
const { guid, install, installURL, slug, status, uninstall } = this.props;
if (status === UNINSTALLED) {
const { guid, install, installURL, slug, status, installTheme, type, uninstall } = this.props;
if (type === THEME_TYPE && status === UNINSTALLED) {
installTheme(this.refs.themeData, slug);
} else if (status === UNINSTALLED) {
install({ guid, installURL, slug });
} else if (status === INSTALLED) {
uninstall({ guid, installURL, slug });
Expand Down Expand Up @@ -69,6 +78,8 @@ export class InstallButton extends React.Component {
checked={isInstalled}
disabled={isDisabled}
onChange={this.props.handleChange}
data-browsertheme={JSON.stringify(getThemeData(this.props))}
ref="themeData"
type="checkbox" />
<label htmlFor={identifier}>
{isDownloading ? <div className="progress"></div> : null}
Expand Down Expand Up @@ -109,7 +120,10 @@ export function mapDispatchToProps(dispatch) {
const payload = {guid, slug, url: installURL};
return addonManager.getAddon()
.then(
() => dispatch({type: 'INSTALL_STATE', payload: {...payload, status: INSTALLED}}),
(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}}));
},

Expand All @@ -119,6 +133,16 @@ export function mapDispatchToProps(dispatch) {
return addonManager.install();
},

installTheme(node, slug, _themeAction = themeAction) {
_themeAction(node, THEME_INSTALL);
return new Promise((resolve) => {
setTimeout(() => {
dispatch({type: 'INSTALL_STATE', payload: {slug, status: INSTALLED}});
resolve();
}, 250);
});
},

uninstall({ guid, installURL, slug }) {
const addonManager = new AddonManager(guid, installURL);
dispatch({type: 'START_UNINSTALL', payload: {slug}});
Expand Down
1 change: 1 addition & 0 deletions src/disco/fakeData.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default {
editorial_description: 'Hover over the image to preview',
heading: 'Japanese Tattoo by MaDonna',
id: '18781',
guid: '18781@personas.mozilla.org',
name: 'Japanese Tattoo',
slug: 'japanese-tattoo',
sub_heading: null,
Expand Down
5 changes: 5 additions & 0 deletions src/disco/themePreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ export default function themeAction(node, action, _doc = document) {
event.initEvent(action, true, false);
node.dispatchEvent(event);
}

export function getThemeData({ id, name, headerURL, footerURL, textcolor, accentcolor }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks a bit odd, but looks like this will do the job of ensuring the shape of the data object?

Maybe a comment could be added in case someone looks at this and thinks it should be factored out.

// This extracts the relevant theme data from the larger add-on data object.
return {id, name, headerURL, footerURL, textcolor, accentcolor};
}
67 changes: 65 additions & 2 deletions tests/client/disco/containers/TestInstallButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
DOWNLOADING,
INSTALLED,
INSTALLING,
THEME_INSTALL,
THEME_TYPE,
UNINSTALLED,
UNINSTALLING,
UNKNOWN,
Expand All @@ -27,6 +29,7 @@ describe('<InstallButton />', () => {
dispatch: sinon.spy(),
setInitialStatus: sinon.spy(),
install: sinon.spy(),
installTheme: sinon.spy(),
uninstall: sinon.spy(),
i18n: getFakeI18nInst(),
...props,
Expand Down Expand Up @@ -104,6 +107,16 @@ describe('<InstallButton />', () => {
}, Error, 'Invalid add-on status');
});

it('should call installTheme function on click when uninstalled theme', () => {
const installTheme = sinon.spy();
const slug = 'my-theme';
const button = renderButton({installTheme, type: THEME_TYPE, slug, status: UNINSTALLED});
const themeData = button.refs.themeData;
const root = findDOMNode(button);
Simulate.click(root);
assert(installTheme.calledWith(themeData, slug));
});

it('should call install function on click when uninstalled', () => {
const guid = '@foo';
const install = sinon.spy();
Expand Down Expand Up @@ -186,7 +199,7 @@ describe('<InstallButton />', () => {
});

describe('setInitialStatus', () => {
it('sets the status to INSTALLED when found', () => {
it('sets the status to INSTALLED when add-on found', () => {
stubAddonManager();
const dispatch = sinon.spy();
const guid = '@foo';
Expand All @@ -202,6 +215,38 @@ describe('<InstallButton />', () => {
});
});

it('sets the status to INSTALLED when an installed theme is found', () => {
stubAddonManager({getAddon: Promise.resolve({type: 'theme', isEnabled: true})});
const dispatch = sinon.spy();
const guid = '@foo';
const slug = 'foo';
const installURL = 'http://the.url';
const { setInitialStatus } = mapDispatchToProps(dispatch);
return setInitialStatus({guid, installURL, slug})
.then(() => {
assert(dispatch.calledWith({
type: 'INSTALL_STATE',
payload: {guid, slug, status: INSTALLED, url: installURL},
}));
});
});

it('sets the status to UNINSTALLED when an uninstalled theme is found', () => {
stubAddonManager({getAddon: Promise.resolve({type: 'theme', isEnabled: false})});
const dispatch = sinon.spy();
const guid = '@foo';
const slug = 'foo';
const installURL = 'http://the.url';
const { setInitialStatus } = mapDispatchToProps(dispatch);
return setInitialStatus({guid, installURL, slug})
.then(() => {
assert(dispatch.calledWith({
type: 'INSTALL_STATE',
payload: {guid, slug, status: UNINSTALLED, url: installURL},
}));
});
});

it('sets the status to UNINSTALLED when not found', () => {
stubAddonManager({getAddon: Promise.reject()});
const dispatch = sinon.spy();
Expand Down Expand Up @@ -252,7 +297,7 @@ describe('<InstallButton />', () => {
const installURL = 'https://mysite.com/download.xpi';
const slug = 'uninstall';

it('installs the addon on a new AddonManager', () => {
it('prepares the addon on a new AddonManager', () => {
stubAddonManager();
const dispatch = sinon.spy();
const { uninstall } = mapDispatchToProps(dispatch);
Expand All @@ -275,6 +320,24 @@ describe('<InstallButton />', () => {
});
});

describe('installTheme', () => {
it('installs the theme', () => {
const node = sinon.stub();
const slug = 'install-theme';
const spyThemeAction = sinon.spy();
const dispatch = sinon.spy();
const { installTheme } = mapDispatchToProps(dispatch);
return installTheme(node, slug, spyThemeAction)
.then(() => {
assert(spyThemeAction.calledWith(node, THEME_INSTALL));
assert(dispatch.calledWith({
type: 'INSTALL_STATE',
payload: {slug, status: INSTALLED},
}));
});
});
});

describe('mapDispatchToProps', () => {
it('is empty when there is no navigator', () => {
const configStub = sinon.stub(config, 'get').returns(true);
Expand Down
2 changes: 1 addition & 1 deletion tests/client/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function findByTag(root, tag) {
return matches[0];
}

export function stubAddonManager({ getAddon = Promise.resolve() } = {}) {
export function stubAddonManager({ getAddon = Promise.resolve({type: 'addon'}) } = {}) {
const instance = sinon.createStubInstance(AddonManager);
instance.getAddon = sinon.stub().returns(getAddon);
instance.install = sinon.stub().returns(Promise.resolve());
Expand Down
4 changes: 2 additions & 2 deletions webpack.dev.config.babel.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const webpackHost = config.get('webpackServerHost');
const webpackPort = config.get('webpackServerPort');
const assetsPath = path.resolve(__dirname, 'dist');

const hmr = `webpack-hot-middleware/client?path=http://${webpackHost}:${webpackPort}/__webpack_hmr`;
const hmr = `webpack-hot-middleware/client?path=//${webpackHost}:${webpackPort}/__webpack_hmr`;

const appName = config.get('appName');
const appsBuildList = appName ? [appName] : config.get('validAppNames');
Expand All @@ -59,7 +59,7 @@ export default Object.assign({}, webpackConfig, {
path: assetsPath,
filename: '[name]-[hash].js',
chunkFilename: '[name]-[hash].js',
publicPath: `http://${webpackHost}:${webpackPort}/`,
publicPath: `//${webpackHost}:${webpackPort}/`,
}),
module: {
loaders: [{
Expand Down