diff --git a/app/assets/javascripts/app/pages/profile.js b/app/assets/javascripts/app/pages/profile.js
index 1c1a7e5f3b03aebf48c1a7a990febe7d9f8f9304..17a21ea3b7aad7699ed581394eed1ce86267c6da 100644
--- a/app/assets/javascripts/app/pages/profile.js
+++ b/app/assets/javascripts/app/pages/profile.js
@@ -14,13 +14,8 @@ app.pages.Profile = app.views.Base.extend({
   tooltipSelector: '.profile_button div, .sharing_message_container',
 
   initialize: function(opts) {
-    if( app.hasPreload('person') ) {
-      this.model = new app.models.Person(app.parsePreload('person'));
-    } else if(opts && opts.person_id) {
-      this.model = new app.models.Person({guid: opts.person_id});
-      this.model.fetch();
-    } else {
-      throw new Error("unable to load person");
+    if( !this.model ) {
+      this._populateModel(opts);
     }
 
     if( app.hasPreload('photos') )
@@ -41,6 +36,17 @@ app.pages.Profile = app.views.Base.extend({
     app.events.on('aspect_membership:update', this.reload, this);
   },
 
+  _populateModel: function(opts) {
+    if( app.hasPreload('person') ) {
+      this.model = new app.models.Person(app.parsePreload('person'));
+    } else if(opts && opts.person_id) {
+      this.model = new app.models.Person({guid: opts.person_id});
+      this.model.fetch();
+    } else {
+      throw new Error("unable to load person");
+    }
+  },
+
   sidebarView: function() {
     if( !this.model.has('profile') ) return false;
     return new app.views.ProfileSidebar({
diff --git a/app/assets/javascripts/widgets/direction-detector.js b/app/assets/javascripts/widgets/direction-detector.js
index 2db21a1f797a22180cf400ef43437eeb784641ef..101c1e50ae87ead01e53210fc64dd7c396a0636c 100644
--- a/app/assets/javascripts/widgets/direction-detector.js
+++ b/app/assets/javascripts/widgets/direction-detector.js
@@ -17,7 +17,7 @@
       });
     });
 
-    this.isRTL = app.helpers.txtDirection;
+    this.isRTL = app.helpers.txtDirection.isRTL;
 
     this.updateBinds = function() {
       $.each(self.binds, function(index, bind) {
diff --git a/spec/javascripts/app/helpers/direction_detector_spec.js b/spec/javascripts/app/helpers/direction_detector_spec.js
index 6ade3d00b40c33e938a7687950a3ac7f6f69b15d..2067530d8519f1c8d1c27a7b531cb7bd30553987 100644
--- a/spec/javascripts/app/helpers/direction_detector_spec.js
+++ b/spec/javascripts/app/helpers/direction_detector_spec.js
@@ -32,6 +32,3 @@ describe("app.helpers.txtDirection", function() {
     });
   });
 });
-
-
-101
diff --git a/spec/javascripts/app/models/person_spec.js b/spec/javascripts/app/models/person_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b36b8beb685c321ccc20560c8571bd1db81a1010
--- /dev/null
+++ b/spec/javascripts/app/models/person_spec.js
@@ -0,0 +1,69 @@
+
+describe("app.models.Person", function() {
+  beforeEach(function() {
+    this.mutual_contact = factory.person({relationship: 'mutual'});
+    this.sharing_contact = factory.person({relationship :'sharing'});
+    this.receiving_contact = factory.person({relationship: 'receiving'});
+    this.blocked_contact = factory.person({relationship: 'blocked', block: {id: 1}});
+  });
+
+  context("#isSharing", function() {
+    it("indicates if the person is sharing", function() {
+      expect(this.mutual_contact.isSharing()).toBeTruthy();
+      expect(this.sharing_contact.isSharing()).toBeTruthy();
+
+      expect(this.receiving_contact.isSharing()).toBeFalsy();
+      expect(this.blocked_contact.isSharing()).toBeFalsy();
+    });
+  });
+
+  context("#isReceiving", function() {
+    it("indicates if the person is receiving", function() {
+      expect(this.mutual_contact.isReceiving()).toBeTruthy();
+      expect(this.receiving_contact.isReceiving()).toBeTruthy();
+
+      expect(this.sharing_contact.isReceiving()).toBeFalsy();
+      expect(this.blocked_contact.isReceiving()).toBeFalsy();
+    });
+  });
+
+  context("#isMutual", function() {
+    it("indicates if we share mutually with the person", function() {
+      expect(this.mutual_contact.isMutual()).toBeTruthy();
+
+      expect(this.receiving_contact.isMutual()).toBeFalsy();
+      expect(this.sharing_contact.isMutual()).toBeFalsy();
+      expect(this.blocked_contact.isMutual()).toBeFalsy();
+    });
+  });
+
+  context("#isBlocked", function() {
+    it("indicates whether we blocked the person", function() {
+      expect(this.blocked_contact.isBlocked()).toBeTruthy();
+
+      expect(this.mutual_contact.isBlocked()).toBeFalsy();
+      expect(this.receiving_contact.isBlocked()).toBeFalsy();
+      expect(this.sharing_contact.isBlocked()).toBeFalsy();
+    });
+  });
+
+  context("#block", function() {
+    it("POSTs a block to the server", function() {
+      this.sharing_contact.block();
+      var request = jasmine.Ajax.requests.mostRecent();
+
+      expect(request.method).toEqual("POST");
+      expect($.parseJSON(request.params).block.person_id).toEqual(this.sharing_contact.id);
+    });
+  });
+
+  context("#unblock", function() {
+    it("DELETEs a block from the server", function(){
+      this.blocked_contact.unblock();
+      var request = jasmine.Ajax.requests.mostRecent();
+
+      expect(request.method).toEqual("DELETE");
+      expect(request.url).toEqual(Routes.block_path(this.blocked_contact.get('block').id));
+    });
+  });
+});
diff --git a/spec/javascripts/app/pages/profile_spec.js b/spec/javascripts/app/pages/profile_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..bd527737154021b7d0ce4daf19ae5a2650d2d995
--- /dev/null
+++ b/spec/javascripts/app/pages/profile_spec.js
@@ -0,0 +1,24 @@
+
+describe("app.pages.Profile", function() {
+  beforeEach(function() {
+    this.model = factory.person();
+    spyOn(this.model, 'block').and.returnValue($.Deferred());
+    spyOn(this.model, 'unblock').and.returnValue($.Deferred());
+    this.view = new app.pages.Profile({model: this.model});
+  });
+
+  context("#blockPerson", function() {
+    it("calls person#block", function() {
+      spyOn(window, 'confirm').and.returnValue(true);
+      this.view.blockPerson();
+      expect(this.model.block).toHaveBeenCalled();
+    });
+  });
+
+  context("#unblockPerson", function() {
+    it("calls person#unblock", function() {
+      this.view.unblockPerson();
+      expect(this.model.unblock).toHaveBeenCalled();
+    });
+  });
+});
diff --git a/spec/javascripts/app/views/profile_header_view_spec.js b/spec/javascripts/app/views/profile_header_view_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..82a746c2d188345caba8a674659e3bd14a3736f1
--- /dev/null
+++ b/spec/javascripts/app/views/profile_header_view_spec.js
@@ -0,0 +1,26 @@
+
+describe("app.views.ProfileHeader", function() {
+  beforeEach(function() {
+    this.model = factory.personWithProfile({
+      diaspora_id: "my@pod",
+      name: "User Name",
+      profile: { tags: ['test'] }
+    });
+    this.view = new app.views.ProfileHeader({model: this.model});
+  });
+
+  context("#presenter", function() {
+    it("contains necessary elements", function() {
+      expect(this.view.presenter()).toEqual(jasmine.objectContaining({
+        diaspora_id: "my@pod",
+        name: "User Name",
+        is_blocked: false,
+        is_own_profile: false,
+        has_tags: true,
+        profile: jasmine.objectContaining({
+          tags: ['test']
+        })
+      }));
+    });
+  });
+});
diff --git a/spec/javascripts/app/views/profile_sidebar_view_spec.js b/spec/javascripts/app/views/profile_sidebar_view_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..d241dc7cec5b0b50b0357deae4abdf14f74700f1
--- /dev/null
+++ b/spec/javascripts/app/views/profile_sidebar_view_spec.js
@@ -0,0 +1,42 @@
+
+describe("app.views.ProfileSidebar", function() {
+  beforeEach(function() {
+    this.model = factory.personWithProfile({
+      diaspora_id: "alice@umbrella.corp",
+      name: "Project Alice",
+      relationship: 'mutual',
+      profile: {
+        bio: "confidential",
+        location: "underground",
+        gender: "female",
+        birthday: "2012-09-14",
+        tags: ['zombies', 'evil', 'blood', 'gore']
+
+      }
+    });
+    this.view = new app.views.ProfileSidebar({model: this.model});
+
+    loginAs(factory.userAttrs());
+  });
+
+  context("#presenter", function() {
+    it("contains necessary elements", function() {
+      console.log(this.view.presenter());
+      expect(this.view.presenter()).toEqual(jasmine.objectContaining({
+        relationship: 'mutual',
+        do_profile_btns: true,
+        do_profile_info: true,
+        is_sharing: true,
+        is_receiving: true,
+        is_mutual: true,
+        is_not_blocked: true,
+        profile: jasmine.objectContaining({
+          bio: "confidential",
+          location: "underground",
+          gender: "female",
+          birthday: "2012-09-14"
+        })
+      }));
+    });
+  });
+});
diff --git a/spec/javascripts/helpers/factory.js b/spec/javascripts/helpers/factory.js
index b8e5d360b43803d7355bdae31dd4d53fef44dd59..b0e27ef872484edca7ecb47315cfaa94b380275e 100644
--- a/spec/javascripts/helpers/factory.js
+++ b/spec/javascripts/helpers/factory.js
@@ -79,8 +79,8 @@ factory = {
     }
   },
 
-  profile : function(overrides) {
-    var id = overrides && overrides.id || factory.id.next()
+  profileAttrs: function(overrides) {
+    var id = (overrides && overrides.id) ? overrides.id : factory.id.next();
     var defaults = {
       "bio": "I am a cat lover and I love to run",
       "birthday": "2012-04-17",
@@ -99,9 +99,38 @@ factory = {
       "person_id": "person" + id,
       "searchable": true,
       "updated_at": "2012-04-17T23:48:36Z"
-    }
+    };
+    return _.extend({}, defaults, overrides);
+  },
+
+  profile : function(overrides) {
+    return new app.models.Profile(factory.profileAttrs(overrides));
+  },
+
+  personAttrs: function(overrides) {
+    var id = (overrides && overrides.id) ? overrides.id : factory.id.next();
+    var defaults = {
+      "id": id,
+      "guid": factory.guid(),
+      "name": "Bob Grimm",
+      "diaspora_id": "bob@localhost:3000",
+      "relationship": "sharing",
+      "is_own_profile": false
+    };
+    return _.extend({}, defaults, overrides);
+  },
 
-    return new app.models.Profile(_.extend(defaults, overrides))
+  person: function(overrides) {
+    return new app.models.Person(factory.personAttrs(overrides));
+  },
+
+  personWithProfile: function(overrides) {
+    var profile_overrides = _.clone(overrides.profile);
+    delete overrides.profile;
+    var defaults = {
+      profile: factory.profileAttrs(profile_overrides)
+    };
+    return factory.person(_.extend({}, defaults, overrides));
   },
 
   photoAttrs : function(overrides){
diff --git a/spec/presenters/base_presenter_spec.rb b/spec/presenters/base_presenter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..561d1993f4b91c8f79eb4644b52a0238188dbfb9
--- /dev/null
+++ b/spec/presenters/base_presenter_spec.rb
@@ -0,0 +1,25 @@
+require "spec_helper"
+
+describe BasePresenter do
+  it "falls back to nil" do
+    p = BasePresenter.new(nil)
+    expect(p.anything).to be(nil)
+    expect { p.otherthing }.not_to raise_error
+  end
+
+  it "calls methods on the wrapped object" do
+    obj = double(hello: "world")
+    p = BasePresenter.new(obj)
+
+    expect(p.hello).to eql("world")
+    expect(obj).to have_received(:hello)
+  end
+
+  describe "#as_collection" do
+    it "returns an array of data" do
+      coll = [double(data: "one"), double(data: "two"), double(data: "three")]
+      res = BasePresenter.as_collection(coll, :data)
+      expect(res).to eql(["one", "two", "three"])
+    end
+  end
+end