├── .gitignore ├── .rspec ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── README.md ├── app.rb ├── assets ├── fonts │ ├── icomoon.eot │ ├── icomoon.svg │ ├── icomoon.ttf │ └── icomoon.woff ├── less │ ├── _icomoon.less │ └── app.less ├── script │ ├── app.js │ ├── config.js │ ├── feed-list-view.js │ └── feed-view.js ├── style │ ├── _icomoon.css │ └── app.css └── templates │ ├── config.js │ ├── feed-list-view.hbs │ └── feed-view.hbs ├── docker-compose.yml ├── gulpfile.js ├── integration-test.sh ├── karma.conf.js ├── mocks ├── fav-feeds.json └── feeds.json ├── package.json ├── public ├── fonts │ ├── icomoon.eot │ ├── icomoon.svg │ ├── icomoon.ttf │ └── icomoon.woff ├── index.html ├── script │ ├── app.js │ └── app.min.js └── style │ ├── app.css │ └── app.min.css ├── spec ├── features │ └── list_spec.rb ├── pages │ └── feed_list_page.rb └── spec_helper.rb ├── start.sh ├── test ├── icodeit.har ├── local.har └── netsniff.js └── upload.sh /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7 2 | 3 | RUN mkdir /webapp 4 | COPY ./start.sh /webapp/start.sh 5 | COPY ./public /webapp 6 | 7 | ENV PORT 8080 8 | 9 | WORKDIR /webapp 10 | CMD /webapp/start.sh -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://ruby.taobao.org' 2 | 3 | gem 'sinatra' 4 | 5 | group :test do 6 | gem 'rspec' 7 | gem 'capybara' 8 | gem 'poltergeist' 9 | gem 'site_prism' 10 | end -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://ruby.taobao.org/ 3 | specs: 4 | addressable (2.3.6) 5 | capybara (2.4.4) 6 | mime-types (>= 1.16) 7 | nokogiri (>= 1.3.3) 8 | rack (>= 1.0.0) 9 | rack-test (>= 0.5.4) 10 | xpath (~> 2.0) 11 | cliver (0.3.2) 12 | diff-lcs (1.2.5) 13 | mime-types (2.4.3) 14 | mini_portile (0.6.2) 15 | multi_json (1.11.0) 16 | nokogiri (1.6.6.2) 17 | mini_portile (~> 0.6.0) 18 | poltergeist (1.5.1) 19 | capybara (~> 2.1) 20 | cliver (~> 0.3.1) 21 | multi_json (~> 1.0) 22 | websocket-driver (>= 0.2.0) 23 | rack (1.6.4) 24 | rack-protection (1.5.3) 25 | rack 26 | rack-test (0.6.3) 27 | rack (>= 1.0) 28 | rspec (3.1.0) 29 | rspec-core (~> 3.1.0) 30 | rspec-expectations (~> 3.1.0) 31 | rspec-mocks (~> 3.1.0) 32 | rspec-core (3.1.7) 33 | rspec-support (~> 3.1.0) 34 | rspec-expectations (3.1.2) 35 | diff-lcs (>= 1.2.0, < 2.0) 36 | rspec-support (~> 3.1.0) 37 | rspec-mocks (3.1.3) 38 | rspec-support (~> 3.1.0) 39 | rspec-support (3.1.2) 40 | sinatra (1.4.6) 41 | rack (~> 1.4) 42 | rack-protection (~> 1.4) 43 | tilt (>= 1.3, < 3) 44 | site_prism (2.6) 45 | addressable (~> 2.3.3) 46 | capybara (>= 2.1, < 3.0) 47 | tilt (2.0.1) 48 | websocket-driver (0.3.5) 49 | xpath (2.0.0) 50 | nokogiri (~> 1.3) 51 | 52 | PLATFORMS 53 | ruby 54 | 55 | DEPENDENCIES 56 | capybara 57 | poltergeist 58 | rspec 59 | sinatra 60 | site_prism 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bookmarks-frontend 2 | 3 | There are some useful setup here for this project: 4 | 5 | Tech stack here is: 6 | 7 | 1. gulp 8 | 2. browserify 9 | 3. backbone.js 10 | 4. handlebars 11 | 5. less 12 | 6. karma/jasmine 13 | 14 | It's a typical front end project you can start working on without feel shame. 15 | 16 | ```sh 17 | $ npm install 18 | $ gulp dev 19 | ``` 20 | 21 | and since we're doing frontend and backend separation, I'm introducing the `sinatra` as a fake-server to do the tricks. So 22 | 23 | ```sh 24 | $ bundle install 25 | $ ruby app.rb 26 | ``` 27 | 28 | It's recommended that start a gulp `watch` in a terminal, and launch the sinatra in another one. 29 | -------------------------------------------------------------------------------- /app.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'sinatra' 4 | 5 | get '/' do 6 | File.open('public/index.html').read 7 | end 8 | 9 | get '/api/feeds' do 10 | content_type 'application/json' 11 | File.open('mocks/feeds.json').read 12 | end 13 | 14 | get '/api/fav-feeds/:id' do 15 | content_type 'application/json' 16 | File.open('mocks/fav-feeds.json').read 17 | end 18 | 19 | post '/api/feeds/:id' do 20 | 21 | end 22 | -------------------------------------------------------------------------------- /assets/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abruzzi/bookmarks-frontend/7a70ae8d665b33a52b8ac205e9bf66b0f8b5c7c9/assets/fonts/icomoon.eot -------------------------------------------------------------------------------- /assets/fonts/icomoon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /assets/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abruzzi/bookmarks-frontend/7a70ae8d665b33a52b8ac205e9bf66b0f8b5c7c9/assets/fonts/icomoon.ttf -------------------------------------------------------------------------------- /assets/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abruzzi/bookmarks-frontend/7a70ae8d665b33a52b8ac205e9bf66b0f8b5c7c9/assets/fonts/icomoon.woff -------------------------------------------------------------------------------- /assets/less/_icomoon.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'icomoon'; 3 | src:url('../fonts/icomoon.eot?ordz2w'); 4 | src:url('../fonts/icomoon.eot?#iefixordz2w') format('embedded-opentype'), 5 | url('../fonts/icomoon.ttf?ordz2w') format('truetype'), 6 | url('../fonts/icomoon.woff?ordz2w') format('woff'), 7 | url('fonts/icomoon.svg?ordz2w#icomoon') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | [class^="icon-"], [class*=" icon-"] { 13 | font-family: 'icomoon'; 14 | speak: none; 15 | font-style: normal; 16 | font-weight: normal; 17 | font-variant: normal; 18 | text-transform: none; 19 | line-height: 1; 20 | 21 | /* Better Font Rendering =========== */ 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | } 25 | 26 | .icon-heart-o:before { 27 | content: "\e600"; 28 | } 29 | .icon-heart:before { 30 | content: "\e601"; 31 | } 32 | -------------------------------------------------------------------------------- /assets/less/app.less: -------------------------------------------------------------------------------- 1 | @import "_icomoon"; 2 | 3 | @background-color: #ffffff; 4 | @link-color: #05C3DE; 5 | @text-color: #666666; 6 | @text-gray-color: #999999; 7 | 8 | body { 9 | margin: 0; 10 | background-color: @background-color; 11 | color: @text-color; 12 | font-family: 'Open Sans', serif-sans; 13 | font-size: 62.5%; 14 | } 15 | 16 | .container { 17 | width: 90%; 18 | max-width: 800px; 19 | margin: 0 auto; 20 | overflow: hidden; 21 | } 22 | 23 | .banner { 24 | box-shadow: 0 1px 10px @text-gray-color; 25 | background-color: @link-color; 26 | color: #ffffff; 27 | 28 | h2 { 29 | margin: 0; 30 | padding: 1em 0; 31 | text-align: center; 32 | font-size: 2em; 33 | text-transform: uppercase; 34 | } 35 | } 36 | 37 | @heart-color: #F04E98; 38 | 39 | .status { 40 | ul { 41 | list-style: none; 42 | padding: 0; 43 | margin: 0; 44 | overflow: hidden; 45 | 46 | li { 47 | padding: 1em 0; 48 | font-size: 1.5em; 49 | float: left; 50 | } 51 | 52 | .count { 53 | padding: 0 1em; 54 | } 55 | 56 | .number { 57 | background-color: #ffffff; 58 | color: #22C3DC; 59 | display: inline-block; 60 | width: 1em; 61 | height: 1em; 62 | line-height: 1em; 63 | text-align: center; 64 | border-radius: 100%; 65 | padding: .2em; 66 | } 67 | } 68 | } 69 | 70 | .feeds { 71 | margin: 0; 72 | padding: 0; 73 | 74 | li { 75 | list-style: none; 76 | } 77 | 78 | a { 79 | color: @link-color; 80 | text-decoration: none; 81 | } 82 | 83 | .feed-item { 84 | padding: 2em 1em; 85 | position: relative; 86 | border-bottom: 1px dashed @text-gray-color; 87 | 88 | h3 { 89 | font-size: 1.5em; 90 | font-weight: normal; 91 | margin: 0 2em; 92 | } 93 | 94 | .favicon { 95 | color: @heart-color; 96 | position: absolute; 97 | font-size: 1.5em; 98 | margin: .3em 0; 99 | cursor: pointer; 100 | } 101 | 102 | .date { 103 | color: @text-gray-color; 104 | float: right; 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /assets/script/app.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var FeedListView = require('./feed-list-view.js'); 3 | var Backbone = require('backbone'); 4 | // var FeedListModel = require('./feed-list-model.js'); 5 | var _ = require('lodash'); 6 | 7 | var config = require('./config'); 8 | 9 | Backbone.$ = $; 10 | 11 | $(function() { 12 | var feeds = $.get(config.backend+'/api/feeds'); 13 | var favorite = $.get(config.backend+'/api/fav-feeds/1'); 14 | 15 | $.when(feeds, favorite).then(function(feeds, favorite) { 16 | var ids = _.pluck(favorite[0], 'id'); 17 | var extended = _.map(feeds[0], function(feed) { 18 | return _.extend(feed, {status: _.includes(ids, feed.id)}); 19 | }); 20 | 21 | var feedList = new Backbone.Collection(extended); 22 | var feedListView = new FeedListView(feedList); 23 | 24 | $('.container').append(feedListView.render()); 25 | }); 26 | }); -------------------------------------------------------------------------------- /assets/script/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | backend: 'http://192.168.99.100:8000' 3 | } -------------------------------------------------------------------------------- /assets/script/feed-list-view.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var _ = require('lodash'); 3 | var Backbone = require('backbone'); 4 | 5 | Backbone.$ = $; 6 | 7 | var FeedView = require('./feed-view.js'); 8 | 9 | module.exports = Backbone.View.extend({ 10 | initialize: function(model) { 11 | this.model = model; 12 | this.model.bind('change', _.bind(this.render, this)); 13 | }, 14 | 15 | tagName: 'ul', 16 | 17 | className: 'feeds', 18 | 19 | render: function() { 20 | var that = this; 21 | 22 | _.each(this.model.toJSON(), function(feed) { 23 | return that.$el.append(new FeedView(feed).render()); 24 | }); 25 | 26 | return this.$el; 27 | }, 28 | }); -------------------------------------------------------------------------------- /assets/script/feed-view.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var _ = require('lodash'); 3 | var Backbone = require('backbone'); 4 | 5 | Backbone.$ = $; 6 | 7 | var template = require('../templates/feed-view.hbs'); 8 | 9 | module.exports = Backbone.View.extend({ 10 | initialize: function(model) { 11 | this.model = new Backbone.Model(model); 12 | this.model.bind('change', _.bind(this.render, this)); 13 | }, 14 | 15 | events: { 16 | 'click .favicon': 'toggleFavorite' 17 | }, 18 | 19 | tagName: 'li', 20 | 21 | className: 'feed', 22 | 23 | render: function() { 24 | var html = template(this.model.toJSON()); 25 | this.$el.html(html); 26 | 27 | return this.$el; 28 | }, 29 | 30 | toggleFavorite: function(event) { 31 | event.preventDefault(); 32 | var that = this; 33 | $.post('/api/feeds/'+this.model.get('id')).done(function(){ 34 | var status = that.model.get('status'); 35 | that.model.set('status', !status); 36 | }); 37 | } 38 | }); -------------------------------------------------------------------------------- /assets/style/_icomoon.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'icomoon'; 3 | src: url('../fonts/icomoon.eot?ordz2w'); 4 | src: url('../fonts/icomoon.eot?#iefixordz2w') format('embedded-opentype'), url('../fonts/icomoon.ttf?ordz2w') format('truetype'), url('../fonts/icomoon.woff?ordz2w') format('woff'), url('fonts/icomoon.svg?ordz2w#icomoon') format('svg'); 5 | font-weight: normal; 6 | font-style: normal; 7 | } 8 | [class^="icon-"], 9 | [class*=" icon-"] { 10 | font-family: 'icomoon'; 11 | speak: none; 12 | font-style: normal; 13 | font-weight: normal; 14 | font-variant: normal; 15 | text-transform: none; 16 | line-height: 1; 17 | /* Better Font Rendering =========== */ 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | } 21 | .icon-heart-o:before { 22 | content: "\e600"; 23 | } 24 | .icon-heart:before { 25 | content: "\e601"; 26 | } 27 | -------------------------------------------------------------------------------- /assets/style/app.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'icomoon'; 3 | src: url('../fonts/icomoon.eot?ordz2w'); 4 | src: url('../fonts/icomoon.eot?#iefixordz2w') format('embedded-opentype'), url('../fonts/icomoon.ttf?ordz2w') format('truetype'), url('../fonts/icomoon.woff?ordz2w') format('woff'), url('fonts/icomoon.svg?ordz2w#icomoon') format('svg'); 5 | font-weight: normal; 6 | font-style: normal; 7 | } 8 | [class^="icon-"], 9 | [class*=" icon-"] { 10 | font-family: 'icomoon'; 11 | speak: none; 12 | font-style: normal; 13 | font-weight: normal; 14 | font-variant: normal; 15 | text-transform: none; 16 | line-height: 1; 17 | /* Better Font Rendering =========== */ 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | } 21 | .icon-heart-o:before { 22 | content: "\e600"; 23 | } 24 | .icon-heart:before { 25 | content: "\e601"; 26 | } 27 | body { 28 | margin: 0; 29 | background-color: #ffffff; 30 | color: #666666; 31 | font-family: 'Open Sans', serif-sans; 32 | font-size: 62.5%; 33 | } 34 | .container { 35 | width: 90%; 36 | max-width: 800px; 37 | margin: 0 auto; 38 | overflow: hidden; 39 | } 40 | .banner { 41 | box-shadow: 0 1px 10px #999999; 42 | background-color: #05C3DE; 43 | color: #ffffff; 44 | } 45 | .banner h2 { 46 | margin: 0; 47 | padding: 1em 0; 48 | text-align: center; 49 | font-size: 2em; 50 | text-transform: uppercase; 51 | } 52 | .status ul { 53 | list-style: none; 54 | padding: 0; 55 | margin: 0; 56 | overflow: hidden; 57 | } 58 | .status ul li { 59 | padding: 1em 0; 60 | font-size: 1.5em; 61 | float: left; 62 | } 63 | .status ul .count { 64 | padding: 0 1em; 65 | } 66 | .status ul .number { 67 | background-color: #ffffff; 68 | color: #22C3DC; 69 | display: inline-block; 70 | width: 1em; 71 | height: 1em; 72 | line-height: 1em; 73 | text-align: center; 74 | border-radius: 100%; 75 | padding: .2em; 76 | } 77 | .feeds { 78 | margin: 0; 79 | padding: 0; 80 | } 81 | .feeds li { 82 | list-style: none; 83 | } 84 | .feeds a { 85 | color: #05C3DE; 86 | text-decoration: none; 87 | } 88 | .feeds .feed-item { 89 | padding: 2em 1em; 90 | position: relative; 91 | border-bottom: 1px dashed #999999; 92 | } 93 | .feeds .feed-item h3 { 94 | font-size: 1.5em; 95 | font-weight: normal; 96 | margin: 0 2em; 97 | } 98 | .feeds .feed-item .favicon { 99 | color: #F04E98; 100 | position: absolute; 101 | font-size: 1.5em; 102 | margin: .3em 0; 103 | cursor: pointer; 104 | } 105 | .feeds .feed-item .date { 106 | color: #999999; 107 | float: right; 108 | } 109 | -------------------------------------------------------------------------------- /assets/templates/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | backend: '#backend#' 3 | } -------------------------------------------------------------------------------- /assets/templates/feed-list-view.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/templates/feed-view.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#if status}} 3 | 4 | {{else}} 5 | 6 | {{/if}} 7 | 8 | 9 |

{{this.title}}

10 |
11 | {{this.publicDate}} 12 |
-------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | app: 2 | build: . 3 | ports: 4 | - 8080:8080 5 | volumes: 6 | - .:/web 7 | working_dir: /web 8 | entrypoint: /web/start.sh 9 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'), 4 | less = require('gulp-less'), 5 | livereload = require('gulp-livereload'), 6 | browserify = require('browserify'), 7 | source = require('vinyl-source-stream'), 8 | uglify = require('gulp-uglify'), 9 | concat = require('gulp-concat'), 10 | minifyCss = require('gulp-minify-css'), 11 | replace = require('gulp-replace'); 12 | 13 | gulp.task('less', function() { 14 | gulp.src('assets/less/*.less') 15 | .pipe(less()) 16 | .pipe(gulp.dest('assets/style')); 17 | }); 18 | 19 | gulp.task('watch', function() { 20 | livereload.listen(); 21 | gulp.watch('assets/less/*.less', ['concatcss']); 22 | gulp.watch('assets/script/*.js', ['browserify']); 23 | }); 24 | 25 | gulp.task('browserify', function() { 26 | return browserify('assets/script/app.js') 27 | .bundle() 28 | .pipe(source('app.js')) 29 | .pipe(gulp.dest('public/script')) 30 | .pipe(livereload()); 31 | }); 32 | 33 | var backend = 'http://quiet-atoll-8237.herokuapp.com'; 34 | 35 | gulp.task('prepareConfig', function() { 36 | gulp.src(['assets/templates/config.js']) 37 | .pipe(replace(/#backend#/g, 'http://localhost:8100')) 38 | .pipe(gulp.dest('assets/script/')); 39 | }); 40 | 41 | gulp.task('prepareStaging', function() { 42 | gulp.src(['assets/templates/config.js']) 43 | .pipe(replace(/#backend#/g, 'http://192.168.99.100:8000')) 44 | .pipe(gulp.dest('assets/script/')); 45 | }); 46 | 47 | gulp.task('prepareRelease', function() { 48 | gulp.src(['assets/templates/config.js']) 49 | .pipe(replace(/#backend#/g, backend)) 50 | .pipe(gulp.dest('assets/script/')); 51 | }); 52 | 53 | gulp.task('script', ['browserify'], function() { 54 | return gulp.src('public/script/app.js') 55 | .pipe(uglify()) 56 | .pipe(concat('app.min.js')) 57 | .pipe(gulp.dest('public/script')); 58 | }); 59 | 60 | gulp.task('concatcss', ['less'], function() { 61 | return gulp.src(['assets/style/*.css']) 62 | .pipe(concat('app.css')) 63 | .pipe(gulp.dest('public/style/')) 64 | .pipe(livereload()); 65 | }); 66 | 67 | gulp.task('css', ['less'], function() { 68 | return gulp.src(['assets/style/*.css']) 69 | .pipe(concat('app.min.css')) 70 | .pipe(minifyCss()) 71 | .pipe(gulp.dest('public/style/')); 72 | }); 73 | 74 | gulp.task('dev', ['prepareConfig', 'browserify', 'concatcss']); 75 | gulp.task('build', ['prepareConfig', 'script', 'css']); 76 | gulp.task('staging', ['prepareStaging', 'script', 'css']); 77 | gulp.task('release', ['prepareRelease', 'script', 'css']); 78 | 79 | gulp.task('default', ['dev']); 80 | -------------------------------------------------------------------------------- /integration-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PORT=8100 4 | bundle install 5 | 6 | # launch the application 7 | echo "launch the application" 8 | ruby app.rb 2>&1 & 9 | PID=$! 10 | 11 | # wait for it to start up 12 | sleep 3 13 | 14 | # run the cucumber features and record the status 15 | rspec 16 | RES=$? 17 | 18 | # terminate after cucumber 19 | echo "terminate the application" 20 | kill -9 $PID 21 | 22 | # now we know whether the cucumber success or not 23 | exit $RES 24 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Fri Jan 08 2016 19:52:15 GMT+1100 (AEDT) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'test/**/*-spec.js' 19 | ], 20 | 21 | 22 | // list of files to exclude 23 | exclude: [ 24 | ], 25 | 26 | 27 | // preprocess matching files before serving them to the browser 28 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 29 | preprocessors: { 30 | }, 31 | 32 | 33 | // test results reporter to use 34 | // possible values: 'dots', 'progress' 35 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 36 | reporters: ['progress'], 37 | 38 | 39 | // web server port 40 | port: 9876, 41 | 42 | 43 | // enable / disable colors in the output (reporters and logs) 44 | colors: true, 45 | 46 | 47 | // level of logging 48 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 49 | logLevel: config.LOG_INFO, 50 | 51 | 52 | // enable / disable watching file and executing tests whenever any file changes 53 | autoWatch: true, 54 | 55 | 56 | // start these browsers 57 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 58 | browsers: ['PhantomJS'], 59 | 60 | 61 | // Continuous Integration mode 62 | // if true, Karma captures browsers, runs the tests and exits 63 | singleRun: false 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /mocks/fav-feeds.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 3, 4 | "url": "http://abruzzi.github.com/2015/02/build-sample-application-by-using-underscore-and-jquery/", 5 | "title": "使用underscore.js构建前端应用", 6 | "publicDate": "2015年1月20日" 7 | } 8 | ] -------------------------------------------------------------------------------- /mocks/feeds.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "url": "http://abruzzi.github.com/2015/03/list-comprehension-in-python/", 5 | "title": "Python中的 list comprehension 以及 generator", 6 | "publicDate": "2015年3月20日" 7 | }, 8 | { 9 | "id": 2, 10 | "url": "http://abruzzi.github.com/2015/03/build-monitor-script-based-on-inotify/", 11 | "title": "使用inotify/fswatch构建自动监控脚本", 12 | "publicDate": "2015年2月1日" 13 | }, 14 | { 15 | "id": 3, 16 | "url": "http://abruzzi.github.com/2015/02/build-sample-application-by-using-underscore-and-jquery/", 17 | "title": "使用underscore.js构建前端应用", 18 | "publicDate": "2015年1月20日" 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bookmarks-frontend", 3 | "version": "1.0.0", 4 | "description": "A bookmark application, frontend part", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm test" 8 | }, 9 | "browserify": { 10 | "transform": [ 11 | "hbsfy" 12 | ] 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/abruzzi/bookmarks-frontend.git" 17 | }, 18 | "keywords": [ 19 | "bookmarks", 20 | "frontend", 21 | "soc" 22 | ], 23 | "author": "Qiu Juntao", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/abruzzi/bookmarks-frontend/issues" 27 | }, 28 | "devDependencies": { 29 | "backbone": "^1.2.1", 30 | "browserify": "^10.2.4", 31 | "gulp": "^3.9.0", 32 | "gulp-concat": "^2.5.2", 33 | "gulp-connect": "^2.2.0", 34 | "gulp-less": "^3.0.5", 35 | "gulp-livereload": "^3.8.1", 36 | "gulp-minify-css": "^1.1.6", 37 | "gulp-replace": "^0.5.4", 38 | "gulp-uglify": "^1.2.0", 39 | "handlebars": "^3.0.3", 40 | "hbsfy": "^2.2.1", 41 | "jquery": "^2.1.4", 42 | "karma": "^0.12.37", 43 | "karma-browserify": "^4.4.2", 44 | "karma-jasmine": "^0.3.6", 45 | "karma-phantomjs-launcher": "^0.2.3", 46 | "lodash": "^3.9.3", 47 | "underscore": "^1.8.3", 48 | "vinyl-source-stream": "^1.1.0", 49 | "yslow": "^3.1.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /public/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abruzzi/bookmarks-frontend/7a70ae8d665b33a52b8ac205e9bf66b0f8b5c7c9/public/fonts/icomoon.eot -------------------------------------------------------------------------------- /public/fonts/icomoon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abruzzi/bookmarks-frontend/7a70ae8d665b33a52b8ac205e9bf66b0f8b5c7c9/public/fonts/icomoon.ttf -------------------------------------------------------------------------------- /public/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abruzzi/bookmarks-frontend/7a70ae8d665b33a52b8ac205e9bf66b0f8b5c7c9/public/fonts/icomoon.woff -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bookmarks 5 | 6 | 9 | 10 | 11 | 12 | 13 |
14 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /public/style/app.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'icomoon'; 3 | src: url('../fonts/icomoon.eot?ordz2w'); 4 | src: url('../fonts/icomoon.eot?#iefixordz2w') format('embedded-opentype'), url('../fonts/icomoon.ttf?ordz2w') format('truetype'), url('../fonts/icomoon.woff?ordz2w') format('woff'), url('fonts/icomoon.svg?ordz2w#icomoon') format('svg'); 5 | font-weight: normal; 6 | font-style: normal; 7 | } 8 | [class^="icon-"], 9 | [class*=" icon-"] { 10 | font-family: 'icomoon'; 11 | speak: none; 12 | font-style: normal; 13 | font-weight: normal; 14 | font-variant: normal; 15 | text-transform: none; 16 | line-height: 1; 17 | /* Better Font Rendering =========== */ 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | } 21 | .icon-heart-o:before { 22 | content: "\e600"; 23 | } 24 | .icon-heart:before { 25 | content: "\e601"; 26 | } 27 | 28 | @font-face { 29 | font-family: 'icomoon'; 30 | src: url('../fonts/icomoon.eot?ordz2w'); 31 | src: url('../fonts/icomoon.eot?#iefixordz2w') format('embedded-opentype'), url('../fonts/icomoon.ttf?ordz2w') format('truetype'), url('../fonts/icomoon.woff?ordz2w') format('woff'), url('fonts/icomoon.svg?ordz2w#icomoon') format('svg'); 32 | font-weight: normal; 33 | font-style: normal; 34 | } 35 | [class^="icon-"], 36 | [class*=" icon-"] { 37 | font-family: 'icomoon'; 38 | speak: none; 39 | font-style: normal; 40 | font-weight: normal; 41 | font-variant: normal; 42 | text-transform: none; 43 | line-height: 1; 44 | /* Better Font Rendering =========== */ 45 | -webkit-font-smoothing: antialiased; 46 | -moz-osx-font-smoothing: grayscale; 47 | } 48 | .icon-heart-o:before { 49 | content: "\e600"; 50 | } 51 | .icon-heart:before { 52 | content: "\e601"; 53 | } 54 | body { 55 | margin: 0; 56 | background-color: #ffffff; 57 | color: #666666; 58 | font-family: 'Open Sans', serif-sans; 59 | font-size: 62.5%; 60 | } 61 | .container { 62 | width: 90%; 63 | max-width: 800px; 64 | margin: 0 auto; 65 | overflow: hidden; 66 | } 67 | .banner { 68 | box-shadow: 0 1px 10px #999999; 69 | background-color: #05c3de; 70 | color: #ffffff; 71 | } 72 | .banner h2 { 73 | margin: 0; 74 | padding: 1em 0; 75 | text-align: center; 76 | font-size: 2em; 77 | text-transform: uppercase; 78 | } 79 | .status ul { 80 | list-style: none; 81 | padding: 0; 82 | margin: 0; 83 | overflow: hidden; 84 | } 85 | .status ul li { 86 | padding: 1em 0; 87 | font-size: 1.5em; 88 | float: left; 89 | } 90 | .status ul .count { 91 | padding: 0 1em; 92 | } 93 | .status ul .number { 94 | background-color: #ffffff; 95 | color: #22C3DC; 96 | display: inline-block; 97 | width: 1em; 98 | height: 1em; 99 | line-height: 1em; 100 | text-align: center; 101 | border-radius: 100%; 102 | padding: .2em; 103 | } 104 | .feeds { 105 | margin: 0; 106 | padding: 0; 107 | } 108 | .feeds li { 109 | list-style: none; 110 | } 111 | .feeds a { 112 | color: #05c3de; 113 | text-decoration: none; 114 | } 115 | .feeds .feed-item { 116 | padding: 2em 1em; 117 | position: relative; 118 | border-bottom: 1px dashed #999999; 119 | } 120 | .feeds .feed-item h3 { 121 | font-size: 1.5em; 122 | font-weight: normal; 123 | margin: 0 2em; 124 | } 125 | .feeds .feed-item .favicon { 126 | color: #f04e98; 127 | position: absolute; 128 | font-size: 1.5em; 129 | margin: .3em 0; 130 | cursor: pointer; 131 | } 132 | .feeds .feed-item .date { 133 | color: #999999; 134 | float: right; 135 | } 136 | -------------------------------------------------------------------------------- /public/style/app.min.css: -------------------------------------------------------------------------------- 1 | .feeds li,.status ul{list-style:none}@font-face{font-family:icomoon;src:url(../fonts/icomoon.eot?ordz2w);src:url(../fonts/icomoon.eot?#iefixordz2w) format('embedded-opentype'),url(../fonts/icomoon.ttf?ordz2w) format('truetype'),url(../fonts/icomoon.woff?ordz2w) format('woff'),url(fonts/icomoon.svg?ordz2w#icomoon) format('svg');font-weight:400;font-style:normal}@font-face{font-family:icomoon;src:url(../fonts/icomoon.eot?ordz2w);src:url(../fonts/icomoon.eot?#iefixordz2w) format('embedded-opentype'),url(../fonts/icomoon.ttf?ordz2w) format('truetype'),url(../fonts/icomoon.woff?ordz2w) format('woff'),url(fonts/icomoon.svg?ordz2w#icomoon) format('svg');font-weight:400;font-style:normal}[class*=" icon-"],[class^=icon-]{font-family:icomoon;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-heart-o:before{content:"\e600"}.icon-heart:before{content:"\e601"}body{margin:0;background-color:#fff;color:#666;font-family:'Open Sans',serif-sans;font-size:62.5%}.container{width:90%;max-width:800px;margin:0 auto;overflow:hidden}.banner{box-shadow:0 1px 10px #999;background-color:#05C3DE;color:#fff}.banner h2{margin:0;padding:1em 0;text-align:center;font-size:2em;text-transform:uppercase}.status ul{padding:0;margin:0;overflow:hidden}.status ul li{padding:1em 0;font-size:1.5em;float:left}.status ul .count{padding:0 1em}.status ul .number{background-color:#fff;color:#22C3DC;display:inline-block;width:1em;height:1em;line-height:1em;text-align:center;border-radius:100%;padding:.2em}.feeds{margin:0;padding:0}.feeds a{color:#05C3DE;text-decoration:none}.feeds .feed-item{padding:2em 1em;position:relative;border-bottom:1px dashed #999}.feeds .feed-item h3{font-size:1.5em;font-weight:400;margin:0 2em}.feeds .feed-item .favicon{color:#F04E98;position:absolute;font-size:1.5em;margin:.3em 0;cursor:pointer}.feeds .feed-item .date{color:#999;float:right} -------------------------------------------------------------------------------- /spec/features/list_spec.rb: -------------------------------------------------------------------------------- 1 | #encoding: utf-8 2 | require 'spec_helper' 3 | 4 | describe 'Feeds List Page' do 5 | let(:list_page) {FeedListPage.new} 6 | 7 | before do 8 | list_page.load 9 | end 10 | 11 | it 'user can see a banner and some feeds' do 12 | expect(list_page).to have_banner 13 | expect(list_page).to have_feeds 14 | end 15 | 16 | it 'user can see 3 feeds in the list' do 17 | expect(list_page.all_feeds).to have_feed_items count: 3 18 | end 19 | 20 | it 'feed has some detail information' do 21 | first = list_page.all_feeds.feed_items.first 22 | expect(first.title).to eql("Python中的 list comprehension 以及 generator") 23 | end 24 | end -------------------------------------------------------------------------------- /spec/pages/feed_list_page.rb: -------------------------------------------------------------------------------- 1 | class FeedSectoin < SitePrism::Section 2 | element :item, '.feed-item' 3 | element :heart, '.favicon' 4 | element :heading, 'h3' 5 | 6 | def title 7 | heading.text 8 | end 9 | end 10 | 11 | class FeedListSection < SitePrism::Section 12 | sections :feed_items, FeedSectoin, "li" 13 | end 14 | 15 | class FeedListPage < SitePrism::Page 16 | set_url "http://localhost:8100/" 17 | 18 | element :banner, '.banner' 19 | element :feeds, '.feeds' 20 | 21 | section :all_feeds, FeedListSection, ".feeds" 22 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'capybara' 2 | require 'capybara/rspec' 3 | require 'capybara/poltergeist' 4 | require 'site_prism' 5 | 6 | Capybara.configure do |config| 7 | config.run_server = false 8 | config.current_driver = :poltergeist 9 | config.default_wait_time = 10 10 | config.app_host = 'http://localhost:8100/' 11 | end 12 | 13 | SitePrism.configure do |config| 14 | config.use_implicit_waits = true 15 | end 16 | 17 | require 'pages/feed_list_page' 18 | 19 | RSpec.configure do |config| 20 | config.expect_with :rspec do |expectations| 21 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 22 | end 23 | 24 | config.mock_with :rspec do |mocks| 25 | mocks.verify_partial_doubles = true 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd public 4 | python -m SimpleHTTPServer $PORT -------------------------------------------------------------------------------- /test/local.har: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "version": "1.2", 4 | "creator": { 5 | "name": "PhantomJS", 6 | "version": "1.8.1" 7 | }, 8 | "pages": [ 9 | { 10 | "startedDateTime": "2016-01-10T09:45:10.312Z", 11 | "id": "http://localhost:8100", 12 | "title": "Bookmarks", 13 | "pageTimings": { 14 | "onLoad": 48 15 | } 16 | } 17 | ], 18 | "entries": [ 19 | { 20 | "startedDateTime": "2016-01-10T09:45:10.301Z", 21 | "time": 13, 22 | "request": { 23 | "method": "GET", 24 | "url": "http://localhost:8100/", 25 | "httpVersion": "HTTP/1.1", 26 | "cookies": [], 27 | "headers": [ 28 | { 29 | "name": "User-Agent", 30 | "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X) AppleWebKit/534.34 (KHTML, like Gecko) PhantomJS/1.8.1 Safari/534.34" 31 | }, 32 | { 33 | "name": "Accept", 34 | "value": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" 35 | } 36 | ], 37 | "queryString": [], 38 | "headersSize": -1, 39 | "bodySize": -1 40 | }, 41 | "response": { 42 | "status": 200, 43 | "statusText": "OK ", 44 | "httpVersion": "HTTP/1.1", 45 | "cookies": [], 46 | "headers": [ 47 | { 48 | "name": "Content-Type", 49 | "value": "text/html;charset=utf-8" 50 | }, 51 | { 52 | "name": "Content-Length", 53 | "value": "702" 54 | }, 55 | { 56 | "name": "X-Xss-Protection", 57 | "value": "1; mode=block" 58 | }, 59 | { 60 | "name": "X-Content-Type-Options", 61 | "value": "nosniff" 62 | }, 63 | { 64 | "name": "X-Frame-Options", 65 | "value": "SAMEORIGIN" 66 | }, 67 | { 68 | "name": "Server", 69 | "value": "WEBrick/1.3.1 (Ruby/1.9.3/2014-02-24)" 70 | }, 71 | { 72 | "name": "Date", 73 | "value": "Sun, 10 Jan 2016 09:45:10 GMT" 74 | }, 75 | { 76 | "name": "Connection", 77 | "value": "Keep-Alive" 78 | } 79 | ], 80 | "redirectURL": "", 81 | "headersSize": -1, 82 | "bodySize": 702, 83 | "content": { 84 | "size": 702, 85 | "mimeType": "text/html;charset=utf-8" 86 | } 87 | }, 88 | "cache": {}, 89 | "timings": { 90 | "blocked": 0, 91 | "dns": -1, 92 | "connect": -1, 93 | "send": 0, 94 | "wait": 11, 95 | "receive": 2, 96 | "ssl": -1 97 | }, 98 | "pageref": "http://localhost:8100" 99 | }, 100 | { 101 | "startedDateTime": "2016-01-10T09:45:10.313Z", 102 | "time": 3, 103 | "request": { 104 | "method": "GET", 105 | "url": "http://localhost:8100/style/app.css", 106 | "httpVersion": "HTTP/1.1", 107 | "cookies": [], 108 | "headers": [ 109 | { 110 | "name": "User-Agent", 111 | "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X) AppleWebKit/534.34 (KHTML, like Gecko) PhantomJS/1.8.1 Safari/534.34" 112 | }, 113 | { 114 | "name": "Accept", 115 | "value": "text/css,*/*;q=0.1" 116 | }, 117 | { 118 | "name": "Referer", 119 | "value": "http://localhost:8100/" 120 | } 121 | ], 122 | "queryString": [], 123 | "headersSize": -1, 124 | "bodySize": -1 125 | }, 126 | "response": { 127 | "status": 200, 128 | "statusText": "OK ", 129 | "httpVersion": "HTTP/1.1", 130 | "cookies": [], 131 | "headers": [ 132 | { 133 | "name": "Content-Type", 134 | "value": "text/css;charset=utf-8" 135 | }, 136 | { 137 | "name": "Last-Modified", 138 | "value": "Sat, 09 Jan 2016 09:41:47 GMT" 139 | }, 140 | { 141 | "name": "Content-Length", 142 | "value": "2862" 143 | }, 144 | { 145 | "name": "X-Content-Type-Options", 146 | "value": "nosniff" 147 | }, 148 | { 149 | "name": "Server", 150 | "value": "WEBrick/1.3.1 (Ruby/1.9.3/2014-02-24)" 151 | }, 152 | { 153 | "name": "Date", 154 | "value": "Sun, 10 Jan 2016 09:45:10 GMT" 155 | }, 156 | { 157 | "name": "Connection", 158 | "value": "Keep-Alive" 159 | } 160 | ], 161 | "redirectURL": "", 162 | "headersSize": -1, 163 | "bodySize": 2862, 164 | "content": { 165 | "size": 2862, 166 | "mimeType": "text/css;charset=utf-8" 167 | } 168 | }, 169 | "cache": {}, 170 | "timings": { 171 | "blocked": 0, 172 | "dns": -1, 173 | "connect": -1, 174 | "send": 0, 175 | "wait": 3, 176 | "receive": 0, 177 | "ssl": -1 178 | }, 179 | "pageref": "http://localhost:8100" 180 | }, 181 | { 182 | "startedDateTime": "2016-01-10T09:45:10.314Z", 183 | "time": 4, 184 | "request": { 185 | "method": "GET", 186 | "url": "http://localhost:8100/script/app.js", 187 | "httpVersion": "HTTP/1.1", 188 | "cookies": [], 189 | "headers": [ 190 | { 191 | "name": "User-Agent", 192 | "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X) AppleWebKit/534.34 (KHTML, like Gecko) PhantomJS/1.8.1 Safari/534.34" 193 | }, 194 | { 195 | "name": "Accept", 196 | "value": "*/*" 197 | }, 198 | { 199 | "name": "Referer", 200 | "value": "http://localhost:8100/" 201 | } 202 | ], 203 | "queryString": [], 204 | "headersSize": -1, 205 | "bodySize": -1 206 | }, 207 | "response": { 208 | "status": 200, 209 | "statusText": "OK ", 210 | "httpVersion": "HTTP/1.1", 211 | "cookies": [], 212 | "headers": [ 213 | { 214 | "name": "Content-Type", 215 | "value": "application/javascript;charset=utf-8" 216 | }, 217 | { 218 | "name": "Last-Modified", 219 | "value": "Sat, 09 Jan 2016 09:41:47 GMT" 220 | }, 221 | { 222 | "name": "Content-Length", 223 | "value": "821229" 224 | }, 225 | { 226 | "name": "X-Content-Type-Options", 227 | "value": "nosniff" 228 | }, 229 | { 230 | "name": "Server", 231 | "value": "WEBrick/1.3.1 (Ruby/1.9.3/2014-02-24)" 232 | }, 233 | { 234 | "name": "Date", 235 | "value": "Sun, 10 Jan 2016 09:45:10 GMT" 236 | }, 237 | { 238 | "name": "Connection", 239 | "value": "Keep-Alive" 240 | } 241 | ], 242 | "redirectURL": "", 243 | "headersSize": -1, 244 | "bodySize": 147384, 245 | "content": { 246 | "size": 147384, 247 | "mimeType": "application/javascript;charset=utf-8" 248 | } 249 | }, 250 | "cache": {}, 251 | "timings": { 252 | "blocked": 0, 253 | "dns": -1, 254 | "connect": -1, 255 | "send": 0, 256 | "wait": 3, 257 | "receive": 1, 258 | "ssl": -1 259 | }, 260 | "pageref": "http://localhost:8100" 261 | }, 262 | { 263 | "startedDateTime": "2016-01-10T09:45:10.346Z", 264 | "time": 8, 265 | "request": { 266 | "method": "GET", 267 | "url": "http://localhost:8100/api/feeds", 268 | "httpVersion": "HTTP/1.1", 269 | "cookies": [], 270 | "headers": [ 271 | { 272 | "name": "X-Requested-With", 273 | "value": "XMLHttpRequest" 274 | }, 275 | { 276 | "name": "User-Agent", 277 | "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X) AppleWebKit/534.34 (KHTML, like Gecko) PhantomJS/1.8.1 Safari/534.34" 278 | }, 279 | { 280 | "name": "Accept", 281 | "value": "*/*" 282 | }, 283 | { 284 | "name": "Referer", 285 | "value": "http://localhost:8100/" 286 | } 287 | ], 288 | "queryString": [], 289 | "headersSize": -1, 290 | "bodySize": -1 291 | }, 292 | "response": { 293 | "status": 200, 294 | "statusText": "OK ", 295 | "httpVersion": "HTTP/1.1", 296 | "cookies": [], 297 | "headers": [ 298 | { 299 | "name": "Content-Type", 300 | "value": "application/json" 301 | }, 302 | { 303 | "name": "Content-Length", 304 | "value": "690" 305 | }, 306 | { 307 | "name": "X-Content-Type-Options", 308 | "value": "nosniff" 309 | }, 310 | { 311 | "name": "Server", 312 | "value": "WEBrick/1.3.1 (Ruby/1.9.3/2014-02-24)" 313 | }, 314 | { 315 | "name": "Date", 316 | "value": "Sun, 10 Jan 2016 09:45:10 GMT" 317 | }, 318 | { 319 | "name": "Connection", 320 | "value": "Keep-Alive" 321 | } 322 | ], 323 | "redirectURL": "", 324 | "headersSize": -1, 325 | "bodySize": 690, 326 | "content": { 327 | "size": 690, 328 | "mimeType": "application/json" 329 | } 330 | }, 331 | "cache": {}, 332 | "timings": { 333 | "blocked": 0, 334 | "dns": -1, 335 | "connect": -1, 336 | "send": 0, 337 | "wait": 8, 338 | "receive": 0, 339 | "ssl": -1 340 | }, 341 | "pageref": "http://localhost:8100" 342 | }, 343 | { 344 | "startedDateTime": "2016-01-10T09:45:10.347Z", 345 | "time": 8, 346 | "request": { 347 | "method": "GET", 348 | "url": "http://localhost:8100/api/fav-feeds/1", 349 | "httpVersion": "HTTP/1.1", 350 | "cookies": [], 351 | "headers": [ 352 | { 353 | "name": "X-Requested-With", 354 | "value": "XMLHttpRequest" 355 | }, 356 | { 357 | "name": "User-Agent", 358 | "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X) AppleWebKit/534.34 (KHTML, like Gecko) PhantomJS/1.8.1 Safari/534.34" 359 | }, 360 | { 361 | "name": "Accept", 362 | "value": "*/*" 363 | }, 364 | { 365 | "name": "Referer", 366 | "value": "http://localhost:8100/" 367 | } 368 | ], 369 | "queryString": [], 370 | "headersSize": -1, 371 | "bodySize": -1 372 | }, 373 | "response": { 374 | "status": 200, 375 | "statusText": "OK ", 376 | "httpVersion": "HTTP/1.1", 377 | "cookies": [], 378 | "headers": [ 379 | { 380 | "name": "Content-Type", 381 | "value": "application/json" 382 | }, 383 | { 384 | "name": "Content-Length", 385 | "value": "240" 386 | }, 387 | { 388 | "name": "X-Content-Type-Options", 389 | "value": "nosniff" 390 | }, 391 | { 392 | "name": "Server", 393 | "value": "WEBrick/1.3.1 (Ruby/1.9.3/2014-02-24)" 394 | }, 395 | { 396 | "name": "Date", 397 | "value": "Sun, 10 Jan 2016 09:45:10 GMT" 398 | }, 399 | { 400 | "name": "Connection", 401 | "value": "Keep-Alive" 402 | } 403 | ], 404 | "redirectURL": "", 405 | "headersSize": -1, 406 | "bodySize": 240, 407 | "content": { 408 | "size": 240, 409 | "mimeType": "application/json" 410 | } 411 | }, 412 | "cache": {}, 413 | "timings": { 414 | "blocked": 0, 415 | "dns": -1, 416 | "connect": -1, 417 | "send": 0, 418 | "wait": 7, 419 | "receive": 1, 420 | "ssl": -1 421 | }, 422 | "pageref": "http://localhost:8100" 423 | } 424 | ] 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /test/netsniff.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | if (!Date.prototype.toISOString) { 3 | Date.prototype.toISOString = function () { 4 | function pad(n) { return n < 10 ? '0' + n : n; } 5 | function ms(n) { return n < 10 ? '00'+ n : n < 100 ? '0' + n : n } 6 | return this.getFullYear() + '-' + 7 | pad(this.getMonth() + 1) + '-' + 8 | pad(this.getDate()) + 'T' + 9 | pad(this.getHours()) + ':' + 10 | pad(this.getMinutes()) + ':' + 11 | pad(this.getSeconds()) + '.' + 12 | ms(this.getMilliseconds()) + 'Z'; 13 | } 14 | } 15 | 16 | function createHAR(address, title, startTime, resources) 17 | { 18 | var entries = []; 19 | 20 | resources.forEach(function (resource) { 21 | var request = resource.request, 22 | startReply = resource.startReply, 23 | endReply = resource.endReply; 24 | 25 | if (!request || !startReply || !endReply) { 26 | return; 27 | } 28 | 29 | // Exclude Data URI from HAR file because 30 | // they aren't included in specification 31 | if (request.url.match(/(^data:image\/.*)/i)) { 32 | return; 33 | } 34 | 35 | entries.push({ 36 | startedDateTime: request.time.toISOString(), 37 | time: endReply.time - request.time, 38 | request: { 39 | method: request.method, 40 | url: request.url, 41 | httpVersion: "HTTP/1.1", 42 | cookies: [], 43 | headers: request.headers, 44 | queryString: [], 45 | headersSize: -1, 46 | bodySize: -1 47 | }, 48 | response: { 49 | status: endReply.status, 50 | statusText: endReply.statusText, 51 | httpVersion: "HTTP/1.1", 52 | cookies: [], 53 | headers: endReply.headers, 54 | redirectURL: "", 55 | headersSize: -1, 56 | bodySize: startReply.bodySize, 57 | content: { 58 | size: startReply.bodySize, 59 | mimeType: endReply.contentType 60 | } 61 | }, 62 | cache: {}, 63 | timings: { 64 | blocked: 0, 65 | dns: -1, 66 | connect: -1, 67 | send: 0, 68 | wait: startReply.time - request.time, 69 | receive: endReply.time - startReply.time, 70 | ssl: -1 71 | }, 72 | pageref: address 73 | }); 74 | }); 75 | 76 | return { 77 | log: { 78 | version: '1.2', 79 | creator: { 80 | name: "PhantomJS", 81 | version: phantom.version.major + '.' + phantom.version.minor + 82 | '.' + phantom.version.patch 83 | }, 84 | pages: [{ 85 | startedDateTime: startTime.toISOString(), 86 | id: address, 87 | title: title, 88 | pageTimings: { 89 | onLoad: page.endTime - page.startTime 90 | } 91 | }], 92 | entries: entries 93 | } 94 | }; 95 | } 96 | 97 | var page = require('webpage').create(), 98 | system = require('system'); 99 | 100 | if (system.args.length === 1) { 101 | console.log('Usage: netsniff.js '); 102 | phantom.exit(1); 103 | } else { 104 | 105 | page.address = system.args[1]; 106 | page.resources = []; 107 | 108 | page.onLoadStarted = function () { 109 | page.startTime = new Date(); 110 | }; 111 | 112 | page.onResourceRequested = function (req) { 113 | page.resources[req.id] = { 114 | request: req, 115 | startReply: null, 116 | endReply: null 117 | }; 118 | }; 119 | 120 | page.onResourceReceived = function (res) { 121 | if (res.stage === 'start') { 122 | page.resources[res.id].startReply = res; 123 | } 124 | if (res.stage === 'end') { 125 | page.resources[res.id].endReply = res; 126 | } 127 | }; 128 | 129 | page.open(page.address, function (status) { 130 | var har; 131 | if (status !== 'success') { 132 | console.log('FAIL to load the address'); 133 | phantom.exit(1); 134 | } else { 135 | page.endTime = new Date(); 136 | page.title = page.evaluate(function () { 137 | return document.title; 138 | }); 139 | har = createHAR(page.address, page.title, page.startTime, page.resources); 140 | console.log(JSON.stringify(har, undefined, 4)); 141 | phantom.exit(); 142 | } 143 | }); 144 | } -------------------------------------------------------------------------------- /upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | npm install 3 | gulp release 4 | aws s3 cp public/ s3://bookmarks-frontend \ 5 | --recursive \ 6 | --region us-west-2 \ 7 | --acl public-read 8 | --------------------------------------------------------------------------------