├── .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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
15 |
Enjoy your reading
16 |
17 |
18 | -
19 |
Feeds: 3
20 |
21 | -
22 |
Liked: 1
23 |
24 |
25 |
26 |
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 |
--------------------------------------------------------------------------------