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
66 changes: 58 additions & 8 deletions src/disco/components/Addon.js
Original file line number Diff line number Diff line change
@@ -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,
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 like it should be camelCased.

Copy link
Contributor Author

@muffinresearch muffinresearch May 10, 2016

Choose a reason for hiding this comment

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

This is the key expected by the theme preview (not sure why) - so I kept it simple rather than making the component have a different key in this case.

I'll do a test and see if it works with camel-case instead if it does we can change it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep it needs to be the lower-case key.

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,
Copy link
Contributor

Choose a reason for hiding this comment

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

This also looks like it should be camelCased.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As above

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 <div className="logo"><img src={imageURL} alt="" /></div>;
}
return null;
}

getThemeImage() {
const { id } = this.props;
const { id, name } = this.props;
const themeURL = `https://addons-dev-cdn.allizom.org/user-media/addons/${id}/preview_large.jpg?1239806327`;
Copy link
Contributor

@mstriemer mstriemer May 9, 2016

Choose a reason for hiding this comment

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

The headerURL and footerURL are included in the fake data. Should this go in there too? I don't think we'll be constructing this by hand. Same with getLogo()/imageURL.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The expectation is we should be handed URLs so yes it would make sense to move to that rather than building the URLS here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've filed #348 to deal with that separately.

if (this.props.type === 'Theme') {
return <img className="theme-image" src={themeURL} alt="" />;
if (this.props.type === THEME_TYPE) {
return (<a href="#" className="theme-image"
data-browsertheme={this.getBrowserThemeData()}
onBlur={this.resetPreviewTheme}
onClick={this.handleClick}
onFocus={this.previewTheme}
onMouseOut={this.resetPreviewTheme}
onMouseOver={this.previewTheme}>
<img src={themeURL} alt={_(`Preview ${name}`)} /></a>);
}
return null;
}
Expand All @@ -37,11 +69,29 @@ export default class Addon extends React.Component {
return { __html: this.props.editorialDescription };
}

handleClick = (e) => {
e.preventDefault();
}

previewTheme = (e) => {
this.props.themeAction(e.currentTarget, THEME_PREVIEW);
}

resetPreviewTheme = (e) => {
this.props.themeAction(e.currentTarget, THEME_RESET_PREVIEW);
}

render() {
const { heading, subHeading, type } = this.props;

if (validAddonTypes.indexOf(type) === -1) {
throw new Error('Invalid addon type');
}

const addonClasses = classNames('addon', {
theme: type === 'Theme',
theme: type === THEME_TYPE,
});

return (
<div className={addonClasses}>
{this.getThemeImage()}
Expand Down
19 changes: 19 additions & 0 deletions src/disco/constants.js
Original file line number Diff line number Diff line change
@@ -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,
];
14 changes: 12 additions & 2 deletions src/disco/css/Addon.scss
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
@import "~disco/css/inc/vars";
@import "~core/css/inc/mixins";

$addon-padding: 20px;
$primary-text-color: #000;
$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 {
Expand Down
1 change: 1 addition & 0 deletions src/disco/css/inc/vars.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$focus-outline-color: #0096dc;
6 changes: 5 additions & 1 deletion src/disco/fakeData.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<em>&ldquo;This add-on is amazing.&rdquo;</em> — Someone',
heading: 'Something something',
Expand Down
10 changes: 10 additions & 0 deletions src/disco/themePreview.js
Original file line number Diff line number Diff line change
@@ -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);
}
29 changes: 29 additions & 0 deletions tests/client/disco/TestThemePreview.js
Original file line number Diff line number Diff line change
@@ -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');
});
});

88 changes: 73 additions & 15 deletions tests/client/disco/components/TestAddon.js
Original file line number Diff line number Diff line change
@@ -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('<Addon />', () => {
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('<Addon type="Extension"/>', () => {
let root;
const result = {
id: 'test-id',
type: 'Extension',
heading: 'test-heading',
subHeading: 'test-sub-heading',
editorialDescription: 'test-editorial-description',
};

beforeEach(() => {
root = renderIntoDocument(<Addon { ...result } />);
root = renderIntoDocument(<Addon {...result} />);
});

it('renders the heading', () => {
Expand All @@ -30,7 +34,7 @@ describe('<Addon />', () => {
});

it("doesn't render the subheading when not present", () => {
const data = { ...result, subHeading: undefined };
const data = {...result, subHeading: undefined};
root = renderIntoDocument(<Addon {...data} />);
assert.notEqual(root.refs.heading.textContent, 'test-sub-heading');
});
Expand All @@ -43,15 +47,69 @@ describe('<Addon />', () => {
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(<Addon {...data} />);
}, Error, 'Invalid addon type');
});
});


describe('<Addon type="Theme"/>', () => {
let root;

beforeEach(() => {
const data = {...result, type: 'Theme'};
root = renderIntoDocument(<Addon {...data} />);
});

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(<Addon {...data} />);
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(<Addon {...data} />);
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);
});
});