diff --git a/src/disco/components/Addon.js b/src/disco/components/Addon.js
index 80cab9843bd..b439b1c536c 100644
--- a/src/disco/components/Addon.js
+++ b/src/disco/components/Addon.js
@@ -1,34 +1,66 @@
import React, { PropTypes } from 'react';
import classNames from 'classnames';
+import themeAction from 'disco/themePreview';
+import { gettext as _ } from 'core/utils';
import InstallButton from './InstallButton';
+import {
+ validAddonTypes,
+ EXTENSION_TYPE,
+ THEME_TYPE,
+ THEME_PREVIEW,
+ THEME_RESET_PREVIEW,
+} from 'disco/constants';
import 'disco/css/Addon.scss';
export default class Addon extends React.Component {
static propTypes = {
- id: PropTypes.number.isRequired,
- type: PropTypes.string.isRequired,
+ accentcolor: PropTypes.string,
+ editorialDescription: PropTypes.string.isRequired,
+ footerURL: PropTypes.string,
+ headerURL: PropTypes.string,
heading: PropTypes.string.isRequired,
+ id: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
subHeading: PropTypes.string,
- editorialDescription: PropTypes.string.isRequired,
+ textcolor: PropTypes.string,
+ type: PropTypes.oneOf(validAddonTypes).isRequired,
+ themeAction: PropTypes.func,
+ }
+
+ static defaultProps = {
+ // Defaults themeAction to the imported func.
+ themeAction,
+ }
+
+ getBrowserThemeData() {
+ const { id, name, headerURL, footerURL, textcolor, accentcolor } = this.props;
+ return JSON.stringify({id, name, headerURL, footerURL, textcolor, accentcolor});
}
getLogo() {
const { id } = this.props;
const imageURL = `https://addons-dev-cdn.allizom.org/user-media/addon_icons/0/${id}-64.png?modified=1388632826`;
- if (this.props.type === 'Extension') {
+ if (this.props.type === EXTENSION_TYPE) {
return
{this.getThemeImage()}
diff --git a/src/disco/constants.js b/src/disco/constants.js
index 91059fff6bb..6031a7c7ff0 100644
--- a/src/disco/constants.js
+++ b/src/disco/constants.js
@@ -1,6 +1,25 @@
+// Addon States.
export const DOWNLOADING = 'downloading';
export const INSTALLED = 'installed';
export const INSTALLING = 'installing';
export const UNINSTALLED = 'uninstalled';
export const UNINSTALLING = 'uninstalling';
export const UNKNOWN = 'unknown';
+
+// Add-on types.
+export const THEME_TYPE = 'Theme';
+export const EXTENSION_TYPE = 'Extension';
+export const validAddonTypes = [
+ THEME_TYPE,
+ EXTENSION_TYPE,
+];
+
+// Theme preview actions.
+export const THEME_INSTALL = 'InstallBrowserTheme';
+export const THEME_PREVIEW = 'PreviewBrowserTheme';
+export const THEME_RESET_PREVIEW = 'ResetBrowserThemePreview';
+export const validThemeActions = [
+ THEME_INSTALL,
+ THEME_PREVIEW,
+ THEME_RESET_PREVIEW,
+];
diff --git a/src/disco/css/Addon.scss b/src/disco/css/Addon.scss
index a9432a2860e..c41a142a1a2 100644
--- a/src/disco/css/Addon.scss
+++ b/src/disco/css/Addon.scss
@@ -1,3 +1,6 @@
+@import "~disco/css/inc/vars";
+@import "~core/css/inc/mixins";
+
$addon-padding: 20px;
$primary-text-color: #000;
$secondary-text-color: #6a6a6a;
@@ -5,11 +8,18 @@ $secondary-text-color: #6a6a6a;
.addon {
background: #fff;
margin-top: 20px;
- overflow: hidden;
.theme-image {
display: block;
- width: 100%;
+
+ img {
+ display: block;
+ }
+
+ &:hover,
+ &:focus {
+ @include focus();
+ }
}
.install-button {
diff --git a/src/disco/css/inc/vars.scss b/src/disco/css/inc/vars.scss
new file mode 100644
index 00000000000..bb1e607325e
--- /dev/null
+++ b/src/disco/css/inc/vars.scss
@@ -0,0 +1 @@
+$focus-outline-color: #0096dc;
diff --git a/src/disco/fakeData.js b/src/disco/fakeData.js
index 50d8dcb7d8e..23aef4b866e 100644
--- a/src/disco/fakeData.js
+++ b/src/disco/fakeData.js
@@ -2,11 +2,15 @@ export default {
results: [{
editorial_description: 'Hover over the image to preview',
heading: 'Japanese Tattoo by MaDonna',
- id: 18781,
+ id: '18781',
name: 'Japanese Tattoo',
sub_heading: null,
type: 'Theme',
url: 'https://addons-dev.allizom.org/en-US/firefox/addon/japanese-tattoo/',
+ headerURL: 'https://addons-dev-cdn.allizom.org/user-media/addons/18781/personare.jpg?1239806327',
+ footerURL: 'https://addons-dev-cdn.allizom.org/user-media/addons/18781/persona2re.jpg?1239806327',
+ textcolor: '#000000',
+ accentcolor: '#ffffff',
}, {
editorial_description: '
“This add-on is amazing.” — Someone',
heading: 'Something something',
diff --git a/src/disco/themePreview.js b/src/disco/themePreview.js
new file mode 100644
index 00000000000..eb4c5a84d35
--- /dev/null
+++ b/src/disco/themePreview.js
@@ -0,0 +1,10 @@
+import { validThemeActions } from 'disco/constants';
+
+export default function themeAction(node, action, _doc = document) {
+ if (validThemeActions.indexOf(action) === -1) {
+ throw new Error('Invalid theme action requested');
+ }
+ const event = _doc.createEvent('Events');
+ event.initEvent(action, true, false);
+ node.dispatchEvent(event);
+}
diff --git a/tests/client/disco/TestThemePreview.js b/tests/client/disco/TestThemePreview.js
new file mode 100644
index 00000000000..4d8f6c99be2
--- /dev/null
+++ b/tests/client/disco/TestThemePreview.js
@@ -0,0 +1,29 @@
+import themeAction from 'disco/themePreview';
+import { THEME_PREVIEW } from 'disco/constants';
+
+
+describe('Theme Preview Lib', () => {
+ it('throws for invalid action', () => {
+ assert.throws(() => {
+ themeAction(null, 'whatever');
+ }, Error, 'Invalid theme action requested');
+ });
+
+ it('sets-up the event for previews', () => {
+ const fakeNode = {
+ dispatchEvent: sinon.stub(),
+ };
+ const fakeEvent = {
+ initEvent: sinon.stub(),
+ };
+ const fakeDoc = {
+ createEvent: sinon.stub(),
+ };
+ fakeDoc.createEvent.returns(fakeEvent);
+ themeAction(fakeNode, THEME_PREVIEW, fakeDoc);
+ assert.ok(fakeDoc.createEvent.calledWith('Events'), 'Should call createEvent');
+ assert.ok(fakeEvent.initEvent.calledWith(THEME_PREVIEW, true, false), 'Should call initEvent');
+ assert.ok(fakeNode.dispatchEvent.calledWith(fakeEvent), 'should call dispatchEvent');
+ });
+});
+
diff --git a/tests/client/disco/components/TestAddon.js b/tests/client/disco/components/TestAddon.js
index 149839890a8..621f025783e 100644
--- a/tests/client/disco/components/TestAddon.js
+++ b/tests/client/disco/components/TestAddon.js
@@ -1,20 +1,24 @@
import React from 'react';
-import { renderIntoDocument } from 'react-addons-test-utils';
+import { renderIntoDocument, Simulate } from 'react-addons-test-utils';
import { findDOMNode } from 'react-dom';
import Addon from 'disco/components/Addon';
-describe('
', () => {
+import { THEME_PREVIEW, THEME_RESET_PREVIEW } from 'disco/constants';
+
+
+const result = {
+ id: 'test-id',
+ type: 'Extension',
+ heading: 'test-heading',
+ subHeading: 'test-sub-heading',
+ editorialDescription: 'test-editorial-description',
+};
+
+describe('
', () => {
let root;
- const result = {
- id: 'test-id',
- type: 'Extension',
- heading: 'test-heading',
- subHeading: 'test-sub-heading',
- editorialDescription: 'test-editorial-description',
- };
beforeEach(() => {
- root = renderIntoDocument(
);
+ root = renderIntoDocument(
);
});
it('renders the heading', () => {
@@ -30,7 +34,7 @@ describe('
', () => {
});
it("doesn't render the subheading when not present", () => {
- const data = { ...result, subHeading: undefined };
+ const data = {...result, subHeading: undefined};
root = renderIntoDocument(
);
assert.notEqual(root.refs.heading.textContent, 'test-sub-heading');
});
@@ -43,15 +47,69 @@ describe('
', () => {
assert.equal(findDOMNode(root).querySelector('.theme-image'), null);
});
- it('does render the theme image for a theme', () => {
- const data = { ...result, type: 'Theme' };
+ it('throws on invalid add-on type', () => {
+ assert.include(root.refs.heading.textContent, 'test-heading');
+ const data = {...result, type: 'Whatever'};
+ assert.throws(() => {
+ renderIntoDocument(
);
+ }, Error, 'Invalid addon type');
+ });
+});
+
+
+describe('
', () => {
+ let root;
+
+ beforeEach(() => {
+ const data = {...result, type: 'Theme'};
root = renderIntoDocument(
);
+ });
+
+ it('does render the theme image for a theme', () => {
assert.ok(findDOMNode(root).querySelector('.theme-image'));
});
it("doesn't render the logo for a theme", () => {
- const data = { ...result, type: 'Theme' };
- root = renderIntoDocument(
);
assert.notOk(findDOMNode(root).querySelector('.logo'));
});
});
+
+
+describe('Theme Previews', () => {
+ let root;
+ let themeImage;
+ let themeAction;
+
+ beforeEach(() => {
+ themeAction = sinon.stub();
+ const data = {...result, type: 'Theme', themeAction};
+ root = renderIntoDocument(
);
+ themeImage = findDOMNode(root).querySelector('.theme-image');
+ });
+
+ it('runs theme preview onMouseOver on theme image', () => {
+ Simulate.mouseOver(themeImage);
+ assert.ok(themeAction.calledWith(themeImage, THEME_PREVIEW));
+ });
+
+ it('resets theme preview onMouseOut on theme image', () => {
+ Simulate.mouseOut(themeImage);
+ assert.ok(themeAction.calledWith(themeImage, THEME_RESET_PREVIEW));
+ });
+
+ it('runs theme preview onFocus on theme image', () => {
+ Simulate.focus(themeImage);
+ assert.ok(themeAction.calledWith(themeImage, THEME_PREVIEW));
+ });
+
+ it('resets theme preview onBlur on theme image', () => {
+ Simulate.blur(themeImage);
+ assert.ok(themeAction.calledWith(themeImage, THEME_RESET_PREVIEW));
+ });
+
+ it('runs preventDefault onClick', () => {
+ const preventDefault = sinon.stub();
+ Simulate.click(themeImage, {preventDefault});
+ assert.ok(preventDefault.called);
+ });
+});