# encoding: UTF-8 require File.expand_path('../../test_helper', __FILE__) require 'fog' describe "Fields" do before do @site = setup_site @now = Time.now stub_time(@now) Spontaneous::State.delete @site.background_mode = :immediate end after do teardown_site end describe "New content instances" do before do @content_class = Class.new(Piece) do field :title, :default => "Magic" field :thumbnail, :image end @instance = @content_class.new end it "have fields with values defined by prototypes" do f = @instance.fields[:title] assert f.class < Spontaneous::Field::String f.value.must_equal "Magic" end it "have shortcut access methods to fields" do @instance.fields.thumbnail.must_equal @instance.fields[:thumbnail] end it "have a shortcut setter on the Content fields" do @instance.fields.title = "New Title" end it "have a shortcut getter on the Content instance itself" do @instance.title.must_equal @instance.fields[:title] end it "have a shortcut setter on the Content instance itself" do @instance.title = "Boing!" @instance.fields[:title].value.must_equal "Boing!" end # TODO: I want to allow this but don't like overwriting the ::fields # method like this. # it "allow the definition of multiple fields at once" do # content_class = Class.new(Piece) do # fields :title, :photo, :date # end # end end describe "Overwriting fields" do before do @class1 = Class.new(Piece) do field :title, :string, :default => "One" field :date, :string end @class2 = Class.new(@class1) do field :title, :image, :default => "Two", :title => "Two" end @class3 = Class.new(@class2) do field :date, :image, :default => "Three", :title => "Three" end @instance = @class2.new end it "overwrite field definitions" do @class2.fields.first.name.must_equal :title @class2.fields.last.name.must_equal :date @class2.fields.length.must_equal 2 @class2.fields.title.schema_id.must_equal @class1.fields.title.schema_id @class2.fields.title.title.must_equal "Two" @class2.fields.title.title.must_equal "Two" @class3.fields.date.title.must_equal "Three" @class3.fields.date.schema_id.must_equal @class1.fields.date.schema_id assert @instance.title.class < Spontaneous::Field::Image @instance.title.value.to_s.must_equal "Two" instance1 = @class1.new instance3 = @class3.new @instance.title.schema_id.must_equal instance1.title.schema_id instance1.title.schema_id.must_equal instance3.title.schema_id end end describe "Field Prototypes" do before do @content_class = Class.new(Piece) do field :title field :synopsis, :string end @content_class.field :complex, :image, :default => "My default", :comment => "Use this to" end it "be creatable with just a field name" do @content_class.field_prototypes[:title].must_be_instance_of(Spontaneous::Prototypes::FieldPrototype) @content_class.field_prototypes[:title].name.must_equal :title end it "work with just a name & options" do @content_class.field :minimal, :default => "Small" @content_class.field_prototypes[:minimal].name.must_equal :minimal @content_class.field_prototypes[:minimal].default.must_equal "Small" end it "default to basic string class" do assert @content_class.field_prototypes[:title].instance_class < Spontaneous::Field::String end it "map :string type to Field::String" do assert @content_class.field_prototypes[:synopsis].instance_class < Spontaneous::Field::String end it "be listable" do @content_class.field_names.must_equal [:title, :synopsis, :complex] end it "be testable for existance" do assert @content_class.field?(:title) assert @content_class.field?(:synopsis) refute @content_class.field?(:non_existant) i = @content_class.new assert i.field?(:title) refute i.field?(:non_existant) end describe "default values" do before do @prototype = @content_class.field_prototypes[:title] end it "default to a value of ''" do @prototype.default.must_equal "" end it "get recieve calculated default values if default is a proc" do n = 0 @content_class.field :dynamic, :default => proc { (n += 1) } instance = @content_class.new instance.dynamic.value.must_equal "1" instance = @content_class.new instance.dynamic.value.must_equal "2" end it "be able to calculate default values based on properties of owner" do @content_class.field :dynamic, :default => proc { |owner| owner.title.value } instance = @content_class.new(:title => "Frog") instance.dynamic.value.must_equal "Frog" end it "match name to type if sensible" do content_class = Class.new(Piece) do field :image field :date field :chunky end assert content_class.field_prototypes[:image].field_class < Spontaneous::Field::Image assert content_class.field_prototypes[:date].field_class < Spontaneous::Field::Date assert content_class.field_prototypes[:chunky].field_class < Spontaneous::Field::String end it "assigns the value of the field if the default is a proc" do n = 0 f = @content_class.field :dynamic, :default => proc { (n += 1) } f.dynamic_default?.must_equal true instance1 = @content_class.create instance1.dynamic.value.must_equal "1" instance1.reload instance1.dynamic.value.must_equal "1" end it "uses a dynamic default value to set the page slug" do n = 0 page_class = Class.new(::Page) page_class.field :title, default: proc { (n += 1) } page = page_class.create page.slug.must_equal "1" end end describe "Field titles" do before do @content_class = Class.new(Piece) do field :title field :having_fun_yet field :synopsis, :title => "Custom Title" field :description, :title => "Simple Description" end @title = @content_class.field_prototypes[:title] @having_fun = @content_class.field_prototypes[:having_fun_yet] @synopsis = @content_class.field_prototypes[:synopsis] @description = @content_class.field_prototypes[:description] end it "default to a sensible title" do @title.title.must_equal "Title" @having_fun.title.must_equal "Having Fun Yet" @synopsis.title.must_equal "Custom Title" @description.title.must_equal "Simple Description" end end describe "option parsing" do before do @prototype = @content_class.field_prototypes[:complex] end it "parse field class" do assert @prototype.field_class < Spontaneous::Field::Image end it "parse default value" do @prototype.default.must_equal "My default" end it "parse ui comment" do @prototype.comment.must_equal "Use this to" end end describe "sub-classes" do before do @subclass = Class.new(@content_class) do field :child_field end @subsubclass = Class.new(@subclass) do field :distant_relation end end it "inherit super class's field prototypes" do @subclass.field_names.must_equal [:title, :synopsis, :complex, :child_field] @subsubclass.field_names.must_equal [:title, :synopsis, :complex, :child_field, :distant_relation] end it "deal intelligently with manual setting of field order" do @reordered_class = Class.new(@subsubclass) do field_order :child_field, :complex end @reordered_class.field_names.must_equal [:child_field, :complex, :title, :synopsis, :distant_relation] end end describe "fallback values" do before do @content_class = Class.new(Piece) do field :title field :desc1, :fallback => :title field :desc2, :fallback => :desc1 field :desc3, :fallback => :desc9 end Object.send :const_set, :FieldWithFallbacks, @content_class @instance = @content_class.new(:title => "TITLE") end after do Object.send :remove_const, :FieldWithFallbacks rescue nil end it "uses the fallback value for empty fields" do @instance.desc1.value.must_equal "TITLE" end it "cascades the fallback" do @instance.desc2.value.must_equal "TITLE" end it "uses the field value if present" do @instance.desc2 = "DESC2" @instance.desc2.value.must_equal "DESC2" end it "deserializes values properly" do @instance.desc2 = "DESC2" @instance.save @instance.reload @instance.desc2.processed_values[:html].must_equal "DESC2" end it "ignores invalid field names" do @instance.desc3.value.must_equal "" end end end describe "Values" do before do @field_class = Class.new(S::Field::Base) do def outputs [:html, :plain, :fancy] end def generate_html(value, site) "<#{value}>" end def generate_plain(value, site) "*#{value}*" end def generate(output, value, site) case output when :fancy "#{value}!" else value end end end @field = @field_class.new end it "be used as the comparator" do f1 = @field_class.new f1.value = "a" f2 = @field_class.new f2.value = "b" (f1 <=> f2).must_equal -1 (f2 <=> f1).must_equal 1 (f1 <=> f1).must_equal 0 [f2, f1].sort.map(&:value).must_equal ["", ""] end it "be transformed by the update method" do @field.value = "Hello" @field.value.must_equal "" @field.value(:html).must_equal "" @field.value(:plain).must_equal "*Hello*" @field.value(:fancy).must_equal "Hello!" @field.unprocessed_value.must_equal "Hello" end it "appear in the to_s method" do @field.value = "String" @field.to_s.must_equal "" @field.to_s(:html).must_equal "" @field.to_s(:plain).must_equal "*String*" end it "escape ampersands by default" do field_class = Class.new(S::Field::String) do end field = field_class.new field.value = "Hello & Welcome" field.value(:html).must_equal "Hello & Welcome" field.value(:plain).must_equal "Hello & Welcome" end it "educate quotes" do field_class = Class.new(S::Field::String) field = field_class.new field.value = %("John's first... example") field.value(:html).must_equal "“John’s first… example”" field.value(:plain).must_equal "“John’s first… example”" end it "not process values coming from db" do class ContentClass1 < Piece end $transform = lambda { |value| "<#{value}>" } ContentClass1.field :title do def generate_html(value, site) $transform[value] end end instance = ContentClass1.new instance.fields.title = "Monkey" instance.save $transform = lambda { |value| "*#{value}*" } instance = ContentClass1[instance.id] instance.fields.title.value.must_equal "" Object.send(:remove_const, :ContentClass1) end end describe "field instances" do before do ::CC = Class.new(Piece) do field :title, :default => "Magic" do def generate_html(value, site) "*#{value}*" end end end @instance = CC.new end after do Object.send(:remove_const, :CC) end it "have a link back to their owner" do @instance.fields.title.owner.must_equal @instance end it "be created with the right default value" do f = @instance.fields.title f.value.must_equal "*Magic*" end it "eval blocks from prototype defn" do f = @instance.fields.title f.value = "Boo" f.value.must_equal "*Boo*" f.unprocessed_value.must_equal "Boo" end it "have a reference to their prototype" do f = @instance.fields.title f.prototype.must_equal CC.field_prototypes[:title] end it "return the item which isnt empty when using the / method" do a = CC.new(:title => "") b = CC.new(:title => "b") (a.title / b.title).must_equal b.title a.title = "a" (a.title / b.title).must_equal a.title end it "return the item which isnt empty when using the | method" do a = CC.new(:title => "") b = CC.new(:title => "b") (a.title | b.title).must_equal b.title a.title = "a" (a.title | b.title).must_equal a.title end it "return the item which isnt empty when using the or method" do a = CC.new(:title => "") b = CC.new(:title => "b") (a.title.or(b.title)).must_equal b.title a.title = "a" (a.title.or(b.title)).must_equal a.title end end describe "Field value persistence" do before do class ::PersistedField < Piece field :title, :default => "Magic" end end after do Object.send(:remove_const, :PersistedField) end it "work" do instance = ::PersistedField.new instance.fields.title.value = "Changed" instance.save id = instance.id instance = ::PersistedField[id] instance.fields.title.value.must_equal "Changed" end end describe "Value version" do before do class ::PersistedField < Piece field :title, :default => "Magic" end end after do Object.send(:remove_const, :PersistedField) end it "be increased after a change" do instance = ::PersistedField.new instance.fields.title.version.must_equal 0 instance.fields.title.value = "Changed" instance.save instance = ::PersistedField[instance.id] instance.fields.title.value.must_equal "Changed" instance.fields.title.version.must_equal 1 end it "not be increased if the value remains constant" do instance = ::PersistedField.new instance.fields.title.version.must_equal 0 instance.fields.title.value = "Changed" instance.save instance = ::PersistedField[instance.id] instance.fields.title.value = "Changed" instance.save instance = ::PersistedField[instance.id] instance.fields.title.value.must_equal "Changed" instance.fields.title.version.must_equal 1 instance.fields.title.value = "Changed!" instance.save instance = ::PersistedField[instance.id] instance.fields.title.version.must_equal 2 end end describe "Available output formats" do it "include HTML & PDF and default to default value" do f = S::Field::Base.new f.value = "Value" f.to_html.must_equal "Value" f.to_pdf.must_equal "Value" end end describe "String Fields" do before do @content_class = Class.new(::Piece) do field :title, :string end @instance = @content_class.new @field = @instance.title end it "should escape ampersands for the html format" do @field.value = "This & That" @field.value(:html).must_equal "This & That" end end describe "Markdown fields" do before do class ::MarkdownContent < Piece field :text1, :markdown field :text2, :richtext field :text3, :markup end @instance = MarkdownContent.new end after do Object.send(:remove_const, :MarkdownContent) end it "be available as the :markdown type" do assert MarkdownContent.field_prototypes[:text1].field_class < Spontaneous::Field::Markdown end it "be available as the :richtext type" do assert MarkdownContent.field_prototypes[:text2].field_class < Spontaneous::Field::Markdown end it "be available as the :markup type" do assert MarkdownContent.field_prototypes[:text3].field_class < Spontaneous::Field::Markdown end it "process input into HTML" do @instance.text1 = "*Hello* **World**" @instance.text1.value.must_equal "

Hello World

\n" end it "use more sensible linebreaks" do @instance.text1 = "With\nLinebreak" @instance.text1.value.must_equal "

With
\nLinebreak

\n" @instance.text2 = "With \nLinebreak" @instance.text2.value.must_equal "

With
\nLinebreak

\n" end end describe "LongString fields" do before do class ::LongStringContent < Piece field :long1, :longstring field :long2, :long_string field :long3, :text end @instance = LongStringContent.new end after do Object.send(:remove_const, :LongStringContent) end it "is available as the :longstring type" do assert LongStringContent.field_prototypes[:long1].field_class < Spontaneous::Field::LongString end it "is available as the :long_string type" do assert LongStringContent.field_prototypes[:long2].field_class < Spontaneous::Field::LongString end it "is available as the :text type" do assert LongStringContent.field_prototypes[:long3].field_class < Spontaneous::Field::LongString end it "translates newlines to
tags" do @instance.long1 = "this\nlong\nstring" @instance.long1.value.must_equal "this
\nlong
\nstring" end end describe "Editor classes" do it "be defined in base types" do base_class = Spontaneous::Field::Image base_class.editor_class.must_equal "Spontaneous.Field.Image" base_class = Spontaneous::Field::Date base_class.editor_class.must_equal "Spontaneous.Field.Date" base_class = Spontaneous::Field::Markdown base_class.editor_class.must_equal "Spontaneous.Field.Markdown" base_class = Spontaneous::Field::String base_class.editor_class.must_equal "Spontaneous.Field.String" end it "be inherited in subclasses" do base_class = Spontaneous::Field::Image @field_class = Class.new(base_class) @field_class.stubs(:name).returns("CustomField") @field_class.editor_class.must_equal base_class.editor_class @field_class2 = Class.new(@field_class) @field_class2.stubs(:name).returns("CustomField2") @field_class2.editor_class.must_equal base_class.editor_class end it "correctly defined by field prototypes" do base_class = Spontaneous::Field::Image class ::CustomField < Spontaneous::Field::Image self.register(:custom) end class ::CustomContent < ::Piece field :custom end assert CustomContent.fields.custom.instance_class < CustomField CustomContent.fields.custom.instance_class.editor_class.must_equal Spontaneous::Field::Image.editor_class Object.send(:remove_const, :CustomContent) Object.send(:remove_const, :CustomField) end end describe "Field versions" do before do @user = Spontaneous::Permissions::User.create(:email => "user@example.com", :login => "user", :name => "user", :password => "rootpass") @user.reload class ::Piece field :title end # @content_class.stubs(:name).returns("ContentClass") @instance = ::Piece.create end after do # Object.send(:remove_const, :Piece) rescue nil Spontaneous::Permissions::User.delete ::Content.delete S::Field::FieldVersion.delete end it "start out as empty" do assert @instance.title.versions.empty?, "Field version list should be empty" end it "be created every time a field is modified" do @instance.title.value = "one" @instance.save.reload v = @instance.title.versions v.count.must_equal 1 end it "marks a field as unmodified after save" do @instance.title.value = "one" @instance.save.reload @instance.title.modified?.must_equal false end it "have a creation date" do now = Time.now + 1000 stub_time(now) @instance.title.value = "one" @instance.save.reload @instance.reload vv = @instance.title.versions v = vv.first v.created_at.to_i.must_equal now.to_i end it "save the previous value" do stub_time(@now) @instance.title.value = "one" @instance.save.reload vv = @instance.title.versions v = vv.first v.value.must_equal "" stub_time(@now+10) @instance.title.value = "two" @instance.save.reload vv = @instance.title.versions v = vv.first v.value.must_equal "one" stub_time(@now+20) @instance.title.value = "three" @instance.save.reload vv = @instance.title.versions v = vv.first v.value.must_equal "two" end it "keep a track of the version number" do stub_time(@now) @instance.title.value = "one" @instance.save.reload vv = @instance.title.versions v = vv.first v.version.must_equal 1 stub_time(@now+10) @instance.title.value = "two" @instance.save.reload vv = @instance.title.versions vv.count.must_equal 2 v = vv.first v.version.must_equal 2 end it "remember the responsible editor" do @instance.current_editor = @user @instance.title.value = "one" @instance.save.reload vv = @instance.title.versions v = vv.first v.user.must_equal @user end it "have quick access to the last version" do stub_time(@now) @instance.title.value = "one" @instance.save.reload vv = @instance.title.versions v = vv.first v.value.must_equal "" stub_time(@now+10) @instance.title.value = "two" @instance.save.reload vv = @instance.title.versions v = vv.first v.value.must_equal "one" @instance.title.previous_version.value.must_equal "one" end end describe "String fields" do it "be aliased to the :title type" do @content_class = Class.new(::Piece) do field :title, default: "Right" field :something, :title end instance = @content_class.new assert instance.fields.title.class.ancestors.include?(Spontaneous::Field::String), ":title type should inherit from StringField" instance.title.value.must_equal "Right" end end describe "WebVideo fields" do before do @content_class = Class.new(::Piece) do field :video, :webvideo end @content_class.stubs(:name).returns("ContentClass") @instance = @content_class.new @field = @instance.video end it "have their own editor type" do @content_class.fields.video.export(nil)[:type].must_equal "Spontaneous.Field.WebVideo" @instance.video = "http://www.youtube.com/watch?v=_0jroAM_pO4&feature=feedrec_grec_index" fields = @instance.export(nil)[:fields] fields[0][:processed_value].must_equal @instance.video.src end it "recognise youtube URLs" do @instance.video = "http://www.youtube.com/watch?v=_0jroAM_pO4&feature=feedrec_grec_index" @instance.video.value.must_equal "http://www.youtube.com/watch?v=_0jroAM_pO4&feature=feedrec_grec_index" @instance.video.video_id.must_equal "_0jroAM_pO4" @instance.video.provider_id.must_equal "youtube" end it "recognise Vimeo URLs" do @instance.video = "http://vimeo.com/31836285" @instance.video.value.must_equal "http://vimeo.com/31836285" @instance.video.video_id.must_equal "31836285" @instance.video.provider_id.must_equal "vimeo" end it "recognise Vine URLs" do @instance.video = "https://vine.co/v/brI7pTPb3qU" @instance.video.value.must_equal "https://vine.co/v/brI7pTPb3qU" @instance.video.video_id.must_equal "brI7pTPb3qU" @instance.video.provider_id.must_equal "vine" end it "silently handles unknown providers" do @instance.video = "https://idontdovideo.com/video?id=brI7pTPb3qU" @instance.video.value.must_equal "https://idontdovideo.com/video?id=brI7pTPb3qU" @instance.video.video_id.must_equal "https://idontdovideo.com/video?id=brI7pTPb3qU" @instance.video.provider_id.must_equal nil end it "use the YouTube api to extract video metadata" do youtube_info = {"thumbnail_large" => "http://i.ytimg.com/vi/_0jroAM_pO4/hqdefault.jpg", "thumbnail_small"=>"http://i.ytimg.com/vi/_0jroAM_pO4/default.jpg", "title" => "Hilarious QI Moment - Cricket", "description" => "Rob Brydon makes a rather embarassing choice of words whilst discussing the relationship between a cricket's chirping and the temperature. Taken from QI XL Series H episode 11 - Highs and Lows", "user_name" => "morthasa", "upload_date" => "2011-01-14 19:49:44", "tags" => "Hilarious, QI, Moment, Cricket, fun, 11, stephen, fry, alan, davies, Rob, Brydon, SeriesH, Fred, MacAulay, Sandi, Toksvig", "duration" => 78, "stats_number_of_likes" => 297, "stats_number_of_plays" => 53295, "stats_number_of_comments" => 46}#.symbolize_keys response_xml_file = File.expand_path("../../fixtures/fields/youtube_api_response.xml", __FILE__) connection = mock() Spontaneous::Field::WebVideo::YouTube.any_instance.expects(:open).with("http://gdata.youtube.com/feeds/api/videos/_0jroAM_pO4?v=2").returns(connection) doc = Nokogiri::XML(File.open(response_xml_file)) Nokogiri.expects(:XML).with(connection).returns(doc) @field.value = "http://www.youtube.com/watch?v=_0jroAM_pO4" @field.values.must_equal youtube_info.merge(:video_id => "_0jroAM_pO4", :provider => "youtube", :html => "http://www.youtube.com/watch?v=_0jroAM_pO4") end it "use the Vimeo api to extract video metadata" do vimeo_info = {"id"=>29987529, "title"=>"Neon Indian Plays The UO Music Shop", "description"=>"Neon Indian plays electronic instruments from the UO Music Shop, Fall 2011. Read more at blog.urbanoutfitters.com.", "url"=>"http://vimeo.com/29987529", "upload_date"=>"2011-10-03 18:32:47", "mobile_url"=>"http://vimeo.com/m/29987529", "thumbnail_small"=>"http://b.vimeocdn.com/ts/203/565/203565974_100.jpg", "thumbnail_medium"=>"http://b.vimeocdn.com/ts/203/565/203565974_200.jpg", "thumbnail_large"=>"http://b.vimeocdn.com/ts/203/565/203565974_640.jpg", "user_name"=>"Urban Outfitters", "user_url"=>"http://vimeo.com/urbanoutfitters", "user_portrait_small"=>"http://b.vimeocdn.com/ps/251/111/2511118_30.jpg", "user_portrait_medium"=>"http://b.vimeocdn.com/ps/251/111/2511118_75.jpg", "user_portrait_large"=>"http://b.vimeocdn.com/ps/251/111/2511118_100.jpg", "user_portrait_huge"=>"http://b.vimeocdn.com/ps/251/111/2511118_300.jpg", "stats_number_of_likes"=>85, "stats_number_of_plays"=>26633, "stats_number_of_comments"=>0, "duration"=>100, "width"=>1280, "height"=>360, "tags"=>"neon indian, analog, korg, moog, theremin, micropiano, microkorg, kaossilator, kaossilator pro", "embed_privacy"=>"anywhere"}.symbolize_keys connection = mock() connection.expects(:read).returns(Spontaneous.encode_json([vimeo_info])) Spontaneous::Field::WebVideo::Vimeo.any_instance.expects(:open).with("http://vimeo.com/api/v2/video/29987529.json").returns(connection) @field.value = "http://vimeo.com/29987529" @field.values.must_equal vimeo_info.merge(:video_id => "29987529", :provider => "vimeo", :html => "http://vimeo.com/29987529") end describe "with player settings" do before do @content_class.field :video2, :webvideo, :player => { :width => 680, :height => 384, :fullscreen => true, :autoplay => true, :loop => true, :showinfo => false, :youtube => { :theme => 'light', :hd => true, :controls => false }, :vimeo => { :color => "ccc", :api => true } } @instance = @content_class.new @field = @instance.video2 end it "use the configuration in the youtube player HTML" do @field.value = "http://www.youtube.com/watch?v=_0jroAM_pO4&feature=feedrec_grec_index" html = @field.render(:html) html.must_match /^