diff --git a/.eslintignore b/.eslintignore index 974862bb06..40b14045ed 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,3 +3,4 @@ logs/ firefox/ lib/vendor.bundle.js activity-streams-env/ +!.* diff --git a/.storybook/config.js b/.storybook/config.js new file mode 100644 index 0000000000..48e5074b37 --- /dev/null +++ b/.storybook/config.js @@ -0,0 +1,9 @@ +const {configure} = require("@kadira/storybook"); + +function loadStories() { + require("../content-test/components/Spotlight.story"); + require("../content-test/components/ContextMenu.story"); + // require as many stories as you need. +} + +configure(loadStories, module); diff --git a/.storybook/head.html b/.storybook/head.html new file mode 100644 index 0000000000..aa95183a76 --- /dev/null +++ b/.storybook/head.html @@ -0,0 +1 @@ + diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js new file mode 100644 index 0000000000..70dcd78216 --- /dev/null +++ b/.storybook/webpack.config.js @@ -0,0 +1,19 @@ +"use strict"; +const {plugins, resolve} = require("../webpack.common"); + +module.exports = storybookBaseConfig => + Object.assign(storybookBaseConfig, { + resolve, + module: { + loaders: [ + {test: /\.json$/, loader: "json"}, + { + test: /\.jsx?$/, + exclude: /node_modules/, + loader: "babel" + } + ] + }, + devtool: "eval-sourcemap", + plugins: storybookBaseConfig.plugins.concat(plugins) + }); diff --git a/content-src/components/ActivityFeed/ActivityFeed.scss b/content-src/components/ActivityFeed/ActivityFeed.scss index aab22afe55..e4e71094dc 100644 --- a/content-src/components/ActivityFeed/ActivityFeed.scss +++ b/content-src/components/ActivityFeed/ActivityFeed.scss @@ -83,7 +83,7 @@ } &.bookmark .star { - background: url('img/glyph-bookmark-16-blue.svg') no-repeat center center; + background: url('#{$image-path}glyph-bookmark-16-blue.svg') no-repeat center center; background-size: 18px; border: 0; height: 18px; diff --git a/content-src/components/LinkMenuButton/LinkMenuButton.scss b/content-src/components/LinkMenuButton/LinkMenuButton.scss index 662f236d5c..87300ad97f 100644 --- a/content-src/components/LinkMenuButton/LinkMenuButton.scss +++ b/content-src/components/LinkMenuButton/LinkMenuButton.scss @@ -6,7 +6,7 @@ width: $link-menu-button-size; height: $link-menu-button-size; background-color: $white; - background-image: url('img/glyph-more-16.svg'); + background-image: url('#{$image-path}glyph-more-16.svg'); background-position: 65%; background-repeat: no-repeat; background-clip: padding-box; diff --git a/content-src/components/LoadMore/LoadMore.scss b/content-src/components/LoadMore/LoadMore.scss index 3c46220b94..c55414ecc8 100644 --- a/content-src/components/LoadMore/LoadMore.scss +++ b/content-src/components/LoadMore/LoadMore.scss @@ -16,7 +16,7 @@ display: inline-block; width: 15px; height: 15px; - background-image: url('img/glyph-showmore-16.svg'); + background-image: url('#{$image-path}glyph-showmore-16.svg'); background-size: 15px; } diff --git a/content-src/components/Loader/Loader.scss b/content-src/components/Loader/Loader.scss index 3ffe698c9d..8b60983013 100644 --- a/content-src/components/Loader/Loader.scss +++ b/content-src/components/Loader/Loader.scss @@ -14,7 +14,7 @@ width: $loader-size; height: $loader-size; display: inline-block; - background-image: url('img/loading@2x.png'); + background-image: url('#{$image-path}loading@2x.png'); background-size: $loader-size $loader-size; } } diff --git a/content-src/components/MediaPreview/MediaPreview.scss b/content-src/components/MediaPreview/MediaPreview.scss index 7a1a2e9d58..9017e381b0 100644 --- a/content-src/components/MediaPreview/MediaPreview.scss +++ b/content-src/components/MediaPreview/MediaPreview.scss @@ -14,13 +14,13 @@ content: ''; width: 65px; height: 40px; - background: url('img/playButton@2x.png') no-repeat center center; + background: url('#{$image-path}playButton@2x.png') no-repeat center center; background-size: 65px 40px; display: block; } &:hover::after { - background-image: url('img/playButton-hover@2x.png'); + background-image: url('#{$image-path}playButton-hover@2x.png'); } &.isPlaying::after { diff --git a/content-src/components/Search/Search.scss b/content-src/components/Search/Search.scss index a658c8b08b..2a2085360f 100644 --- a/content-src/components/Search/Search.scss +++ b/content-src/components/Search/Search.scss @@ -64,7 +64,7 @@ &:hover > #historyIcon, &.active > #historyIcon { - background-image: url('img/glyph-search-history.svg#search-history-active'); + background-image: url('#{$image-path}glyph-search-history.svg#search-history-active'); } } } @@ -76,7 +76,7 @@ display: inline-block; margin-right: 10px; margin-bottom: -3px; - background-image: url('img/glyph-search-history.svg#search-history'); + background-image: url('#{$image-path}glyph-search-history.svg#search-history'); } .search-partners { @@ -150,13 +150,13 @@ transition: box-shadow 150ms; box-shadow: 0 0 0 0.5px $search-shadow; background-color: $search-blue; - background-image: url('img/glyph-forward-16-white.svg'); + background-image: url('#{$image-path}glyph-forward-16-white.svg'); color: $white; } } .search-label { - background: url('img/glyph-search-16.svg') no-repeat center center; + background: url('#{$image-path}glyph-search-16.svg') no-repeat center center; background-size: 20px; position: absolute; top: 0; @@ -177,7 +177,7 @@ padding: 0; transition: box-shadow 150ms; box-shadow: none; - background: $search-button-grey url('img/glyph-forward-16.svg') no-repeat center center; + background: $search-button-grey url('#${image-path}glyph-forward-16.svg') no-repeat center center; background-size: 16px 16px; &:hover { @@ -186,7 +186,7 @@ transition: box-shadow 150ms; box-shadow: 0 0 0 0.5px $search-shadow; background-color: $search-blue; - background-image: url('img/glyph-forward-16-white.svg'); + background-image: url('#{$image-path}glyph-forward-16-white.svg'); color: $white; } } diff --git a/content-src/main.js b/content-src/main.js index 3a79fb4388..8ea83b828b 100644 --- a/content-src/main.js +++ b/content-src/main.js @@ -18,7 +18,7 @@ const Root = React.createClass({ }); function renderRootWhenAddonIsReady() { - if (window.navigator.activity_streams_addon) { + if (window.navigator.activity_streams_addon || __CONFIG__.USE_SHIM) { ReactDOM.render(, document.getElementById("root")); } else { // If the content bridge to the addon isn't set up yet, try again soon. diff --git a/content-src/styles/icons.scss b/content-src/styles/icons.scss index 2fbfc23f08..2ec09190b5 100644 --- a/content-src/styles/icons.scss +++ b/content-src/styles/icons.scss @@ -12,86 +12,86 @@ } &.icon-activity-stream { - background-image: url('img/glyph-activityStream-16.svg'); + background-image: url('#{$image-path}glyph-activityStream-16.svg'); } &.icon-arrow { - background-image: url('img/glyph-showmore-16.svg'); + background-image: url('#{$image-path}glyph-showmore-16.svg'); } &.icon-bookmark { - background-image: url('img/glyph-bookmark-16.svg'); + background-image: url('#{$image-path}glyph-bookmark-16.svg'); } &.icon-bookmark-white { - background-image: url('img/glyph-bookmark-16-white.svg'); + background-image: url('#{$image-path}glyph-bookmark-16-white.svg'); } &.icon-bookmark-remove { - background-image: url('img/glyph-bookmark-remove-16.svg'); + background-image: url('#{$image-path}glyph-bookmark-remove-16.svg'); } &.icon-delete { - background-image: url('img/glyph-delete-16.svg'); + background-image: url('#{$image-path}glyph-delete-16.svg'); } &.icon-dismiss { - background-image: url('img/glyph-dismiss-16.svg'); + background-image: url('#{$image-path}glyph-dismiss-16.svg'); } &.icon-firefox-white { - background-image: url('img/glyph-firefox-white-16.svg'); + background-image: url('#{$image-path}glyph-firefox-white-16.svg'); } &.icon-new-window { - background-image: url('img/glyph-newWindow-16.svg'); + background-image: url('#{$image-path}glyph-newWindow-16.svg'); } &.icon-new-window-private { - background-image: url('img/glyph-newWindow-private-16.svg'); + background-image: url('#{$image-path}glyph-newWindow-private-16.svg'); } &.icon-forward { - background-image: url('img/glyph-forward-16.svg'); + background-image: url('#{$image-path}glyph-forward-16.svg'); } &.icon-search { - background-image: url('img/glyph-search-16.svg'); + background-image: url('#{$image-path}glyph-search-16.svg'); } &.icon-settings { - background-image: url('img/glyph-settings-16.svg'); + background-image: url('#{$image-path}glyph-settings-16.svg'); } &.icon-showMore { - background-image: url('img/glyph-showmore-16.svg'); + background-image: url('#{$image-path}glyph-showmore-16.svg'); } &.icon-timeline { - background-image: url('img/list-icon.svg'); + background-image: url('#{$image-path}list-icon.svg'); } &.icon-check { - background-image: url('img/glyph-check-16.svg'); + background-image: url('#{$image-path}glyph-check-16.svg'); } &.icon-pocket { - background-image: url('img/glyph-pocket-16.svg'); + background-image: url('#{$image-path}glyph-pocket-16.svg'); } &.icon-historyItem { - background-image: url('img/glyph-historyItem-16.svg'); + background-image: url('#{$image-path}glyph-historyItem-16.svg'); } &.icon-sync { - background-image: url('img/glyph-sync-16.svg'); + background-image: url('#{$image-path}glyph-sync-16.svg'); } &.icon-tab { - background-image: url('img/glyph-tab-16.svg'); + background-image: url('#{$image-path}glyph-tab-16.svg'); } &.icon-topic { - background-image: url('img/glyph-topic-16.svg'); + background-image: url('#{$image-path}glyph-topic-16.svg'); } } diff --git a/content-src/styles/variables.scss b/content-src/styles/variables.scss index 3ca67659cb..61eb23307f 100644 --- a/content-src/styles/variables.scss +++ b/content-src/styles/variables.scss @@ -107,6 +107,8 @@ $context-menu-item-padding: 3px 12px; $icon-size: 16px; +$image-path: 'img/'; + $tooltip-height: 50px; $tooltip-background-color: #FFF3CE; $tooltip-border-radius: 5px; diff --git a/content-test/components/ContextMenu.story.js b/content-test/components/ContextMenu.story.js new file mode 100644 index 0000000000..ce111b9ae0 --- /dev/null +++ b/content-test/components/ContextMenu.story.js @@ -0,0 +1,46 @@ +const React = require("react"); +const {storiesOf} = require("@kadira/storybook"); +const ContextMenu = require("components/ContextMenu/ContextMenu"); + +const DEFAULT_OPTIONS = [ + {label: "Apples", onClick: () => {}}, + {label: "Oranges", onClick: () => {}}, + {label: "Grapes", onClick: () => {}} +]; + +// This is so we can show the context menu. Note that this is only +// necessary because ContextMenu has certain implicit dependencies on +// its container. Fully encapsulated components (hopefully any new +// new ones!) should be renderable without needing a container! +const ContextMenuContainer = React.createClass({ + getInitialState() { + return {visible: true}; + }, + render() { + const style = { + position: "relative", + display: "flex", + alignItems: "center", + justifyContent: "center", + width: 150, + height: 60, + border: "1px dashed #DDD" + }; + const buttonStyle = { + fontSize: 11, + display: "inline-block" + }; + return (
+ this.setState({visible})} /> + +
); + } +}); + +storiesOf("ContextMenu", module) + .add("default props", () => ( + + )); diff --git a/content-test/components/Spotlight.story.js b/content-test/components/Spotlight.story.js new file mode 100644 index 0000000000..1ddaa8a94c --- /dev/null +++ b/content-test/components/Spotlight.story.js @@ -0,0 +1,47 @@ +const React = require("react"); +const {SpotlightItem} = require("components/Spotlight/Spotlight"); +const {storiesOf, action} = require("@kadira/storybook"); +const {createMockProvider} = require("../test-utils"); +const {createSite} = require("../faker"); +const Provider = createMockProvider({dispatch: action("dispatched a redux action")}); + +// XXX should get rid of container here. See comment in ContextMenu.story +// for details. +const Container = props => ( + +
+ {props.children} +
+
+); + +// Note that if a site image is not in the cache, it can take a while +// (eg 10 seconds) to load, because the image load starts very late, for +// unclear reasons. Presumably something to do with faker, tippy-top-sites, +// lorempixel (used by faker for the images), or Spotlight (aka Highlight) +// itself. +storiesOf("Highlight", module) + .add("All valid properties", () => { + const site = createSite({images: 1}); + site.bestImage = site.images[0]; + return (); + }) + .add("Missing an image", () => { + const site = createSite({images: 0}); + return (); + }) + .add("Missing a favicon_url", () => { + const site = createSite({images: 1}); + site.bestImage = site.images[0]; + site.favicon_url = null; + site.favicon = null; + site.favicon_color = null; + site.favicon_colors = null; + return (); + }) + .add("Missing a description", () => { + const site = createSite({images: 1}); + site.bestImage = site.images[0]; + site.description = null; + return (); + }); diff --git a/package.json b/package.json index 8bd8097fe6..fb2048c1be 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "url-parse": "1.1.1" }, "devDependencies": { + "@kadira/storybook": "2.2.1", "babel": "6.5.2", "babel-core": "6.13.2", "babel-loader": "6.2.4", @@ -170,7 +171,7 @@ "firefox": "npm run bundle:webpackAddon && jpm run -b nightly --prefs ./dev-prefs.json", "test": "npm-run-all test:*", "pretest": "npm run bundle && npm run copyTestImages && npm run copyTopSitesJson", - "test:lint": "eslint --ext=.js,.jsx,.json . && sass-lint -v -q", + "test:lint": "eslint --ext=.js,.jsx,.json . .storybook && sass-lint -v -q", "test:checkbinary": "echo \"JPM_FIREFOX_BINARY: ${JPM_FIREFOX_BINARY}\"", "test:jpm": "jpm test -b ${JPM_FIREFOX_BINARY:-\"nightly\"} --prefs ./test-prefs.json -v", "test:karma": "NODE_ENV=test karma start", @@ -179,6 +180,7 @@ "package": "npm run bundle && jpm xpi && mv activity-streams.xpi dist/activity-streams-$npm_package_version.xpi", "travis": "npm run test", "prepush": "npm run test:lint && npm run yamscripts", + "storybook": "start-storybook -p 9001 -s ./data/content", "help": "yamscripts help", "yamscripts": "yamscripts compile", "__": "# NOTE: THESE SCRIPTS ARE COMPILED!!! EDIT yamscripts.yml instead!!!" diff --git a/webpack.common.js b/webpack.common.js new file mode 100644 index 0000000000..fd34cc4c47 --- /dev/null +++ b/webpack.common.js @@ -0,0 +1,46 @@ +"use strict"; +const WebpackNotifierPlugin = require("webpack-notifier"); +const webpack = require("webpack"); +const path = require("path"); +const absolute = relPath => path.join(__dirname, relPath); +const EnvLoaderPlugin = require("webpack-env-loader-plugin"); + +let env = process.env.NODE_ENV || "development"; + +let plugins = [ + new WebpackNotifierPlugin(), + new EnvLoaderPlugin({ + env, + filePattern: "config.{env}.yml", + loadLocalOverride: env === "development" ? "config.yml" : null, + reactEnv: true + }) +]; + +if (env === "production") { + plugins = plugins.concat([ + new webpack.optimize.OccurenceOrderPlugin(), + new webpack.optimize.UglifyJsPlugin({ + test: /vendor/, + compress: {warnings: false} + }), + new webpack.optimize.DedupePlugin() + ]); +} + +module.exports = { + resolve: { + extensions: ["", ".js", ".jsx"], + alias: { + "common": absolute("./common"), + "components": absolute("./content-src/components"), + "reducers": absolute("./content-src/reducers"), + "actions": absolute("./content-src/actions"), + "selectors": absolute("./content-src/selectors"), + "lib": absolute("./content-src/lib"), + "strings": absolute("./strings"), + "test": absolute("./content-test") + } + }, + plugins +}; diff --git a/webpack.config.js b/webpack.config.js index b2ab893dac..0335ebed67 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,9 +1,8 @@ "use strict"; -const WebpackNotifierPlugin = require("webpack-notifier"); const webpack = require("webpack"); +const webpack_common = require("./webpack.common"); const path = require("path"); const absolute = relPath => path.join(__dirname, relPath); -const EnvLoaderPlugin = require("webpack-env-loader-plugin"); const srcPath = absolute("./content-src/main.js"); const outputDir = absolute("./data/content"); @@ -11,29 +10,9 @@ const outputFilename = "bundle.js"; let env = process.env.NODE_ENV || "development"; -let plugins = [ - new WebpackNotifierPlugin(), - new EnvLoaderPlugin({ - env, - filePattern: "config.{env}.yml", - loadLocalOverride: env === "development" ? "config.yml" : null, - reactEnv: true - }) -]; - if (env !== "test") { - plugins.push(new webpack.optimize.CommonsChunkPlugin("vendor", "vendor.bundle.js")); -} - -if (env === "production") { - plugins = plugins.concat([ - new webpack.optimize.OccurenceOrderPlugin(), - new webpack.optimize.UglifyJsPlugin({ - test: /vendor/, - compress: {warnings: false} - }), - new webpack.optimize.DedupePlugin() - ]); + webpack_common.plugins.push( + new webpack.optimize.CommonsChunkPlugin("vendor", "vendor.bundle.js")); } module.exports = { @@ -50,19 +29,6 @@ module.exports = { filename: outputFilename }, target: "web", - resolve: { - extensions: ["", ".js", ".jsx"], - alias: { - "common": absolute("./common"), - "components": absolute("./content-src/components"), - "reducers": absolute("./content-src/reducers"), - "actions": absolute("./content-src/actions"), - "selectors": absolute("./content-src/selectors"), - "lib": absolute("./content-src/lib"), - "strings": absolute("./strings"), - "test": absolute("./content-test") - } - }, module: { loaders: [ {test: /\.json$/, loader: "json"}, @@ -74,5 +40,6 @@ module.exports = { ] }, devtool: env === "production" ? null : "eval", // This is for Firefox - plugins + plugins: webpack_common.plugins, + resolve: webpack_common.resolve }; diff --git a/yamscripts.yml b/yamscripts.yml index 4c4b3748f2..3933214b6a 100644 --- a/yamscripts.yml +++ b/yamscripts.yml @@ -44,7 +44,7 @@ scripts: test: pre: =>bundle && =>copyTestImages && =>copyTopSitesJson # test:lint: Run eslint - lint: eslint --ext=.js,.jsx,.json . && sass-lint -v -q + lint: eslint --ext=.js,.jsx,.json . .storybook && sass-lint -v -q checkbinary: echo "JPM_FIREFOX_BINARY: ${JPM_FIREFOX_BINARY}" # test:jpm: Run jpm tests jpm: jpm test -b ${JPM_FIREFOX_BINARY:-"nightly"} --prefs ./test-prefs.json -v @@ -64,3 +64,6 @@ scripts: # This is just to make sure we don't make commits with failing tests # or uncompiled yamscripts.yml. Run automatically with husky. prepush: =>test:lint && =>yamscripts + +# storybook: build a local react-storybook containing local stories for dev + storybook: start-storybook -p 9001 -s ./data/content