├── .gitignore ├── .travis.yml ├── CHANGES.textile ├── Gemfile ├── README.md ├── Rakefile ├── apotomo.gemspec ├── config └── routes.rb ├── gemfiles ├── Gemfile.rails3-0 ├── Gemfile.rails3-1 ├── Gemfile.rails3-2 └── Gemfile.rails4-0 ├── lib ├── apotomo.rb ├── apotomo │ ├── apotomo.rake │ ├── debugging.rb │ ├── event.rb │ ├── event_handler.rb │ ├── invoke_event_handler.rb │ ├── javascript_generator.rb │ ├── rails │ │ ├── controller_methods.rb │ │ └── view_helper.rb │ ├── railtie.rb │ ├── request_processor.rb │ ├── test_case.rb │ ├── version.rb │ ├── widget.rb │ ├── widget │ │ ├── event_methods.rb │ │ ├── javascript_methods.rb │ │ └── tree_node.rb │ └── widget_shortcuts.rb └── generators │ ├── apotomo │ ├── USAGE │ └── widget_generator.rb │ ├── erb │ └── widget_generator.rb │ ├── haml │ └── widget_generator.rb │ ├── slim │ └── widget_generator.rb │ ├── templates │ ├── view.erb │ ├── view.haml │ ├── view.slim │ ├── widget.rb │ └── widget_test.rb │ └── test_unit │ └── widget_generator.rb └── test ├── apotomo_test.rb ├── dummy ├── Rakefile ├── app │ ├── controllers │ │ └── application_controller.rb │ ├── helpers │ │ └── application_helper.rb │ └── views │ │ └── layouts │ │ └── application.html.erb ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── backtrace_silencers.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── secret_token.rb │ │ └── session_store.rb │ ├── locales │ │ └── en.yml │ └── routes.rb ├── db │ └── test.sqlite3 └── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── favicon.ico │ └── stylesheets │ └── .gitkeep ├── event_handler_test.rb ├── event_methods_test.rb ├── event_test.rb ├── invoke_event_handler_test.rb ├── javascript_generator_test.rb ├── rails ├── caching_test.rb ├── controller_methods_test.rb ├── rails_integration_test.rb ├── view_helper_test.rb └── widget_generator_test.rb ├── render_test.rb ├── request_processor_test.rb ├── test_case_methods.rb ├── test_case_test.rb ├── test_helper.rb ├── tree_node_test.rb ├── widget_shortcuts_test.rb ├── widget_test.rb └── widgets └── mouse ├── eat.erb ├── eating.html.erb ├── educate.html.erb ├── feed.html.erb ├── make_me_squeak.html.erb └── snuggle.html.erb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | pkg 3 | *.gem 4 | .*~ 5 | .bundle 6 | Gemfile*.lock 7 | test/dummy/log/ 8 | test/dummy/tmp/ 9 | /.rvmrc 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | - 2.0.0 4 | notifications: 5 | irc: "irc.freenode.org#cells" 6 | gemfile: 7 | - gemfiles/Gemfile.rails3-0 8 | - gemfiles/Gemfile.rails3-1 9 | - gemfiles/Gemfile.rails3-2 10 | - gemfiles/Gemfile.rails4-0 11 | -------------------------------------------------------------------------------- /CHANGES.textile: -------------------------------------------------------------------------------- 1 | h2. 1.3.2 2 | 3 | * Limit Cells to < 4.0.0. This is the last release of Apotomo, it will be superceded by "cells-widget":https://github.com/apotonick/cells-widget. 4 | 5 | h2. 1.3.1 6 | 7 | * Fix the routing problem with Rails 4.2. 8 | 9 | h2. 1.3.0 10 | 11 | * Removed `Widget::DEFAULT_VIEW_PATHS`. This makes it compatible with Cells 3.11. 12 | 13 | h2. 1.2.6 14 | 15 | * Upgrade to newest hooks and uber, fixing `uninitialized constant Hooks::InheritableAttribute ` (fixes #144). 16 | 17 | h2. 1.2.5 18 | 19 | * Introduce @#widget_tag@ helper allowing you to wrap widget content in an arbitrary tag. Extends @#widget_div@. 20 | 21 | h2. 1.2.4 22 | 23 | * Make it work with Rails 4. 24 | 25 | h2. 1.2.3 26 | 27 | * Maintenance release to make happy people happy. 28 | 29 | h2. 1.2.2 30 | 31 | * Derive @Apotomo::Widget@ from @Cell::Rails@ now to make it compatible with 3.8. 32 | 33 | h2. 1.2.1 34 | 35 | h3. Changes 36 | * Save the @options hash in the constructor since we cannot rely on cells anymore. 37 | 38 | h2. 1.2.0 39 | 40 | h3. Changes 41 | * The @Widget.new@ constructor now expects the parent widget as first argument (except for the root widget, which still gets the damned ActionController instance). Nobody needed orphaned widgets so far so we decided to make it simpler. This makes the @#<<@ method a DSL-method, only. 42 | * You can now access the root widget instance (or any other parent widget) in any subsequent @has_widgets@ block. This fixes "a famous issue":https://github.com/apotonick/apotomo/issues/34 and makes the @:passing@ option work as expected at every tree level. 43 | * The @#widget@ shortcut method no longer returns a valid widget instance but a DSL-specific thing. Use the real constructor form if you need it right away (@MouseWidget.new(parent, :kid)@) or access the instance afterwards (@root[:kid]@). 44 | * Removed the @after_add@ hook. Now that adding happens in the constructor, hook into @after_initialize@. 45 | * Removed @#param@ in favor of @#options@. 46 | * Removed @#emit@ in favor of @#render@. 47 | * Removed @#remove_all!@ and @#remove_from_parent!@. Did you ever use these? 48 | 49 | h2. 1.1.4 50 | 51 | h3. Changes 52 | * New signature: @TestCase#trigger(type, source, options)@. This method no longer messes around with the widget's options hash but simply passes any additional options to @evt#data@. Also, the event @source@ is now the required second argument. 53 | * We now got @TestCase#view_assigns@ to access instance variables from the last rendered widget. 54 | 55 | h2. 1.1.1 56 | 57 | h3. Additions 58 | * You can now attach event handlers to other widgets simply by using the @:passing@ option: @responds_to_event :click, :passing => :root@. 59 | * If you want to debug events, just include the (WIP) @apotomo/debugging@ file and watch your server output on the console. 60 | 61 | h3. Changes 62 | * @Widget.responds_to_event@ is inheritable now. 63 | * The generator now places namespaced widgets into the correct sub-directories. 64 | 65 | h3. Removals 66 | * Removed the alias @Widget.respond_to_event@, please use @Widget.responds_to_event@. 67 | 68 | h2. 1.1 69 | 70 | h3. Changes 71 | * Widgets now reside in @app/widgets/@ and have the @Widget@ appendix per convention, identical to cells and controllers. This introduces the following changes, illustrated with an imaginary @CommentsWidget@: 72 | ** @widget(:comments, ...)@ will search and instantiate @CommentsWidget@ (note the missing "Widget"). 73 | ** The @CommentsWidget@ views are searched in @app/widgets/comments/@ (note the missing "_widget"). 74 | * Including Apotomo::Rails::ControllerMethods by hand is optional. The first call to Controller.has_widgets will include the real module. 75 | * You can now @#render_widget@ in your widget views to render children. 76 | * @#widget@ now accepts one arg only (the widget prefix). @widget(:comments)@ sets id to @:comments@. We also removed the start state argument. 77 | * Start states are no longer accepted, you have to explicitely pass the state to #render_widget. That's why #invoke now requires an explicit state as well. 78 | * #replace/#update now append a @;@ to the JavaScript expression. You may pass an arbitrary selector string as first argument (optional). 79 | * The event is no longer added to #options at triggering time. 80 | * Options passed in #render_widget are no longer merged with #options. They are passed as state-args to reduce complexity. 81 | * We no longer have @opts and @params but #options and #params from Cell::Base. 82 | * The params hash is appended to the event in RequestProcessor#process_for. You can now live completely without accessing the global #params. 83 | * #respond/#update with :selector does not longer prefix the selector with #. It's up to you. 84 | * Event delegates #[] to #data. 85 | 86 | h3. Removals 87 | * Removed #param, it is simply too complex. 88 | * Removed render :render_children and :invoke options. Use #render_widget in your state/view. 89 | * Removed render :suppress_js as it was ugly and kills children. 90 | * Removed #jump_to_state and #last_state. 91 | * Removed `@cell`, now longer available in state views. 92 | * Removed #cell, #container and #section widget shortcuts. 93 | * Removed @rendered_children@ local in views. Use #render_widget. 94 | * Removed ContainerWidget, nobody needs it. 95 | 96 | h3. Bugfixes 97 | * Widget.responds_to_event no longer shares its options with multiple widget instances of the same class. 98 | 99 | 100 | h2. 1.0.4 101 | 102 | h3. Bugfixes 103 | * Caching states works, again. Thanks to Gudleik Rasch for spotting. 104 | * We cleanly require cells-3.4.x now. Thanks to Ryan Bates who remarked that Apotomo 1.0.x tries to require cells-3.5, which was wrong. 105 | 106 | h3. Notes 107 | 108 | * Removed the usage of state-args. If you want the event instance in your triggered state, use @opts[:event] or upgrade to apotomo-1.1. 109 | 110 | 111 | h2. 1.0.3 112 | 113 | h3. Bugfixes 114 | * fixed gemspec to not require useless gem dependencies. 115 | 116 | 117 | h2. 1.0.2 118 | 119 | h3. Changes 120 | * removals from ViewHelper: #trigger_event, #form_to_event and friends as they use deprecated Rails helpers. 121 | 122 | h3. Bugfixes 123 | * Widget#fire now accepts payload data for the fired event. 124 | * triggered states now receive the event object if they expect one argument. 125 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apotomo 2 | 3 | **Web Components for Rails.** 4 | 5 | 6 | ## Overview 7 | 8 | Do you need an _interactive user interface_ for your Rails application? A cool Rich Client Application with dashboards, portlets and AJAX, Drag&Drop and jQuery? 9 | 10 | Is your controller gettin' fat? And your partial-helper-AJAX pile is getting out of control? 11 | 12 | Do you want a framework to make the implementation easier? _You want Apotomo._ 13 | 14 | **Apotomo** is based on [Cells](http://github.com/apotonick/cells), the popular View Components framework for Rails. 15 | 16 | It gives you widgets and encapsulation, bubbling events, AJAX page updates, rock-solid testing and more. Check out for more information. 17 | 18 | ## Installation 19 | 20 | Easy as hell. You just need Ruby 1.9.3/2.0.0 and Rails 3/4. 21 | 22 | Add Apotomo to your `Gemfile`: 23 | 24 | ```ruby 25 | gem 'apotomo' 26 | ``` 27 | 28 | ## Example 29 | 30 | A _shitty_ example is worse than a _shitty_ framework, so let's choose wisely... 31 | 32 | Say you had a blog application. The page showing the post should have a comments block, with a list of comments and a form to post a new comment. Submitting should validate and send back the updated comments list, via AJAX. 33 | 34 | Let's wrap that comments block in a widget. 35 | 36 | ## Generate 37 | 38 | Go and generate a widget stub. 39 | 40 | ```shell 41 | rails generate apotomo:widget Comments display write -e haml 42 | ``` 43 | 44 | ``` 45 | create app/widgets/ 46 | create app/widgets/comments_widget.rb 47 | create app/widgets/comments/ 48 | create app/widgets/comments/display.html.haml 49 | create app/widgets/comments/write.html.haml 50 | create test/widgets/ 51 | create test/widgets/comments_widget_test.rb 52 | ``` 53 | 54 | Nothing special. 55 | 56 | ## Plug it in 57 | 58 | You now tell your controller about the new widget. 59 | 60 | ```ruby 61 | class PostsController < ApplicationController 62 | 63 | has_widgets do |root| 64 | root << widget(:comments, :post => @post) 65 | end 66 | ``` 67 | 68 | This creates a widget instance called `comments_widget` from the class CommentsWidget. We pass the current post into the widget - the block is executed in controller instance context, that's were `@post` comes from. Handy, isn't it? 69 | 70 | ## Render the widget 71 | 72 | Rendering usually happens in your controller view, `app/views/posts/show.html.haml`, for instance. 73 | 74 | ```haml 75 | %h1= @post.title 76 | %p 77 | = @post.body 78 | %p 79 | = render_widget :comments, post: @post 80 | ``` 81 | 82 | ## Write the widget 83 | 84 | A widget is like a cell which is like a mini-controller. 85 | 86 | ```ruby 87 | class CommentsWidget < Apotomo::Widget 88 | responds_to_event :post 89 | 90 | def display(args) 91 | @comments = args[:post].comments # the parameter from outside. 92 | 93 | render 94 | end 95 | ``` 96 | 97 | Having `display` as the default state when rendering, this method collects comments to show and renders its view. 98 | 99 | And look at line 2 - if encountering a `:post` event we invoke `#post`, which is simply another state. How cool is that? 100 | 101 | ```ruby 102 | def post(event) 103 | @comment = Comment.new :post_id => event[:post_id] 104 | @comment.update_attributes event[:comment] # a bit like params[]. 105 | 106 | update :state => :display 107 | end 108 | end 109 | ``` 110 | 111 | The event is processed with three steps in our widget: 112 | 113 | * create the new comment 114 | * re-render the `display` state 115 | * update itself on the page 116 | 117 | Apotomo helps you focusing on your app and takes away the pain of _action dispatching_ and _page updating_. 118 | 119 | ## Triggering events 120 | 121 | So how and where is the `:post` event triggered? 122 | 123 | Take a look at the widget's view `display.html.haml`. 124 | 125 | ```haml 126 | = widget_div do 127 | %ul 128 | - for comment in @comments 129 | %li= comment.text 130 | 131 | = form_for :comment, :url => url_for_event(:post), :remote => true do |f| 132 | = f.text_field :text 133 | = f.submit 134 | ``` 135 | 136 | That's a lot of familiar view code, almost looks like a _partial_. 137 | 138 | As soon as the form is submitted, the form gets serialized and sent using the standard Rails mechanisms. The interesting part here is the endpoint URL returned by #url_for_event as it will trigger an Apotomo event. 139 | 140 | ## Event processing 141 | 142 | Now what happens when the event request is sent? Apotomo - again - does three things for you, it 143 | 144 | * _accepts the request_ on a special event route it adds to your app 145 | * _triggers the event_ in your ruby widget tree, which will invoke the `#post` state in our comment widget 146 | * _sends back_ the page updates your widgets rendered 147 | 148 | ## JavaScript Agnosticism 149 | 150 | In this example, we use jQuery for triggering. We could also use Prototype, RightJS, YUI, or a self-baked framework, that's up to you. 151 | 152 | Also, updating the page is in your hands. Where Apotomo provides handy helpers as `#replace`, you could also _emit your own JavaScript_. 153 | 154 | Look, `replace` basically generates 155 | 156 | ```ruby 157 | jQuery("comments").replaceWith(); 158 | ``` 159 | 160 | If that's not what you want, do 161 | 162 | ```ruby 163 | def post(event) 164 | if event[:comment][:text].explicit? 165 | render :text => 'alert("Hey, you wanted to submit a pervert comment!");' 166 | end 167 | end 168 | ``` 169 | 170 | Apotomo doesn't depend on _any_ JS framework - you choose! 171 | 172 | ## Testing 173 | 174 | Apotomo comes with its own test case and assertions to _build rock-solid web components_. 175 | 176 | ```ruby 177 | class CommentsWidgetTest < Apotomo::TestCase 178 | has_widgets do |root| 179 | root << widget(:comments, :post => @pervert_post) 180 | end 181 | 182 | def test_render 183 | render_widget :comments 184 | assert_select "li#me" 185 | 186 | trigger :post, :comment => {:text => "Sex on the beach"} 187 | assert_response 'alert("Hey, you wanted to submit a pervert comment!");' 188 | end 189 | end 190 | ``` 191 | 192 | You can render your widgets, spec the markup, trigger events and assert the event responses, so far. If you need more, let us know! 193 | 194 | **Using rspec?** please check out [rspec-apotomo]. 195 | 196 | ## Bubbling events 197 | 198 | Note: Let's write this paragraph! 199 | 200 | ## Bugs, Community 201 | 202 | Please visit , the official project page with _lots_ of examples. 203 | 204 | If you have questions, visit us in the IRC channel #cells at irc.freenode.org. 205 | 206 | If you wanna be cool, subscribe to our [feed](http://feeds.feedburner.com/Apotomo)! 207 | 208 | ## License 209 | 210 | Copyright (c) 2007-2013 Nick Sutterer 211 | 212 | Released under the MIT License. 213 | 214 | [rspec-apotomo]: https://github.com/apotonick/rspec-apotomo "apotonick/rspec-apotomo" 215 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rake/testtask' 5 | 6 | desc 'Default: run unit tests.' 7 | task :default => :test 8 | 9 | Rake::TestTask.new(:test) do |test| 10 | test.libs << 'test' 11 | test.test_files = FileList['test/*_test.rb', 'test/rails/*_test.rb'] 12 | test.verbose = true 13 | end 14 | -------------------------------------------------------------------------------- /apotomo.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib/', __FILE__) 3 | $:.unshift lib unless $:.include?(lib) 4 | 5 | require 'apotomo/version' 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "apotomo" 9 | s.version = Apotomo::VERSION 10 | s.platform = Gem::Platform::RUBY 11 | s.authors = ["Nick Sutterer"] 12 | s.email = ["apotonick@gmail.com"] 13 | s.homepage = "http://github.com/apotonick/apotomo" 14 | s.summary = %q{Web components for Rails.} 15 | s.description = %q{Web component framework for Rails providing widgets that trigger events and know when and how to update themselves with AJAX.} 16 | s.license = 'MIT' 17 | 18 | s.files = `git ls-files`.split("\n") 19 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 20 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 21 | s.require_paths = ["lib"] 22 | 23 | s.add_dependency "cells", ">= 3.6.7", "< 4.0.0" 24 | s.add_dependency "onfire", "~> 0.2.0" 25 | s.add_dependency "hooks", "~> 0.4.0" # brings us uber. 26 | 27 | s.add_development_dependency "rake" 28 | s.add_development_dependency "slim" 29 | s.add_development_dependency "haml" 30 | s.add_development_dependency "tzinfo" 31 | s.add_development_dependency "minitest", "~> 4.7.5" 32 | end 33 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | match ":controller/render_event_response", :action => "render_event_response", :as => "apotomo_event", :via => [:get, :post, :put, :patch, :delete] 3 | end 4 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails3-0: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in apotomo.gemspec 4 | gemspec path: '../' 5 | 6 | gem 'railties', '~> 3.0.0' 7 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails3-1: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in apotomo.gemspec 4 | gemspec path: '../' 5 | 6 | gem 'railties', '~> 3.1.0' 7 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails3-2: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in apotomo.gemspec 4 | gemspec path: '../' 5 | 6 | gem 'railties', '~> 3.2.0' 7 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails4-0: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in apotomo.gemspec 4 | gemspec path: '../' 5 | 6 | gem 'railties', '~> 4.0.0' 7 | -------------------------------------------------------------------------------- /lib/apotomo.rb: -------------------------------------------------------------------------------- 1 | module Apotomo 2 | autoload :TestCase, 'apotomo/test_case' 3 | 4 | class << self 5 | def js_framework=(js_framework) 6 | @js_framework = js_framework 7 | @js_generator = JavascriptGenerator.new(js_framework) 8 | end 9 | 10 | attr_reader :js_generator, :js_framework 11 | 12 | # Apotomo setup/configuration helper for initializer. 13 | # 14 | # == Usage/Examples: 15 | # 16 | # Apotomo.setup do |config| 17 | # config.js_framework = :jquery 18 | # end 19 | def setup 20 | yield self 21 | end 22 | end 23 | end 24 | 25 | require 'apotomo/widget' 26 | require 'apotomo/railtie' 27 | require 'apotomo/widget_shortcuts' 28 | require 'apotomo/rails/controller_methods' 29 | require 'apotomo/javascript_generator' 30 | 31 | Apotomo.js_framework = :jquery ### DISCUSS: move to rails.rb 32 | -------------------------------------------------------------------------------- /lib/apotomo/apotomo.rake: -------------------------------------------------------------------------------- 1 | require "rake/testtask" 2 | 3 | namespace "test" do 4 | Rake::TestTask.new(:widgets) do |t| 5 | t.libs << "test" 6 | t.pattern = 'test/widgets/**/*_test.rb' 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/apotomo/debugging.rb: -------------------------------------------------------------------------------- 1 | require "onfire/debugging" 2 | 3 | Apotomo::Event.class_eval do 4 | include Onfire::Event::Debugging 5 | 6 | debug do |widget, event| 7 | puts "#{widget.name}: #{event}" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/apotomo/event.rb: -------------------------------------------------------------------------------- 1 | module Apotomo 2 | # Events are created by Apotomo in #fire. They bubble up from their source to root and trigger 3 | # event handlers. 4 | class Event < Onfire::Event 5 | def to_s 6 | "" 7 | end 8 | 9 | delegate :[], :to => :data 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/apotomo/event_handler.rb: -------------------------------------------------------------------------------- 1 | module Apotomo 2 | # EventHandlers are "callbacks", not knowing why they exist, but what to do. 3 | class EventHandler 4 | 5 | def process_event(event) 6 | # do something, and return content. 7 | nil 8 | end 9 | 10 | def ==(other) 11 | self.to_s == other.to_s 12 | end 13 | 14 | # Invoked by Onfire. 15 | def call(event) 16 | event.source.root.page_updates << process_event(event) 17 | end 18 | 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/apotomo/invoke_event_handler.rb: -------------------------------------------------------------------------------- 1 | require 'apotomo/event_handler' 2 | 3 | module Apotomo 4 | class InvokeEventHandler < EventHandler 5 | attr_accessor :widget_id, :state 6 | 7 | def initialize(options={}) 8 | @widget_id = options[:widget_id] 9 | @state = options[:state] 10 | end 11 | 12 | def process_event(event) 13 | target = event.source.root.find_by_path(widget_id) ### DISCUSS: widget_id or widget_selector? 14 | 15 | target.invoke(state, event) 16 | end 17 | 18 | def to_s; "InvokeEventHandler:#{widget_id}##{state}"; end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/apotomo/javascript_generator.rb: -------------------------------------------------------------------------------- 1 | require 'action_view/helpers/javascript_helper' 2 | 3 | module Apotomo 4 | class JavascriptGenerator 5 | def initialize(framework) 6 | raise "No JS framework specified" if framework.blank? 7 | extend "apotomo/javascript_generator/#{framework}".camelize.constantize 8 | end 9 | 10 | def <<(javascript) 11 | "#{javascript}" 12 | end 13 | 14 | JS_ESCAPER = Object.new.extend(::ActionView::Helpers::JavaScriptHelper) 15 | 16 | # Escape carrier returns and single and double quotes for JavaScript segments. 17 | def self.escape(javascript) 18 | JS_ESCAPER.escape_javascript(javascript) 19 | end 20 | 21 | def escape(javascript) 22 | self.class.escape(javascript) 23 | end 24 | 25 | module Prototype 26 | def prototype; end 27 | def element(id); "jQuery(\"#{id}\")"; end 28 | def update(id, markup); element(id) + '.update("'+escape(markup)+'");'; end 29 | def replace(id, markup); element(id) + '.replace("'+escape(markup)+'");'; end 30 | def update_id(id, markup); update(id, markup); end 31 | def replace_id(id, markup); replace(id, markup); end 32 | end 33 | 34 | module Right 35 | def right; end 36 | def element(id); "jQuery(\"#{id}\")"; end 37 | def update(id, markup); element(id) + '.update("'+escape(markup)+'");'; end 38 | def replace(id, markup); element(id) + '.replace("'+escape(markup)+'");'; end 39 | def update_id(id, markup); update(id, markup); end 40 | def replace_id(id, markup); replace(id, markup); end 41 | end 42 | 43 | module Jquery 44 | def jquery; end 45 | def element(id); "jQuery(\"#{id}\")"; end 46 | def update(id, markup); element(id) + '.html("'+escape(markup)+'");'; end 47 | def replace(id, markup); element(id) + '.replaceWith("'+escape(markup)+'");'; end 48 | def update_id(id, markup); update("##{id}", markup); end 49 | def replace_id(id, markup); replace("##{id}", markup); end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/apotomo/rails/controller_methods.rb: -------------------------------------------------------------------------------- 1 | require 'apotomo/request_processor' 2 | require 'uber/inheritable_attr' 3 | 4 | module Apotomo 5 | module Rails 6 | # Lazy-loads Apotomo support into controllers when needed. 7 | module ControllerMethodsLoader 8 | def has_widgets(*args, &block) 9 | include ControllerMethods 10 | has_widgets(*args, &block) 11 | end 12 | end 13 | 14 | 15 | module ActionViewMethods 16 | delegate :render_widget, :url_for_event, :to => :controller 17 | end 18 | 19 | module ControllerMethods 20 | include WidgetShortcuts 21 | extend ActiveSupport::Concern 22 | 23 | included do 24 | extend Uber::InheritableAttr 25 | extend WidgetShortcuts 26 | 27 | inheritable_attr :has_widgets_blocks 28 | self.has_widgets_blocks = [] 29 | 30 | helper ActionViewMethods 31 | end 32 | 33 | module ClassMethods 34 | # Yields the root widget to setup your widgets for a controller. The block is executed in 35 | # controller _instance_ context, so you may use instance methods and variables of the 36 | # controller. 37 | # 38 | # Example: 39 | # class PostsController < ApplicationController 40 | # has_widgets do |root| 41 | # root << widget(:comments, :user => current_user) 42 | # end 43 | def has_widgets(&block) 44 | has_widgets_blocks << block 45 | end 46 | end 47 | 48 | def apotomo_request_processor 49 | return @apotomo_request_processor if @apotomo_request_processor 50 | 51 | # happens once per request: 52 | options = {:js_framework => Apotomo.js_framework} 53 | 54 | @apotomo_request_processor = Apotomo::RequestProcessor.new(self, options, self.class.has_widgets_blocks) 55 | end 56 | 57 | def apotomo_root 58 | apotomo_request_processor.root 59 | end 60 | 61 | def render_widget(*args, &block) 62 | apotomo_request_processor.render_widget_for(*args, &block) 63 | end 64 | 65 | def render_event_response 66 | page_updates = apotomo_request_processor.process_for(params) 67 | 68 | return render_iframe_updates(page_updates) if params[:apotomo_iframe] 69 | 70 | render :text => page_updates.join("\n"), :content_type => Mime::JS 71 | end 72 | 73 | # Returns the url to trigger a +type+ event from +:source+, which is a non-optional parameter. 74 | # Additional +options+ will be appended to the query string. 75 | # 76 | # Note that this method will use the framework's internal routing if available (e.g. #url_for in Rails). 77 | # 78 | # Example: 79 | # url_for_event(:paginate, :source => 'mouse', :page => 2) 80 | # #=> http://apotomo.de/mouse/process_event_request?type=paginate&source=mouse&page=2 81 | def url_for_event(type, options) 82 | options.reverse_merge!(:type => type) 83 | 84 | apotomo_event_path(apotomo_request_processor.address_for(options)) 85 | end 86 | 87 | protected 88 | # Renders the page updates through an iframe. Copied from responds_to_parent, 89 | # see http://github.com/markcatley/responds_to_parent . 90 | def render_iframe_updates(page_updates) 91 | escaped_script = Apotomo::JavascriptGenerator.escape(page_updates.join("\n")) 92 | 93 | render :text => "", :content_type => 'text/html' 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/apotomo/rails/view_helper.rb: -------------------------------------------------------------------------------- 1 | module Apotomo 2 | module Rails 3 | # == #url_for_event 4 | # 5 | # = url_for_event(:paginate, :page => 2) 6 | # #=> http://apotomo.de/mouse/process_event_request?type=paginate&source=mouse&page=2 7 | # 8 | # == #widget_id 9 | # 10 | # = widget_id 11 | # #=> :mouse 12 | # 13 | # == #children 14 | # 15 | # - children.each do |kid| 16 | # = render_widget kid 17 | module ViewHelper 18 | delegate :children, :url_for_event, :widget_id, :to => :controller 19 | 20 | # Returns the app JavaScript generator. 21 | def js_generator 22 | Apotomo.js_generator 23 | end 24 | 25 | # Creates a form that submits itself via an iFrame and executes the response 26 | # in the parent window. This is needed to upload files via AJAX. 27 | # 28 | # Better call #form_to_event :multipart => true and stay forward-compatible. 29 | def multipart_form_to_event(type, options={}, html_options={}, &block) 30 | options.reverse_merge! :apotomo_iframe => true 31 | html_options.reverse_merge! :target => :apotomo_iframe, :multipart => true 32 | 33 | # i hate rails: 34 | concat(''.html_safe) << form_tag(url_for_event(type, options), html_options, &block) 35 | end 36 | 37 | # Wraps your widget content in a +div+. See #widget_tag. 38 | def widget_div(*args, &block) 39 | widget_tag(:div, *args, &block) 40 | end 41 | 42 | # Wraps your widget content in a +tag+ tag and sets the id. Feel free to pass additional html options. 43 | # 44 | # - widget_tag :span do 45 | # %p I'm wrapped 46 | # 47 | # will render 48 | # 49 | # 50 | #

I'm wrapped

51 | #
52 | # 53 | # Note that you can set the +id+ and other options manually. 54 | # 55 | # - widget_tag :div, id: "comments", class: "yellow" 56 | def widget_tag(tag, options={}, &block) 57 | options.reverse_merge!(:id => widget_id) 58 | 59 | content_tag(tag, options, &block) 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/apotomo/railtie.rb: -------------------------------------------------------------------------------- 1 | require "rails/railtie" 2 | 3 | module Apotomo 4 | class Railtie < ::Rails::Railtie 5 | rake_tasks do 6 | load "apotomo/apotomo.rake" 7 | end 8 | 9 | # As we are a Railtie only, the routes won't be loaded automatically. Beside that, we want our 10 | # route to be the very first (otherwise #resources might supersede it). 11 | initializer 'apotomo.prepend_routes', :after => :add_routing_paths do |app| 12 | app.routes_reloader.paths.unshift(File.dirname(__FILE__) + "/../../config/routes.rb") 13 | end 14 | 15 | # Include a lazy loader via has_widgets. 16 | initializer 'apotomo.add_has_widgets' do |app| 17 | ActionController::Base.extend Apotomo::Rails::ControllerMethodsLoader 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/apotomo/request_processor.rb: -------------------------------------------------------------------------------- 1 | module Apotomo 2 | class RequestProcessor 3 | 4 | class InvalidSourceWidget < RuntimeError; end 5 | 6 | include Hooks 7 | 8 | define_hook :after_initialize 9 | define_hook :after_fire 10 | 11 | attr_reader :root 12 | 13 | 14 | def initialize(controller, options={}, has_widgets_blocks=[]) 15 | @root = Widget.new(controller, :root) 16 | 17 | attach_stateless_blocks_for(has_widgets_blocks, @root, controller) 18 | 19 | run_hook :after_initialize, self 20 | end 21 | 22 | def attach_stateless_blocks_for(blocks, root, controller) 23 | blocks.each { |blk| controller.instance_exec(root, &blk) } 24 | end 25 | 26 | # Called when the browser wants an url_for_event address. This fires the request event in 27 | # the widget tree and collects the rendered page updates. 28 | def process_for(request_params) 29 | source = self.root.find_widget(request_params[:source]) or raise InvalidSourceWidget, "Source #{request_params[:source].inspect} non-existent." 30 | 31 | source.fire(request_params[:type].to_sym, request_params) # set data to params for now. 32 | 33 | run_hook :after_fire, self 34 | source.root.page_updates ### DISCUSS: that's another dependency. 35 | end 36 | 37 | # Renders the widget named +widget_id+. Any additional args is passed through to Widget#invoke. 38 | def render_widget_for(*args) 39 | root.render_widget(*args) 40 | end 41 | 42 | # Computes the address hash for a +:source+ widget and an event +:type+. 43 | # Additional parameters will be merged. 44 | def address_for(options) # DISCUSS: remove/make private/rename? 45 | raise "You forgot to provide :source or :type" unless options.has_key?(:source) and options.has_key?(:type) 46 | options 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/apotomo/test_case.rb: -------------------------------------------------------------------------------- 1 | require 'cell/test_case' 2 | 3 | module Apotomo 4 | # Testing is fun. Test your widgets! 5 | # 6 | # This class helps you testing widgets where it can. It is similar as in a controller. 7 | # A declarative test would look like 8 | # 9 | # class BlogWidgetTest < Apotomo::TestCase 10 | # has_widgets do |root| 11 | # root << widget(:comments_widget, 'post-comments') 12 | # end 13 | # 14 | # it "should be rendered nicely" do 15 | # render_widget 'post-comments' 16 | # 17 | # assert_select "div#post-comments", "Comments for this post" 18 | # end 19 | # 20 | # it "should redraw on :update" do 21 | # trigger :update 22 | # assert_response "jQuery(\"post-comments\").update ..." 23 | # end 24 | # 25 | # For unit testing, you can grab an instance of your tested widget. 26 | # 27 | # it "should be visible" do 28 | # assert root['post-comments'].visible? 29 | # end 30 | # 31 | # See also in Cell::TestCase. 32 | class TestCase < Cell::TestCase # TODO: re-arrange modules in Cell::TestCase and include instead of inheritance. 33 | # Generic test methods to be used in Test::Unit, RSpec, etc. 34 | module TestMethods 35 | extend ActiveSupport::Concern 36 | 37 | include Cell::TestCase::CommonTestMethods 38 | 39 | attr_reader :view_assigns 40 | 41 | def setup 42 | super # defined in Cell::TestCase::CommonTestMethods. 43 | 44 | @controller.instance_eval do 45 | def controller_path 46 | 'barn' 47 | end 48 | end 49 | @controller.extend Apotomo::Rails::ControllerMethods 50 | end 51 | 52 | # Renders the widget +name+. 53 | def render_widget(*args) 54 | @view_assigns = extract_state_ivars_for(root[args.first]) do 55 | @last_invoke = root.render_widget(*args) 56 | end 57 | cleanup_assigns!(@view_assigns) 58 | 59 | @last_invoke 60 | end 61 | 62 | # Triggers an event of +type+. You have to pass the +source+ as second options. 63 | # 64 | # Example: 65 | # 66 | # trigger :submit, :comments 67 | def trigger(type, source, options={}) 68 | source = root.find_widget(source) 69 | source.fire(type, options) 70 | root.page_updates 71 | end 72 | 73 | # Returns the widget tree from TestCase.has_widgets. 74 | def root 75 | blk = self.class.has_widgets_blocks or raise "Please setup a widget tree using has_widgets()" 76 | @root ||= Apotomo::Widget.new(parent_controller, "root").tap do |root| 77 | self.instance_exec(root, &blk) 78 | end 79 | end 80 | 81 | def parent_controller 82 | @controller 83 | end 84 | 85 | private 86 | def cleanup_assigns!(assigns) 87 | assigns.delete(:lookup_context) # dirty but it works. 88 | end 89 | 90 | module ClassMethods 91 | def has_widgets_blocks 92 | @has_widgets 93 | end 94 | 95 | # Setup a widget tree as you're used to it from your controller. Executed in test context. 96 | def has_widgets(&block) 97 | @has_widgets = block 98 | end 99 | end 100 | end 101 | 102 | # After a #trigger this assertion compares the actually triggered page updates with the passed. 103 | # 104 | # Example: 105 | # 106 | # trigger :submit, :source => "post-comments" 107 | # assert_response "alert(\":submit clicked!\")", /\jQuery\("post-comments"\).update/ 108 | def assert_response(*content) 109 | updates = root.page_updates 110 | 111 | i = 0 112 | content.each do |assertion| 113 | if assertion.kind_of? Regexp 114 | assert_match assertion, updates[i] 115 | else 116 | assert_equal assertion, updates[i] 117 | end 118 | 119 | i+=1 120 | end 121 | end 122 | 123 | include Apotomo::WidgetShortcuts 124 | include TestMethods 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/apotomo/version.rb: -------------------------------------------------------------------------------- 1 | module Apotomo 2 | VERSION = '1.3.2' 3 | end 4 | -------------------------------------------------------------------------------- /lib/apotomo/widget.rb: -------------------------------------------------------------------------------- 1 | require 'cells' 2 | require 'onfire' 3 | require 'hooks' 4 | 5 | 6 | require 'apotomo/event' 7 | require 'apotomo/widget_shortcuts' 8 | require 'apotomo/rails/view_helper' 9 | require 'apotomo/rails/controller_methods' # FIXME. 10 | 11 | require 'apotomo/widget/tree_node' 12 | require 'apotomo/widget/event_methods' 13 | require 'apotomo/widget/javascript_methods' 14 | 15 | 16 | module Apotomo 17 | # == Accessing Parameters 18 | # 19 | # Apotomo tries to prevent you from having to access the global #params hash. We have the following 20 | # concepts to retrieve input data. 21 | # 22 | # 1. Configuration values are available both in render and triggered states. Pass those in #widget 23 | # when creating the widget tree. Use #options for reading. 24 | # 25 | # has_widgets do |root| 26 | # root << widget(:mouse_widget, 'mum', :favorites => ["Gouda", "Chedar"]) 27 | # 28 | # and read in your widget state 29 | # 30 | # def display 31 | # @cheese = options[:favorites].first 32 | # 33 | # 2. Request data from forms etc. is available through event.data in the triggered states. 34 | # Use the #[] shortcut to access values directly. 35 | # 36 | # def update(evt) 37 | # @cheese = Cheese.find evt[:cheese_id] 38 | class Widget < Cell::Rails 39 | self.view_paths = "app/widgets" 40 | 41 | include Hooks 42 | 43 | # Use this for setup code you're calling in every state. Almost like a +before_filter+ except that it's 44 | # invoked after the initialization in #has_widgets. 45 | # 46 | # Example: 47 | # 48 | # class MouseWidget < Apotomo::Widget 49 | # after_initialize do 50 | # @cheese = Cheese.find options[:cheese_id] 51 | # end 52 | define_hook :after_initialize 53 | define_hook :has_widgets 54 | 55 | attr_writer :visible 56 | 57 | include TreeNode 58 | 59 | include Onfire 60 | 61 | include EventMethods 62 | include WidgetShortcuts 63 | include JavascriptMethods 64 | 65 | helper Apotomo::Rails::ViewHelper 66 | helper Apotomo::Rails::ActionViewMethods 67 | 68 | abstract! 69 | undef :display # We don't want #display to be listed in #internal_methods. 70 | 71 | attr_reader :name 72 | alias_method :widget_id, :name 73 | 74 | attr_reader :options 75 | 76 | after_initialize do 77 | run_hook :has_widgets, self 78 | end 79 | 80 | 81 | def initialize(parent, id, options={}) 82 | super(parent) # TODO: do that as long as cells do need a parent_controller. 83 | @options = options 84 | @name = id 85 | @visible = true 86 | 87 | setup_tree_node(parent) 88 | 89 | run_hook :after_initialize, self 90 | end 91 | 92 | def parent_controller 93 | # i hope we'll get rid of any parent_controller dependency, soon. 94 | root? ? @parent_controller : root.parent_controller 95 | end 96 | 97 | def visible? 98 | @visible 99 | end 100 | 101 | # Invokes +state+ and hopefully returns the rendered content. 102 | def invoke(state, *args) 103 | return render_state(state, *args) if method(state).arity != 0 # TODO: remove check and make trigger states receive the evt default. 104 | render_state(state) 105 | end 106 | 107 | # Renders and returns a view for the current state. That's why it is usually called at the end of 108 | # a state method. 109 | # 110 | # ==== Options 111 | # * See http://rdoc.info/gems/cells/Cell/Rails:render 112 | # 113 | # Example: 114 | # class MouseWidget < Apotomo::Widget 115 | # def eat 116 | # render 117 | # end 118 | # 119 | # render the view eat.haml. 120 | # 121 | # render :text => "alert('SQUEAK!');" 122 | # 123 | # issues a squeaking alert dialog on the page. 124 | def render(*args, &block) 125 | super 126 | end 127 | 128 | # Returns the widget named +widget_id+ if it's a descendent or self. 129 | def find_widget(widget_id) 130 | find {|node| node.name.to_s == widget_id.to_s} 131 | end 132 | 133 | def address_for_event(type, options={}) 134 | options.reverse_merge! :source => name, 135 | :type => type, 136 | :controller => parent_controller.controller_path # DISCUSS: dependency to parent_controller. 137 | end 138 | 139 | def url_for_event(type, options={}) 140 | apotomo_event_path address_for_event(type, options) 141 | end 142 | 143 | 144 | def self.controller_path 145 | @controller_path ||= name.sub(/Widget$/, '').underscore unless anonymous? 146 | end 147 | 148 | # Renders the +widget+ (instance or id). 149 | def render_widget(widget_id, state=:display, *args) 150 | if widget_id.kind_of?(Widget) 151 | widget = widget_id 152 | else 153 | widget = find_widget(widget_id) or raise "Couldn't render non-existent widget `#{widget_id}`" 154 | end 155 | 156 | widget.invoke(state, *args) 157 | end 158 | end 159 | end 160 | 161 | -------------------------------------------------------------------------------- /lib/apotomo/widget/event_methods.rb: -------------------------------------------------------------------------------- 1 | require 'apotomo/invoke_event_handler' 2 | 3 | module Apotomo 4 | # Event-related methods and onfire bridge for Widget. 5 | module EventMethods 6 | extend ActiveSupport::Concern 7 | 8 | included do 9 | after_initialize do 10 | self.class.responds_to_event_options.each do |args| 11 | type, options = args[0], args[1] || {} 12 | target = self 13 | 14 | if target_id = options[:passing] 15 | target = root.find_widget(target_id) 16 | options = options.reverse_merge(on: widget_id) 17 | end 18 | 19 | target.respond_to_event(type, options) 20 | end 21 | end 22 | 23 | inheritable_attr :responds_to_event_options 24 | self.responds_to_event_options = [] 25 | end 26 | 27 | 28 | attr_writer :page_updates 29 | 30 | def page_updates 31 | @page_updates ||= [] 32 | end 33 | 34 | 35 | module ClassMethods 36 | # Instructs the widget to look out for +type+ events. If an appropriate event starts from or passes the widget, 37 | # the defined trigger state is executed. 38 | # 39 | # class MouseWidget < Apotomo::Widget 40 | # responds_to_event :squeak 41 | # 42 | # def squeak(evt) 43 | # update 44 | # end 45 | # 46 | # Calls #squeak when a :squeak event is encountered. 47 | # 48 | # == Options 49 | # Any option except the event +type+ is optional. 50 | # 51 | # [:with => state] 52 | # executes +state+, defaults to +type+. 53 | # responds_to_event :squeak, :with => :chirp 54 | # will invoke the +#chirp+ state method. 55 | # [:on => id] 56 | # execute the trigger state on another widget. 57 | # responds_to_event :squeak, :on => :cat 58 | # will invoke the +#squeak+ state on the +:cat+ widget. 59 | # [:from => id] 60 | # executes the state only if the event origins from +id+. 61 | # responds_to_event :squeak, :from => :kid 62 | # will invoke the +#squeak+ state if +:kid+ triggered and if +:kid+ is a decendent of the current widget. 63 | # [:passing => id] 64 | # attaches the observer to another widget. Useful if you want to catch bubbling events in +root+. 65 | # responds_to_event :squeak, :passing => :root 66 | # will invoke the state on the current widget if the event passes +:root+ (which is highly probable). 67 | # 68 | # == Inheritance 69 | # Note that the observers are inherited. This allows deriving a widget class without having to redefine the 70 | # responds_to_event blocks. 71 | def responds_to_event(*options) 72 | responds_to_event_options << options 73 | end 74 | end 75 | 76 | # Same as #responds_to_event but executed on the widget instance, only. 77 | def respond_to_event(type, options={}) 78 | # DISCUSS: do we need the :once option? how could we avoid re-adding? 79 | options = options.reverse_merge(:once => true, 80 | :with => type, 81 | :on => widget_id) 82 | 83 | handler = InvokeEventHandler.new(:widget_id => options[:on], :state => options[:with]) 84 | return if options[:once] and event_table.all_handlers_for(type, options[:from]).include?(handler) 85 | 86 | on(type, :call => handler, :from => options[:from]) 87 | end 88 | 89 | # Fire an event of +type+ and let it bubble up. You may add arbitrary payload data to the event. 90 | # 91 | # Example: 92 | # 93 | # trigger(:dropped, :area => 59) 94 | # 95 | # which can be queried in a triggered state. 96 | # 97 | # def on_drop(event) 98 | # if event[:area] == 59 99 | def trigger(*args) 100 | fire(*args) 101 | end 102 | 103 | # Get all handlers from self for the passed event (overriding Onfire#local_event_handlers). 104 | def handlers_for_event(event) 105 | event_table.all_handlers_for(event.type, event.source.name) # we key with widget_id. 106 | end 107 | 108 | protected 109 | def event_for(*args) # defined in Onfire: we want Apotomo::Event. 110 | Event.new(*args) 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/apotomo/widget/javascript_methods.rb: -------------------------------------------------------------------------------- 1 | module Apotomo 2 | module JavascriptMethods 3 | # Returns the escaped script. 4 | def escape_js(script) 5 | Apotomo.js_generator.escape(script) 6 | end 7 | 8 | # Wraps the rendered content in a replace statement according to your +Apotomo.js_framework+ setting. 9 | # Received the same options as #render plus an optional +selector+ to change the selector. 10 | # 11 | # Example (with Apotomo.js_framework = :jquery): 12 | # 13 | # def hungry 14 | # replace 15 | # 16 | # will render the current state's view and wrap it like 17 | # 18 | # "jQuery(\"#mouse\").replaceWith(\"
hungry!<\\/div>\")" 19 | # 20 | # You may pass a selector and pass options to render here, as well. 21 | # 22 | # replace "#jerry h1", :view => :squeak 23 | # #=> "jQuery(\"#jerry h1\").replaceWith(\"
squeak!<\\/div>\")" 24 | def replace(*args) 25 | wrap_in_javascript_for(:replace, *args) 26 | end 27 | 28 | # Same as #replace except that the content is wrapped in an update statement. 29 | # 30 | # Example for +:jquery+: 31 | # 32 | # update :view => :peek 33 | # #=> "jQuery(\"#mouse\").html(\"looking...")" 34 | def update(*args) 35 | wrap_in_javascript_for(:update, *args) 36 | end 37 | 38 | private 39 | def wrap_in_javascript_for(mode, *args) 40 | selector = args.first.is_a?(String) ? args.shift : false 41 | content = render(*args) 42 | 43 | selector ? 44 | Apotomo.js_generator.send(mode, selector, content) : # replace(:twitter) 45 | Apotomo.js_generator.send("#{mode}_id", name, content) # replace_id(:twitter) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/apotomo/widget/tree_node.rb: -------------------------------------------------------------------------------- 1 | module Apotomo 2 | module TreeNode 3 | include Enumerable 4 | include Apotomo::WidgetShortcuts::DSL 5 | 6 | # DISCUSS: do we need it? we have []! 7 | # DISCUSS: #children receives a block, but #childrenHash doesn't 8 | attr_reader :childrenHash 9 | attr_accessor :parent 10 | 11 | def setup_tree_node(parent) # DISCUSS: make private? 12 | @parent = nil 13 | @childrenHash = {} 14 | @children = [] # TODO: order of widgets in this variable isn't tested anywhere!!! 15 | 16 | # DISCUSS: and what if not a Widget? 17 | parent.add_widget(self) if parent.kind_of? Widget # TODO: as long as cells needs parent_controller. 18 | end 19 | 20 | # Print the string representation of this node. 21 | def to_s 22 | # DISCUSS: why self.widget_id but parent.name ? 23 | "Node ID: #{widget_id} Parent: " + (root? ? "ROOT" : "#{parent.name}") + 24 | " Children: #{children.length}" + " Total Nodes: #{size}" 25 | end 26 | 27 | def add_widget(child) # TODO: rename #add, make private 28 | raise "Child already added" if @childrenHash.has_key?(child.name) 29 | 30 | @childrenHash[child.widget_id] = child 31 | @children << child 32 | child.parent = self 33 | 34 | child 35 | end 36 | 37 | # Removes the specified child node from the receiver node. 38 | # The removed children nodes are orphaned but available 39 | # if an alternate reference exists. 40 | # Returns the child node. 41 | def remove!(child) 42 | @childrenHash.delete(child.name) 43 | @children.delete(child) 44 | # DISCUSS: why `unless child == nil`? if child is nil, an exception has been raised two locs above! 45 | child.root! unless child == nil 46 | child 47 | end 48 | 49 | # Private method which sets this node as a root node. 50 | def root! 51 | @parent = nil 52 | end 53 | 54 | # Indicates whether this node is a root node. Note that 55 | # orphaned children will also be reported as root nodes. 56 | def root? 57 | @parent == nil 58 | end 59 | 60 | # Returns an array of all the immediate children. 61 | # If a block is given, yields each child node to the block. 62 | def children 63 | if block_given? 64 | @children.each { |child| yield child } 65 | else 66 | @children 67 | end 68 | end 69 | 70 | # Returns every node (including the receiver node) from the 71 | # tree to the specified block. 72 | def each(&block) 73 | yield self 74 | children { |child| child.each(&block) } 75 | end 76 | 77 | # Returns the requested node from the set of immediate 78 | # children. 79 | # 80 | # If the key is _numeric_, then the in-sequence array of 81 | # children is accessed (see Tree#children). 82 | # If the key is not _numeric_, then it is assumed to be 83 | # the *name* of the child node to be returned. 84 | def [](name) 85 | if name.kind_of?(Integer) 86 | children[name] 87 | else 88 | childrenHash[name] 89 | end 90 | end 91 | 92 | # Returns the total number of nodes in this tree, rooted 93 | # at the receiver node. 94 | def size 95 | children.inject(1) {|sum, node| sum + node.size} 96 | end 97 | 98 | # Pretty prints the tree starting with the receiver node. 99 | def printTree(tab = 0) 100 | children {|child| child.printTree(tab + 4)} 101 | end 102 | 103 | # Returns the root for this node. 104 | def root 105 | root = self 106 | root = root.parent while !root.root? 107 | root 108 | end 109 | 110 | # Provides a comparision operation for the nodes. Comparision 111 | # is based on the natural character-set ordering for the 112 | # node names. 113 | # DUISCUSS: useful? 114 | # DUISCUSS: <, >, etc., operators doesn't work because of Comparable isn't included 115 | def <=>(other) 116 | return +1 if other == nil 117 | self.name <=> other.name 118 | end 119 | 120 | protected :parent=, :root! 121 | 122 | def find_by_path(selector) 123 | next_node = self 124 | last = nil # prevents self-finding loop. 125 | selector.to_s.split(/ /).each do |node_id| 126 | last = next_node = next_node.find {|n| 127 | n.name.to_s == node_id.to_s and not n==last 128 | } 129 | end 130 | 131 | next_node 132 | end 133 | 134 | 135 | # Returns the path from the widget to root, encoded as 136 | # a string of slash-seperated names. 137 | def path 138 | path = [name] 139 | ancestor = parent 140 | while ancestor 141 | path << ancestor.name 142 | ancestor = ancestor.parent 143 | end 144 | 145 | path.reverse.join("/") 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /lib/apotomo/widget_shortcuts.rb: -------------------------------------------------------------------------------- 1 | module Apotomo 2 | # Create widget trees using the #widget DSL. 3 | module WidgetShortcuts 4 | # Shortcut for creating an instance of class_name+"_widget" named +id+. Yields self. 5 | # Note that this creates a proxy object, only. The actual widget is built not until you added 6 | # it, e.g. using #<<. 7 | # 8 | # Example: 9 | # 10 | # root << widget(:comments) 11 | # 12 | # will create a +CommentsWidget+ with id :comments attached to +root+. 13 | # 14 | # widget(:comments, 'post-comments', :user => current_user) 15 | # 16 | # sets id to 'posts_comments' and #options to the hash. 17 | # 18 | # You can also use namespaces. 19 | # 20 | # widget('jquery/tabs', 'panel') 21 | # 22 | # Add a block if you need to grab the created widget right away. 23 | # 24 | # root << widget(:comments) do |comments| 25 | # comments.markdown! 26 | # end 27 | # 28 | # Using #widget is just a shortcut, you can always use the constructor as well. 29 | # 30 | # CommentsWidget.new(root, :comments) 31 | def widget(*args, &block) 32 | FactoryProxy.new(*args, &block) 33 | end 34 | 35 | class FactoryProxy 36 | def initialize(prefix, *args, &block) 37 | options = args.extract_options! 38 | id = args.shift || prefix 39 | 40 | @prefix, @id, @options, @block = prefix, id, options, block 41 | end 42 | 43 | def build(parent) 44 | widget = constant_for(@prefix).new(parent, @id, @options) 45 | @block.call(widget) if @block 46 | widget 47 | end 48 | 49 | private 50 | def constant_for(class_name) # TODO: use Cell.class_from_cell_name. 51 | "#{class_name}_widget".classify.constantize 52 | end 53 | end 54 | 55 | # Mixed into Widget. 56 | module DSL 57 | def <<(child) 58 | child.build(self) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/generators/apotomo/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Stubs out a new cell widget, its state views and a functional test. 3 | Pass the cell name, either CamelCased or under_scored, and a list 4 | of states as arguments. 5 | 6 | This generates a cell class in app/cells and view templates in 7 | app/cells/cell_name. 8 | 9 | Example: 10 | './script/generate widget Posting new' 11 | 12 | This will create an Apotomo Posting cell: 13 | Cell: app/cells/posting_cell.rb 14 | Views: app/cells/posting/new.html.erb 15 | Test: test/functional/test_posting_cell.rb 16 | -------------------------------------------------------------------------------- /lib/generators/apotomo/widget_generator.rb: -------------------------------------------------------------------------------- 1 | require 'generators/cells/base' 2 | 3 | module Apotomo 4 | module Generators 5 | module BasePathMethods 6 | private 7 | def base_path 8 | File.join('app/widgets', class_path, file_name) 9 | end 10 | end 11 | 12 | class WidgetGenerator < ::Cells::Generators::Base 13 | include BasePathMethods 14 | 15 | source_root File.expand_path('../../templates', __FILE__) 16 | 17 | hook_for(:template_engine) 18 | hook_for(:test_framework) 19 | 20 | check_class_collision :suffix => "Widget" 21 | 22 | 23 | def create_cell_file 24 | template 'widget.rb', "#{base_path}_widget.rb" 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/generators/erb/widget_generator.rb: -------------------------------------------------------------------------------- 1 | require 'generators/erb/cell_generator' 2 | require 'generators/apotomo/widget_generator' 3 | 4 | module Erb 5 | module Generators 6 | class WidgetGenerator < CellGenerator 7 | include ::Apotomo::Generators::BasePathMethods 8 | source_root File.expand_path('../../templates', __FILE__) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/generators/haml/widget_generator.rb: -------------------------------------------------------------------------------- 1 | require 'generators/haml/cell_generator' 2 | require 'generators/apotomo/widget_generator' 3 | 4 | module Haml 5 | module Generators 6 | class WidgetGenerator < CellGenerator 7 | include ::Apotomo::Generators::BasePathMethods 8 | source_root File.expand_path('../../templates', __FILE__) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/generators/slim/widget_generator.rb: -------------------------------------------------------------------------------- 1 | require 'generators/slim/cell_generator' 2 | require 'generators/apotomo/widget_generator' 3 | 4 | module Slim 5 | module Generators 6 | class WidgetGenerator < CellGenerator 7 | include ::Apotomo::Generators::BasePathMethods 8 | source_root File.expand_path('../../templates', __FILE__) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/generators/templates/view.erb: -------------------------------------------------------------------------------- 1 | <%%= widget_div do %> 2 |

3 | <%= class_name %>Widget#<%= @state %> 4 |

5 | 6 |

7 | Find me in <%= @path %> 8 |

9 | <%% end %> 10 | -------------------------------------------------------------------------------- /lib/generators/templates/view.haml: -------------------------------------------------------------------------------- 1 | = widget_div do 2 | %h1 3 | <%= class_name %>Widget#<%= @state %> 4 | %p 5 | Find me in <%= @path %> 6 | -------------------------------------------------------------------------------- /lib/generators/templates/view.slim: -------------------------------------------------------------------------------- 1 | = widget_div do 2 | h1 <%= class_name %>Widget#<%= @state %> 3 | p Find me in <%= @path %> 4 | -------------------------------------------------------------------------------- /lib/generators/templates/widget.rb: -------------------------------------------------------------------------------- 1 | class <%= class_name %>Widget < Apotomo::Widget 2 | 3 | <% for action in actions -%> 4 | def <%= action %> 5 | render 6 | end 7 | 8 | <% end -%> 9 | end 10 | -------------------------------------------------------------------------------- /lib/generators/templates/widget_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class <%= class_name %>WidgetTest < Apotomo::TestCase 4 | has_widgets do |root| 5 | root << widget(:<%= file_name %>) 6 | end 7 | 8 | test "display" do 9 | render_widget :<%= file_name %> 10 | assert_select "h1" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/generators/test_unit/widget_generator.rb: -------------------------------------------------------------------------------- 1 | require 'generators/cells/base' 2 | 3 | module TestUnit 4 | module Generators 5 | class WidgetGenerator < ::Cells::Generators::Base 6 | source_root File.expand_path('../../templates', __FILE__) 7 | 8 | def create_test 9 | @states = actions 10 | template 'widget_test.rb', File.join('test/widgets/', class_path, "#{file_name}_widget_test.rb") 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/apotomo_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ApotomoTest < MiniTest::Spec 4 | describe "The main module ::Apotomo" do 5 | describe "when setting #js_framework" do 6 | before do 7 | Apotomo.js_framework = :jquery 8 | end 9 | 10 | it "respond to #js_framework and return javascript framework's name" do 11 | assert_equal :jquery, Apotomo.js_framework 12 | end 13 | 14 | it "respond to #js_generator and return an correct instance" do 15 | assert_kind_of Apotomo::JavascriptGenerator, Apotomo.js_generator 16 | assert_kind_of Apotomo::JavascriptGenerator::Jquery, Apotomo.js_generator 17 | end 18 | end 19 | 20 | it "respond to #setup" do 21 | Apotomo.setup do |config| 22 | config.js_framework = :jquery 23 | end 24 | 25 | # TODO: Apotomo expect #js_framework 26 | assert_respond_to Apotomo.js_generator, :jquery 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | require 'rake' 6 | 7 | Dummy::Application.load_tasks 8 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag :all %> 6 | <%= javascript_include_tag :defaults %> 7 | <%= csrf_meta_tag %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Dummy::Application 5 | -------------------------------------------------------------------------------- /test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | #require "active_model/railtie" 4 | #require "active_record/railtie" 5 | require "action_controller/railtie" 6 | require "action_view/railtie" 7 | #require "action_mailer/railtie" 8 | 9 | Bundler.require 10 | #require "dummy" 11 | 12 | module Dummy 13 | class Application < Rails::Application 14 | # Settings in config/environments/* take precedence over those specified here. 15 | # Application configuration should go into files in config/initializers 16 | # -- all .rb files in that directory are automatically loaded. 17 | 18 | # Custom directories with classes and modules you want to be autoloadable. 19 | # config.autoload_paths += %W(#{config.root}/extras) 20 | 21 | # Only load the plugins named here, in the order given (default is alphabetical). 22 | # :all can be used as a placeholder for all plugins not explicitly named. 23 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 24 | 25 | # Activate observers that should always be running. 26 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 27 | 28 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 29 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 30 | # config.time_zone = 'Central Time (US & Canada)' 31 | 32 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 33 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 34 | # config.i18n.default_locale = :de 35 | 36 | # JavaScript files you want as :defaults (application.js is always included). 37 | # config.action_view.javascript_expansions[:defaults] = %w(jquery rails) 38 | 39 | # Configure the default encoding used in templates for Ruby 1.9. 40 | config.encoding = "utf-8" 41 | 42 | # Configure sensitive parameters which will be filtered from the log file. 43 | config.filter_parameters += [:password] 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | gemfile = File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | if File.exist?(gemfile) 5 | ENV['BUNDLE_GEMFILE'] = gemfile 6 | require 'bundler' 7 | Bundler.setup 8 | end 9 | 10 | $:.unshift File.expand_path('../../../../lib', __FILE__) -------------------------------------------------------------------------------- /test/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3-ruby (not necessary on OS X Leopard) 3 | development: 4 | adapter: sqlite3 5 | database: db/development.sqlite3 6 | pool: 5 7 | timeout: 5000 8 | 9 | # Warning: The database defined as "test" will be erased and 10 | # re-generated from your development database when you run "rake". 11 | # Do not set this db to the same as development or production. 12 | test: 13 | adapter: sqlite3 14 | database: db/test.sqlite3 15 | pool: 5 16 | timeout: 5000 17 | 18 | production: 19 | adapter: sqlite3 20 | database: db/production.sqlite3 21 | pool: 5 22 | timeout: 5000 23 | -------------------------------------------------------------------------------- /test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Dummy::Application.initialize! 6 | -------------------------------------------------------------------------------- /test/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/environment.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the webserver when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_view.debug_rjs = true 15 | config.action_controller.perform_caching = false 16 | 17 | # Don't care if the mailer can't send 18 | config.action_mailer.raise_delivery_errors = false 19 | 20 | # Print deprecation notices to the Rails logger 21 | config.active_support.deprecation = :log 22 | 23 | # Only use best-standards-support built into browsers 24 | config.action_dispatch.best_standards_support = :builtin 25 | end 26 | 27 | -------------------------------------------------------------------------------- /test/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/environment.rb 3 | 4 | # The production environment is meant for finished, "live" apps. 5 | # Code is not reloaded between requests 6 | config.cache_classes = true 7 | 8 | # Full error reports are disabled and caching is turned on 9 | config.consider_all_requests_local = false 10 | config.action_controller.perform_caching = true 11 | 12 | # Specifies the header that your server uses for sending files 13 | config.action_dispatch.x_sendfile_header = "X-Sendfile" 14 | 15 | # For nginx: 16 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' 17 | 18 | # If you have no front-end server that supports something like X-Sendfile, 19 | # just comment this out and Rails will serve the files 20 | 21 | # See everything in the log (default is :info) 22 | # config.log_level = :debug 23 | 24 | # Use a different logger for distributed setups 25 | # config.logger = SyslogLogger.new 26 | 27 | # Use a different cache store in production 28 | # config.cache_store = :mem_cache_store 29 | 30 | # Disable Rails's static asset server 31 | # In production, Apache or nginx will already do this 32 | config.serve_static_assets = false 33 | 34 | # Enable serving of images, stylesheets, and javascripts from an asset server 35 | # config.action_controller.asset_host = "http://assets.example.com" 36 | 37 | # Disable delivery errors, bad email addresses will be ignored 38 | # config.action_mailer.raise_delivery_errors = false 39 | 40 | # Enable threaded mode 41 | # config.threadsafe! 42 | 43 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 44 | # the I18n.default_locale when a translation can not be found) 45 | config.i18n.fallbacks = true 46 | 47 | # Send deprecation notices to registered listeners 48 | config.active_support.deprecation = :notify 49 | end 50 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/environment.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Log error messages when you accidentally call methods on nil. 11 | config.whiny_nils = true 12 | 13 | # Show full error reports and disable caching 14 | config.consider_all_requests_local = true 15 | config.action_controller.perform_caching = false 16 | 17 | # Raise exceptions instead of rendering exception templates 18 | config.action_dispatch.show_exceptions = false 19 | 20 | # Disable request forgery protection in test environment 21 | config.action_controller.allow_forgery_protection = false 22 | 23 | # Tell Action Mailer not to deliver emails to the real world. 24 | # The :test delivery method accumulates sent emails in the 25 | # ActionMailer::Base.deliveries array. 26 | #config.action_mailer.delivery_method = :test 27 | 28 | # Use SQL instead of Active Record's schema dumper when creating the test database. 29 | # This is necessary if your schema can't be completely dumped by the schema dumper, 30 | # like if you have constraints or database-specific column types 31 | # config.active_record.schema_format = :sql 32 | 33 | # Print deprecation notices to the stderr 34 | config.active_support.deprecation = :stderr 35 | end 36 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | Dummy::Application.config.secret_token = '50777595d4b6041cfca51636cf4e262508f96cc081757729e7123d04a5abdf9e3554e4f519b71106e41c5146a9b07b5affd2e410af1130db3a7f4538b70e2429' 8 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Dummy::Application.config.session_store :cookie_store, :key => '_dummy_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rake db:sessions:create") 8 | # Dummy::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /test/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.routes.draw do 2 | get "barn/widget", :to => "barn#widget" 3 | get ':controller(/:action(/:id(.:format)))' 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/db/test.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apotonick/apotomo/b90bde13c22d52d5af6773d36492c8b402684e77/test/dummy/db/test.sqlite3 -------------------------------------------------------------------------------- /test/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /test/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /test/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |

We've been notified about this issue and we'll take a look at it shortly.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /test/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apotonick/apotomo/b90bde13c22d52d5af6773d36492c8b402684e77/test/dummy/public/favicon.ico -------------------------------------------------------------------------------- /test/dummy/public/stylesheets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apotonick/apotomo/b90bde13c22d52d5af6773d36492c8b402684e77/test/dummy/public/stylesheets/.gitkeep -------------------------------------------------------------------------------- /test/event_handler_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class EventHandlerTest < MiniTest::Spec 4 | include Apotomo::TestCaseMethods::TestController 5 | 6 | describe "EventHandler" do 7 | before do 8 | @mum = mouse 9 | @mum << mouse_mock(:kid) 10 | end 11 | 12 | it "respond to #process_event" do 13 | h = Apotomo::EventHandler.new 14 | e = Apotomo::Event.new(:squeak, @mum) 15 | assert_equal nil, h.process_event(e) 16 | end 17 | 18 | it "respond to #call and push #process_events' results ordered to root's #page_updates" do 19 | [@mum, @mum[:kid], @mum].each_with_index do |source, i| 20 | e = Apotomo::Event.new(:squeak, source) 21 | h = Apotomo::EventHandler.new 22 | h.stub :process_event, "tick#{i}" do 23 | h.call(e) 24 | end 25 | end 26 | 27 | assert_equal 3, @mum.page_updates.size 28 | assert_equal "tick0", @mum.page_updates[0] 29 | assert_equal "tick1", @mum.page_updates[1] 30 | assert_equal "tick2", @mum.page_updates[2] 31 | assert_equal 0, @mum[:kid].page_updates.size 32 | end 33 | 34 | #TODO: handler expect #process_event 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /test/event_methods_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class EventMethodsTest < MiniTest::Spec 4 | include Apotomo::TestCaseMethods::TestController 5 | 6 | def handler(id, state) 7 | Apotomo::InvokeEventHandler.new(:widget_id => id, :state => state) 8 | end 9 | 10 | describe "#respond_to_event and #fire" do 11 | before do 12 | mum_and_kid! 13 | end 14 | 15 | it "alert @mum first, then make her squeak when @kid squeaks" do 16 | @kid.fire :squeak 17 | 18 | assert_equal ['be alerted', 'answer squeak'], @mum.list 19 | end 20 | 21 | it "make @mum just squeak back when jerry squeaks" do 22 | @mum << mouse_mock(:jerry) 23 | @mum[:jerry].fire :squeak 24 | 25 | assert_equal ['answer squeak'], @mum.list 26 | end 27 | 28 | it "make @mum run away while @kid keeps watching" do 29 | @kid.fire :footsteps 30 | 31 | assert_equal ['peek', 'escape'], @mum.list 32 | end 33 | 34 | it "by default add a handler only once" do 35 | @mum.respond_to_event :peep, :with => :answer_squeak 36 | @mum.respond_to_event :peep, :with => :answer_squeak 37 | @mum.fire :peep 38 | 39 | assert_equal ['answer squeak'], @mum.list 40 | end 41 | 42 | it "squeak back twice when using the :once => false option" do 43 | @mum.respond_to_event :peep, :with => :answer_squeak 44 | @mum.respond_to_event :peep, :with => :answer_squeak, :once => false 45 | @mum.fire :peep 46 | 47 | assert_equal ['answer squeak', 'answer squeak'], @mum.list 48 | end 49 | 50 | it "also accept an event argument only" do 51 | @mum.respond_to_event :answer_squeak 52 | @mum.fire :answer_squeak 53 | 54 | assert_equal ['answer squeak'], @mum.list 55 | end 56 | 57 | it "make pass the event into the triggered state" do 58 | @mum.instance_eval do 59 | respond_to_event :footsteps 60 | 61 | def footsteps(evt) 62 | list << evt 63 | end 64 | end 65 | 66 | @mum.trigger :footsteps, "near" 67 | 68 | assert_kind_of Apotomo::Event, @mum.list.last 69 | end 70 | 71 | it "accept payload data for the event" do 72 | @mum.respond_to_event :answer_squeak 73 | @mum.instance_eval do 74 | def answer_squeak(evt) 75 | list << evt.data 76 | end 77 | end 78 | 79 | @mum.fire :answer_squeak, :volume => 9 80 | 81 | assert_equal [{:volume => 9}], @mum.list 82 | end 83 | 84 | 85 | describe "#responds_to_event with :passing" do 86 | before do 87 | class AdolescentMouse < MouseWidget 88 | responds_to_event :squeak, :passing => :root 89 | end 90 | 91 | @root = mouse(:root) 92 | end 93 | 94 | it "add handlers to root when called with :passing" do 95 | AdolescentMouse.new(@root, 'jerry') 96 | 97 | assert_equal [handler('jerry', :squeak)], @root.event_table.all_handlers_for(:squeak, 'jerry') 98 | end 99 | 100 | it "inherit :passing handlers" do 101 | Class.new(AdolescentMouse).new(@root, 'jerry') 102 | 103 | assert_equal [handler('jerry', :squeak)], @root.event_table.all_handlers_for(:squeak, 'jerry') 104 | end 105 | 106 | it "preserves options when called with :passing" do 107 | AdolescentMouse.new(@root, 'jerry') 108 | 109 | AdolescentMouse.responds_to_event_options.each do |event, options| 110 | if event == :squeak 111 | assert_equal({ :passing => :root }, options) 112 | end 113 | end 114 | end 115 | end 116 | 117 | describe "#responds_to_event in class context" do 118 | class AdultMouse < Apotomo::Widget 119 | responds_to_event :peep, :with => :answer_squeak 120 | end 121 | 122 | class BabyMouse < AdultMouse 123 | responds_to_event :peep 124 | responds_to_event :footsteps, :with => :squeak 125 | end 126 | 127 | before do 128 | @mum = AdultMouse.new(parent_controller, 'mum') 129 | end 130 | 131 | it "add the handlers at creation time" do 132 | assert_equal [handler('mum', :answer_squeak)], @mum.event_table.all_handlers_for(:peep, 'mum') 133 | end 134 | 135 | it "inherit handlers" do 136 | assert_equal [[:peep, {:with=>:answer_squeak}]], AdultMouse.responds_to_event_options 137 | assert_equal [[:peep, {:with=>:answer_squeak}], [:peep], [:footsteps, {:with=>:squeak}]], BabyMouse.responds_to_event_options 138 | end 139 | 140 | it "not share responds_to_event options between different instances" do 141 | assert_equal [handler('mum', :answer_squeak)], @mum.event_table.all_handlers_for(:peep, 'mum') 142 | assert_equal [handler('dad', :answer_squeak)], AdultMouse.new(parent_controller, 'dad', :show).event_table.all_handlers_for(:peep, 'dad') 143 | end 144 | end 145 | 146 | describe "#trigger" do 147 | it "be an alias for #fire" do 148 | @kid.trigger :footsteps 149 | 150 | assert_equal ['peek', 'escape'], @mum.list 151 | end 152 | end 153 | 154 | describe "page_updates" do 155 | it "expose a simple Array for now" do 156 | assert_kind_of Array, @mum.page_updates 157 | assert_equal 0, @mum.page_updates.size 158 | end 159 | 160 | it "be queued in root#page_updates after #fire" do 161 | @mum.fire :footsteps 162 | 163 | assert_equal ["escape"], @mum.page_updates 164 | end 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /test/event_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class EventTest < MiniTest::Spec 4 | include Apotomo::TestCaseMethods::TestController 5 | 6 | describe "Event" do 7 | it "is a kind of Onfire::Event" do 8 | @event = Apotomo::Event.new(:footsteps, 'mum') 9 | 10 | assert_kind_of Onfire::Event, @event 11 | end 12 | 13 | it "respond to #type and #source" do 14 | @event = Apotomo::Event.new(:footsteps, 'mum') 15 | 16 | assert_equal :footsteps, @event.type 17 | assert_equal 'mum', @event.source 18 | end 19 | 20 | it "accept an additional data object and respond to #data" do 21 | @event = Apotomo::Event.new(:footsteps, 'mum', {:volume => :loud}) 22 | 23 | assert_equal({:volume => :loud}, @event.data) 24 | end 25 | 26 | it "delegate #[] to data" do 27 | @event = Apotomo::Event.new(:footsteps, 'mum', {:volume => :loud}) 28 | 29 | assert_equal :loud, @event[:volume] 30 | end 31 | 32 | it "respond to #to_s" do 33 | @event = Apotomo::Event.new(:footsteps, mouse('mum')) 34 | 35 | assert_equal "", @event.to_s 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/invoke_event_handler_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class EventHandlerTest < MiniTest::Spec 4 | include Apotomo::TestCaseMethods::TestController 5 | 6 | describe "InvokeEventHandler" do 7 | describe "constructor" do 8 | it "accept no arguments and create clean instance" do 9 | h = Apotomo::InvokeEventHandler.new 10 | 11 | assert_nil h.widget_id 12 | assert_nil h.state 13 | end 14 | 15 | it "accept options and set them" do 16 | h = Apotomo::InvokeEventHandler.new(:widget_id => :widget, :state => :state) 17 | 18 | assert_equal :widget, h.widget_id 19 | assert_equal :state, h.state 20 | end 21 | end 22 | 23 | describe "equality methods" do 24 | it "repond to #==" do 25 | h1 = Apotomo::InvokeEventHandler.new(:widget_id => :widget, :state => :state) 26 | h2 = Apotomo::InvokeEventHandler.new(:widget_id => :widget, :state => :state) 27 | 28 | assert h1 == h2 29 | assert h2 == h1 30 | end 31 | 32 | it "repond to #!=" do 33 | h1 = Apotomo::InvokeEventHandler.new(:widget_id => :widget, :state => :state) 34 | 35 | h3 = Apotomo::InvokeEventHandler.new(:widget_id => :another_widget, :state => :state) 36 | assert h1 != h3 37 | assert h3 != h1 38 | 39 | h4 = Apotomo::InvokeEventHandler.new(:widget_id => :widget, :state => :another_state) 40 | assert h1 != h4 41 | assert h4 != h1 42 | 43 | h5 = Apotomo::InvokeEventHandler.new 44 | assert h1 != h5 45 | assert h5 != h1 46 | 47 | # TODO: test InvokeEventHandler == EventHandler 48 | end 49 | end 50 | 51 | it "respond to #to_s" do 52 | h = Apotomo::InvokeEventHandler.new 53 | h.widget_id = :widget_id 54 | h.state = :my_state 55 | 56 | assert_equal "InvokeEventHandler:widget_id#my_state", h.to_s 57 | end 58 | end 59 | 60 | ### TODO: test #process_event 61 | 62 | end 63 | -------------------------------------------------------------------------------- /test/javascript_generator_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class JavascriptGeneratorTest < MiniTest::Spec 4 | describe "The JavascriptGenerator" do 5 | describe "constructor" do 6 | it "accept framework name and return an correct instance" do 7 | @gen = Apotomo::JavascriptGenerator.new(:Jquery) 8 | 9 | assert_kind_of Apotomo::JavascriptGenerator, @gen 10 | assert_kind_of Apotomo::JavascriptGenerator::Jquery, @gen 11 | end 12 | 13 | it "raise an error if no framework passed" do 14 | assert_raises RuntimeError do 15 | Apotomo::JavascriptGenerator.new(nil) 16 | end 17 | end 18 | end 19 | 20 | it "respond to ::escape" do 21 | assert_equal '', Apotomo::JavascriptGenerator.escape(nil) 22 | assert_equal %(This \\"thing\\" is really\\n netos\\'), Apotomo::JavascriptGenerator.escape(%(This "thing" is really\n netos')) 23 | assert_equal %(backslash\\\\test), Apotomo::JavascriptGenerator.escape(%(backslash\\test)) 24 | assert_equal %(dont <\\/close> tags), Apotomo::JavascriptGenerator.escape(%(dont tags)) 25 | end 26 | 27 | describe "in jQuery mode" do 28 | before do 29 | @gen = Apotomo::JavascriptGenerator.new(:Jquery) 30 | end 31 | 32 | it "respond to #escape" do 33 | assert_equal '', @gen.escape(nil) 34 | assert_equal %(This \\"thing\\" is really\\n netos\\'), @gen.escape(%(This "thing" is really\n netos')) 35 | assert_equal %(backslash\\\\test), @gen.escape(%(backslash\\test)) 36 | assert_equal %(dont <\\/close> tags), @gen.escape(%(dont tags)) 37 | end 38 | 39 | it "respond to #<< and return argument converted to String" do 40 | assert_equal "bla_bla", (@gen << "bla_bla") 41 | assert_equal "bla_bla", (@gen << :bla_bla) 42 | end 43 | 44 | it "respond to #jquery" do 45 | assert_respond_to @gen, :jquery 46 | end 47 | 48 | it "respond to #element" do 49 | assert_equal "jQuery(\"#drinks\")", @gen.element("#drinks") 50 | end 51 | 52 | it "respond to #replace" do 53 | assert_equal "jQuery(\"#drinks\").replaceWith(\"EMPTY!\");", @gen.replace("#drinks", 'EMPTY!') 54 | end 55 | 56 | it "respond to #replace_id" do 57 | assert_equal "jQuery(\"#drinks\").replaceWith(\"EMPTY!\");", @gen.replace_id("drinks", 'EMPTY!') 58 | end 59 | 60 | it "respond to #update" do 61 | assert_equal "jQuery(\"#drinks\").html(\"
  • <\\/li>\");", @gen.update("#drinks", '
  • ') 62 | end 63 | 64 | it "respond to #update_id" do 65 | assert_equal "jQuery(\"#drinks\").html(\"EMPTY!\");", @gen.update_id("drinks", 'EMPTY!') 66 | end 67 | end 68 | 69 | # TODO: Prototype mode 70 | 71 | # TODO: Right mode 72 | 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/rails/caching_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | # TODO: assert that same-named cells and widgets don't overwrite their caches. 4 | 5 | class CachingTest < MiniTest::Spec 6 | include Apotomo::TestCaseMethods::TestController 7 | 8 | class CheeseWidget < Apotomo::Widget 9 | cache :holes 10 | 11 | def holes(count) 12 | render :text => count 13 | end 14 | end 15 | 16 | describe "A caching widget" do 17 | before do 18 | ActionController::Base.perform_caching = true 19 | @cheese = CheeseWidget.new(parent_controller, 'cheese', :holes) 20 | end 21 | 22 | after do 23 | ActionController::Base.perform_caching = false 24 | end 25 | 26 | it "invoke the cached state only once" do 27 | assert_equal "1", @cheese.invoke(:holes, 1) 28 | assert_equal "1", @cheese.invoke(:holes, 2) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/rails/controller_methods_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ControllerMethodsTest < MiniTest::Spec 4 | include Apotomo::TestCaseMethods::TestController 5 | 6 | describe "A Rails controller" do 7 | describe "responding to #apotomo_root" do 8 | it "initially return a root widget" do 9 | assert_equal 1, @controller.apotomo_root.size 10 | end 11 | 12 | it "allow tree modifications" do 13 | @controller.apotomo_root << mouse_mock 14 | assert_equal 2, @controller.apotomo_root.size 15 | end 16 | end 17 | 18 | describe "responding to #apotomo_request_processor" do 19 | it "initially return the processor which has an empty root" do 20 | assert_kind_of Apotomo::RequestProcessor, @controller.apotomo_request_processor 21 | assert_equal 1, @controller.apotomo_request_processor.root.size 22 | end 23 | end 24 | 25 | describe "invoking #has_widgets" do 26 | before do 27 | @controller.class.has_widgets do |root| 28 | root << widget(:mouse, 'mum') 29 | end 30 | end 31 | 32 | it "add the widgets to apotomo_root" do 33 | assert_equal 'mum', @controller.apotomo_root['mum'].name 34 | end 35 | 36 | it "add the widgets only once in apotomo_root" do 37 | @controller.apotomo_root 38 | assert @controller.apotomo_root['mum'] 39 | end 40 | 41 | it "allow multiple calls to has_widgets" do 42 | @controller.class.has_widgets do |root| 43 | root << widget(:mouse, 'kid') 44 | end 45 | 46 | assert @controller.apotomo_root['mum'] 47 | assert @controller.apotomo_root['kid'] 48 | end 49 | 50 | it "inherit has_widgets blocks to sub-controllers" do 51 | berry = widget(:mouse, 'berry') 52 | @sub_controller = Class.new(@controller.class) do 53 | has_widgets { |root| root << berry } 54 | end.new 55 | @sub_controller.params = {} 56 | 57 | assert @sub_controller.apotomo_root['mum'] 58 | assert @sub_controller.apotomo_root['berry'] 59 | end 60 | 61 | it "be executed in controller describe" do 62 | @controller.instance_eval do 63 | def roomies; ['mice', 'cows']; end 64 | end 65 | 66 | @controller.class.has_widgets do |root| 67 | root << widget(:mouse, 'kid', :display, :roomies => roomies) 68 | end 69 | 70 | assert_equal ['mice', 'cows'], @controller.apotomo_root['kid'].options[:roomies] 71 | end 72 | end 73 | 74 | 75 | 76 | describe "invoking #url_for_event" do 77 | it "compute an url for any widget" do 78 | assert_equal "/barn/render_event_response?source=mouse&type=footsteps&volume=9", @controller.url_for_event(:footsteps, :source => :mouse, :volume => 9) 79 | end 80 | end 81 | end 82 | 83 | describe "invoking #render_widget" do 84 | before do 85 | @mum = mouse_mock('mum', 'eating') 86 | end 87 | 88 | it "render the widget" do 89 | @controller.apotomo_root << @mum 90 | assert_equal "
    burp!
    \n", @controller.render_widget('mum', :eat) 91 | end 92 | end 93 | 94 | 95 | describe "processing an event request" do 96 | before do 97 | @mum = mouse 98 | @mum << mouse_mock(:kid) 99 | @kid = @mum[:kid] 100 | 101 | @kid.respond_to_event :doorSlam, :with => :eating, :on => 'mum' 102 | @kid.respond_to_event :doorSlam, :with => :squeak 103 | @mum.respond_to_event :doorSlam, :with => :squeak 104 | 105 | @mum.instance_eval do 106 | def squeak; render :js => 'squeak();'; end 107 | end 108 | @kid.instance_eval do 109 | def squeak; render :text => 'squeak!', :update => :true; end 110 | end 111 | end 112 | 113 | ### DISCUSS: needed? 114 | ### FIXME: could somebody get that working? 115 | describe "in event mode" do 116 | it "set the MIME type to text/javascript" do 117 | skip 118 | 119 | @controller.apotomo_root << @mum 120 | 121 | get :render_event_response, :source => :kid, :type => :doorSlam 122 | 123 | assert_equal Mime::JS, @response.content_type 124 | assert_equal "jQuery(\"mum\").replace(\"
    burp!<\\/div>\")\njQuery(\"kid\").update(\"squeak!\")\nsqueak();", @response.body 125 | end 126 | end 127 | end 128 | 129 | ### FIXME: could somebody get that working? 130 | describe "Routing" do 131 | it "generate routes to the render_event_response action" do 132 | skip 133 | 134 | assert_generates "/barn/render_event_response?type=squeak", { :controller => "barn", :action => "render_event_response", :type => "squeak" } 135 | 136 | assert_recognizes({ :controller => "apotomo", :action => "render_event_response", :type => "squeak" }, "/apotomo/render_event_response?type=squeak") 137 | end 138 | end 139 | 140 | end 141 | -------------------------------------------------------------------------------- /test/rails/rails_integration_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RailsIntegrationTest < ActionController::TestCase 4 | include Apotomo::TestCaseMethods::TestController 5 | 6 | class KidWidget < MouseWidget 7 | responds_to_event :squeak, :passing => :root 8 | 9 | def feed 10 | render # invokes #url_for_event. 11 | end 12 | 13 | def squeak 14 | render :text => "squeak!" 15 | end 16 | end 17 | 18 | 19 | class MumWidget < MouseWidget 20 | responds_to_event :squeak 21 | responds_to_event :sniff 22 | 23 | has_widgets do |me| 24 | me << widget("rails_integration_test/kid", :kid) 25 | end 26 | 27 | def eat 28 | render 29 | end 30 | 31 | def make_me_squeak 32 | render 33 | end 34 | 35 | def squeak(evt) 36 | render :text => evt.data 37 | end 38 | 39 | def sniff(evt) 40 | render :text => "sniff sniff" 41 | end 42 | 43 | def child 44 | render :text => render_widget(:kid, :feed) 45 | end 46 | end 47 | 48 | 49 | def setup 50 | super 51 | 52 | @controller.class.has_widgets do |root| 53 | MumWidget.new(root, 'mum') 54 | end 55 | 56 | @controller.instance_eval do 57 | def mum 58 | render :text => render_widget('mum', params[:state]) 59 | end 60 | end 61 | end 62 | 63 | test "provide the rails view helpers in state views" do 64 | get 'mum', :state => :make_me_squeak 65 | assert_select "a", "mum" 66 | end 67 | 68 | test "render" do 69 | get 'mum', :state => :child 70 | puts "-" 71 | assert_equal "/barn/render_event_response?source=kid&type=click\n", @response.body 72 | end 73 | 74 | test "process events" do 75 | get 'render_event_response', :source => 'root', :type => :squeak 76 | assert_equal "squeak!", @response.body 77 | end 78 | 79 | test "#page_updates is populated with event responses" do 80 | get 'render_event_response', :source => 'root', :type => :squeak 81 | 82 | assert_equal ["squeak!"], @controller.apotomo_root.page_updates 83 | end 84 | 85 | test "pass the event with all params data as state-args" do 86 | get 'render_event_response', :source => "mum", :type => "squeak", :pitch => "high" 87 | assert_equal "{\"source\"=>\"mum\", \"type\"=>\"squeak\", \"pitch\"=>\"high\", \"controller\"=>\"barn\", \"action\"=>\"render_event_response\"}\nsqueak!", @response.body 88 | end 89 | 90 | test "render updates to the parent window for an iframe request" do 91 | get 'render_event_response', :source => 'mum', :type => :sniff, :apotomo_iframe => true 92 | 93 | assert_response :success 94 | assert_equal 'text/html', @response.content_type 95 | assert_equal "", @response.body 96 | end 97 | 98 | 99 | # describe "ActionView" do 100 | test "respond to #render_widget" do 101 | @controller.instance_eval do 102 | def mum 103 | render :inline => "<%= render_widget 'mum', :eat %>" 104 | end 105 | end 106 | 107 | get :mum 108 | assert_select "#mum", "burp!" 109 | end 110 | 111 | test "respond to #url_for_event" do 112 | @controller.instance_eval do 113 | def mum 114 | render :inline => "<%= url_for_event :footsteps, :source => 'mum' %>" 115 | end 116 | end 117 | 118 | get :mum 119 | assert_equal "/barn/render_event_response?source=mum&type=footsteps", @response.body 120 | end 121 | end 122 | 123 | 124 | class IncludingApotomoSupportTest < ActiveSupport::TestCase 125 | # describe "A controller not including ControllerMethods explicitely" do 126 | setup do 127 | @class = Class.new(ActionController::Base) 128 | @controller = @class.new 129 | @controller.request = ActionController::TestRequest.new 130 | end 131 | 132 | test "respond to .has_widgets only" do 133 | assert_respond_to @class, :has_widgets 134 | assert_not_respond_to @class, :apotomo_request_processor 135 | end 136 | 137 | test "mixin all methods after first use of .has_widgets" do 138 | @class.has_widgets do |root| 139 | end 140 | 141 | assert_respond_to @class, :has_widgets 142 | assert_respond_to @controller, :apotomo_request_processor 143 | end 144 | # end 145 | end 146 | -------------------------------------------------------------------------------- /test/rails/view_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'action_view/test_case' 3 | 4 | class ViewHelperTest < Apotomo::TestCase 5 | include Apotomo::TestCaseMethods::TestController 6 | include ActionDispatch::Assertions::DomAssertions 7 | 8 | # TODO: use Cell::TestCase#in_view here. 9 | def in_view(subject, &block) 10 | subject = subject.new(@controller, :mum) unless subject.kind_of?(Apotomo::Widget) 11 | setup_test_states_in(subject) 12 | subject.invoke(:in_view, block) 13 | end 14 | 15 | ### DISCUSS: what is this for? 16 | teardown do 17 | Apotomo.js_framework = :prototype 18 | end 19 | 20 | # describe "Rails::ViewHelper" do 21 | ### DISCUSS: needed? 22 | ### FIXME: could somebody get that working? 23 | test "respond to #multipart_form_to_event" do 24 | skip 25 | 26 | assert_dom_equal( "
    ", 27 | in_view(MouseWidget) do 28 | multipart_form_to_event(:footsteps) 29 | end) 30 | end 31 | 32 | test "respond to #url_for_event" do 33 | assert_equal "/barn/render_event_response?source=mum&type=footsteps", in_view(MouseWidget) { url_for_event(:footsteps) } 34 | end 35 | 36 | test "respond to #url_for_event with a namespaced controller" do 37 | @controller = namespaced_controller 38 | assert_equal "/farm/barn/render_event_response?source=mum&type=footsteps", in_view(MouseWidget) { url_for_event(:footsteps) } 39 | end 40 | 41 | test "respond to #widget_tag" do 42 | assert_equal('squeak!', in_view(MouseWidget) do 43 | widget_tag(:span) do 44 | "squeak!" 45 | end 46 | end) 47 | end 48 | 49 | test "respond to #widget_tag with options" do 50 | assert_equal('squeak!', in_view(MouseWidget) do 51 | widget_tag(:span, :id => 'kid', :class => "mouse") do 52 | "squeak!" 53 | end 54 | end) 55 | end 56 | 57 | test "respond to #widget_div" do 58 | assert_equal('
    squeak!
    ', in_view(MouseWidget) do widget_div { "squeak!" } end) 59 | end 60 | 61 | test "respond to #widget_id" do 62 | assert_equal 'mum', in_view(MouseWidget) { widget_id } 63 | end 64 | 65 | test "respond to #render_widget" do 66 | mum = mouse 67 | MouseWidget.new(mum, :kid) 68 | 69 | assert_equal("
    burp!
    \n", in_view(mum) do 70 | render_widget('kid', :eat) 71 | end) 72 | end 73 | 74 | test "respond to #children" do 75 | mum = mouse 76 | MouseWidget.new(mum, :kid) 77 | 78 | assert_equal("
    burp!
    \n", in_view(mum) do 79 | children.collect do |child| 80 | render_widget(child, :eat) 81 | end.join.html_safe 82 | end) 83 | end 84 | 85 | # TODO: test #js_generator 86 | 87 | # TODO: test instance variables access 88 | 89 | # end 90 | end 91 | -------------------------------------------------------------------------------- /test/rails/widget_generator_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'generators/apotomo/widget_generator' 3 | 4 | class WidgetGeneratorTest < Rails::Generators::TestCase 5 | destination File.join(Rails.root, "tmp") 6 | setup :prepare_destination 7 | tests ::Apotomo::Generators::WidgetGenerator 8 | 9 | # describe "Running rails g apotomo::widget" do 10 | # describe "Gerbil squeak snuggle" do 11 | test "create the standard assets" do 12 | 13 | run_generator %w(Gerbil squeak snuggle -t test_unit) 14 | 15 | assert_file "app/widgets/gerbil_widget.rb", /class GerbilWidget < Apotomo::Widget/ 16 | assert_file "app/widgets/gerbil_widget.rb", /def snuggle/ 17 | assert_file "app/widgets/gerbil_widget.rb", /def squeak/ 18 | assert_file "app/widgets/gerbil/snuggle.html.erb", %r(app/widgets/gerbil/snuggle\.html\.erb) 19 | assert_file "app/widgets/gerbil/snuggle.html.erb", %r(

    ) 20 | assert_file "app/widgets/gerbil/squeak.html.erb", %r(app/widgets/gerbil/squeak\.html\.erb) 21 | 22 | assert_file "test/widgets/gerbil_widget_test.rb", %r(class GerbilWidgetTest < Apotomo::TestCase) 23 | assert_file "test/widgets/gerbil_widget_test.rb", %r(widget\(:gerbil\)) 24 | end 25 | 26 | test "create haml assets with -e haml" do 27 | run_generator %w(Gerbil squeak snuggle -e haml -t test_unit) 28 | 29 | assert_file "app/widgets/gerbil_widget.rb", /class GerbilWidget < Apotomo::Widget/ 30 | assert_file "app/widgets/gerbil_widget.rb", /def snuggle/ 31 | assert_file "app/widgets/gerbil_widget.rb", /def squeak/ 32 | assert_file "app/widgets/gerbil/snuggle.html.haml", %r(app/widgets/gerbil/snuggle\.html\.haml) 33 | assert_file "app/widgets/gerbil/snuggle.html.haml", %r(%p) 34 | assert_file "app/widgets/gerbil/squeak.html.haml", %r(app/widgets/gerbil/squeak\.html\.haml) 35 | assert_file "test/widgets/gerbil_widget_test.rb" 36 | end 37 | 38 | test "create slim assets with -e slim" do 39 | run_generator %w(Gerbil squeak snuggle -e slim -t test_unit) 40 | 41 | assert_file "app/widgets/gerbil_widget.rb", /class GerbilWidget < Apotomo::Widget/ 42 | assert_file "app/widgets/gerbil_widget.rb", /def snuggle/ 43 | assert_file "app/widgets/gerbil_widget.rb", /def squeak/ 44 | assert_file "app/widgets/gerbil/snuggle.html.slim", %r(app/widgets/gerbil/snuggle\.html\.slim) 45 | assert_file "app/widgets/gerbil/snuggle.html.slim", %r(p) 46 | assert_file "app/widgets/gerbil/squeak.html.slim", %r(app/widgets/gerbil/squeak\.html\.slim) 47 | assert_file "test/widgets/gerbil_widget_test.rb" 48 | end 49 | 50 | test "work with namespaces" do 51 | run_generator %w(Gerbil::Mouse squeak -t test_unit) 52 | 53 | assert_file "app/widgets/gerbil/mouse_widget.rb", /class Gerbil::MouseWidget < Apotomo::Widget/ 54 | assert_file "app/widgets/gerbil/mouse_widget.rb", /def squeak/ 55 | assert_file "app/widgets/gerbil/mouse/squeak.html.erb", %r(app/widgets/gerbil/mouse/squeak\.html\.erb) 56 | assert_file "test/widgets/gerbil/mouse_widget_test.rb" 57 | end 58 | 59 | # end 60 | # end 61 | end 62 | -------------------------------------------------------------------------------- /test/render_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RenderTest < MiniTest::Spec 4 | include Apotomo::TestCaseMethods::TestController 5 | 6 | describe "#render" do 7 | before do 8 | @mum = mouse('mum') 9 | end 10 | 11 | it "per default display the state content framed in a div" do 12 | assert_equal '

    burp!
    ', @mum.invoke(:eating) 13 | end 14 | 15 | describe "with :text" do 16 | before do 17 | @mum.instance_eval { def eating; render :text => "burp!!!"; end } 18 | end 19 | 20 | it "render the :text" do 21 | assert_equal "burp!!!", @mum.invoke(:eating) 22 | end 23 | end 24 | 25 | it "accept :state and options" do 26 | @mum.instance_eval { def eat(what); render :text => "#{what} today?"; end } 27 | 28 | assert_equal "Rice today?", @mum.render({:state => :eat}, "Rice") 29 | assert_match "Rice today?", @mum.update({:state => :eat}, "Rice") 30 | assert_match "Rice today?", @mum.replace({:state => :eat}, "Rice") 31 | end 32 | 33 | it "expose its instance variables in the rendered view" do 34 | @mum = mouse('mum') do 35 | def educate 36 | @who = "the cat" 37 | @what = "run away" 38 | render 39 | end 40 | end 41 | assert_equal 'If you see the cat do run away!', @mum.invoke(:educate) 42 | end 43 | 44 | describe "with #render" do 45 | describe "and :text" do 46 | before do 47 | @mum.instance_eval do 48 | def squeak 49 | render :text => "squeak();" 50 | end 51 | end 52 | end 53 | 54 | it "just return the plain :text" do 55 | assert_equal 'squeak();', @mum.invoke(:squeak) 56 | end 57 | end 58 | 59 | describe "and no options" do 60 | before do 61 | @mum.instance_eval do 62 | def squeak 63 | render 64 | end 65 | end 66 | end 67 | 68 | it "render the view" do 69 | assert_equal "
    burp!
    ", @mum.invoke(:eating) 70 | end 71 | end 72 | 73 | describe "and :view" do 74 | before do 75 | @mum.instance_eval do 76 | def squeak 77 | render :view => :eating 78 | end 79 | end 80 | end 81 | 82 | it "render the :view" do 83 | assert_equal "
    burp!
    ", @mum.invoke(:squeak) 84 | end 85 | end 86 | end 87 | 88 | describe "#update" do 89 | it "wrap the :text in an update statement" do 90 | @mum.instance_eval do 91 | def squeak 92 | update :text => "squeak!" 93 | end 94 | end 95 | assert_equal "jQuery(\"#mum\").html(\"squeak!\");", @mum.invoke(:squeak) 96 | end 97 | 98 | it "accept a selector" do 99 | @mum.instance_eval do 100 | def squeak 101 | update "div#mouse", :text => '
    squeak!
    ' 102 | end 103 | end 104 | assert_equal "jQuery(\"div#mouse\").html(\"
    squeak!<\\/div>\");", @mum.invoke(:squeak) 105 | end 106 | end 107 | 108 | describe "#replace" do 109 | it "wrap the :text in a replace statement" do 110 | @mum.instance_eval do 111 | def squeak 112 | replace :text => '
    squeak!
    ' 113 | end 114 | end 115 | assert_equal "jQuery(\"#mum\").replaceWith(\"
    squeak!<\\/div>\");", @mum.invoke(:squeak) 116 | end 117 | 118 | it "accept a selector" do 119 | @mum.instance_eval do 120 | def squeak 121 | replace "div#mouse", :text => '
    squeak!
    ' 122 | end 123 | end 124 | assert_equal "jQuery(\"div#mouse\").replaceWith(\"
    squeak!<\\/div>\");", @mum.invoke(:squeak) 125 | end 126 | end 127 | 128 | describe "#escape_js" do 129 | it "escape the string" do 130 | assert_equal "
    squeak!<\\/div>", @mum.escape_js('
    squeak!
    ') 131 | end 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /test/request_processor_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RequestProcessorTest < MiniTest::Spec 4 | include Apotomo::TestCaseMethods::TestController 5 | 6 | describe "RequestProcessor" do 7 | before do 8 | @processor = Apotomo::RequestProcessor.new(parent_controller) 9 | @processor.root << mouse_mock 10 | end 11 | 12 | describe "constructor, #parent_controller, #root" do 13 | it "provide #parent_controller and a single root-node for #root" do 14 | assert_kind_of Apotomo::Widget, @processor.root 15 | assert_equal 2, @processor.root.size # because we added a child 16 | assert_equal :root, @processor.root.name 17 | 18 | assert_equal parent_controller, @processor.root.parent_controller 19 | end 20 | 21 | # TODO: test options argument 22 | 23 | # TODO: test has_widgets_blocks argument 24 | 25 | # TODO: test if after_initialize hook has been run 26 | end 27 | 28 | it "allow external modification of the tree" do # DISCUSS: needed? 29 | assert_equal 2, @processor.root.size 30 | end 31 | 32 | it "delegate #render_widget_for to #root" do 33 | # TODO: @processor.root should expect #render_widget_for 34 | assert_equal 'squeak!', @processor.render_widget_for('mouse', :squeak) 35 | end 36 | 37 | describe "#attach_stateless_blocks_for" do 38 | it "allow has_widgets blocks with root parameter" do 39 | @processor.send(:attach_stateless_blocks_for, [ 40 | Proc.new{ |root| 41 | root << widget(:mouse, 'mouse_sister') 42 | }, 43 | Proc.new{ |root| 44 | root << widget(:mouse, 'mouse_brother') 45 | }], @processor.root, parent_controller) 46 | 47 | # TODO: test if blocks are yielded 48 | # TODO: test what blocks gets 49 | 50 | assert_kind_of MouseWidget, @processor.root['mouse_sister'] 51 | assert_equal 'mouse_sister', @processor.root['mouse_sister'].name 52 | assert_kind_of MouseWidget, @processor.root['mouse_brother'] 53 | assert_equal 'mouse_brother', @processor.root['mouse_brother'].name 54 | end 55 | end 56 | 57 | describe "#process_for" do 58 | before do 59 | class KidWidget < Apotomo::Widget 60 | responds_to_event :doorSlam, :with => :flight 61 | responds_to_event :doorSlam, :with => :squeak 62 | 63 | def flight 64 | render :text => "away from here!" 65 | end 66 | 67 | def squeak 68 | render :text => "squeak!" 69 | end 70 | end 71 | 72 | procs = [Proc.new{ |root| 73 | root << widget(:mouse, 'mum') 74 | KidWidget.new(root['mum'], 'kid') 75 | }] 76 | 77 | @processor = Apotomo::RequestProcessor.new(parent_controller, {:js_framework => :prototype}, procs) 78 | end 79 | 80 | it "return an empty array if nothing was triggered" do 81 | assert_equal [], @processor.process_for(:type => :mouseClick, :source => 'kid') 82 | end 83 | 84 | it "return ordered results if something was triggered" do 85 | assert_equal ["away from here!", "squeak!"], @processor.process_for(:type => :doorSlam, :source => 'kid') 86 | end 87 | 88 | # TODO: test a situation: root.page_updates is not empty before #process_for call 89 | 90 | # TODO: widget instance should expect #fire 91 | 92 | # TODO: make this test without #inspect 93 | # TODO: widget instance should expect responder method (replace this test with) 94 | it "append the params hash to the triggered event" do 95 | KidWidget.class_eval do 96 | def squeak(evt) 97 | render :text => evt.data.inspect 98 | end 99 | end 100 | 101 | assert_equal ["away from here!", %Q({:type=>:doorSlam, :param=>:value, :source=>"kid"})], @processor.process_for(:type => :doorSlam, :param => :value, :source => 'kid') 102 | end 103 | 104 | # TODO: test if after_fire hook has been run 105 | 106 | it "raise an exception when :source is unknown" do 107 | e = assert_raises Apotomo::RequestProcessor::InvalidSourceWidget do 108 | @processor.process_for(:type => :squeak, :source => 'tom') 109 | end 110 | assert_match "Source \"tom\" non-existent", e.message 111 | end 112 | end 113 | 114 | describe "#address_for" do 115 | before do 116 | @processor = Apotomo::RequestProcessor.new(parent_controller) 117 | end 118 | 119 | it "accept an event :type and :source" do 120 | assert_equal({:type => :squeak, :source => 'mum'}, @processor.address_for(:type => :squeak, :source => 'mum')) 121 | end 122 | 123 | it "accept arbitrary options" do 124 | assert_equal({:type => :squeak, :volume => 'loud', :source => 'mum'}, @processor.address_for(:type => :squeak, :volume => 'loud', :source => 'mum')) 125 | end 126 | 127 | it "complain if no :type given" do 128 | e = assert_raises RuntimeError do 129 | @processor.address_for(:source => 'mum') 130 | end 131 | assert_equal "You forgot to provide :source or :type", e.message 132 | end 133 | 134 | it "complain if no :source given" do 135 | e = assert_raises RuntimeError do 136 | @processor.address_for(:type => :footsteps) 137 | end 138 | assert_equal "You forgot to provide :source or :type", e.message 139 | end 140 | end 141 | end 142 | end 143 | 144 | class RequestProcessorHooksTest < MiniTest::Spec 145 | include Apotomo::TestCaseMethods::TestController 146 | include Apotomo::TestCaseMethods 147 | 148 | describe "RequestProcessor' hooks" do 149 | before do 150 | @kid = mouse_mock(:kid) 151 | @class = Class.new(Apotomo::RequestProcessor) 152 | @class.instance_eval do 153 | def kid=(kid); @kid = kid; end 154 | def kid; @kid; end 155 | end 156 | @class.kid = @kid 157 | end 158 | 159 | describe ":after_initialize hook" do 160 | # TODO: test when hooks are called 161 | # TODO: test if block is yielded 162 | # TODO: test what blocks gets 163 | 164 | it "be called after the has_widgets blocks invokation" do 165 | @class.after_initialize do |r| 166 | r.root[:mum] << self.class.kid # requires that :mum is there, yet. 167 | end 168 | 169 | @r = @class.new(parent_controller, {}, 170 | [Proc.new { |root| root << widget(:mouse, :mum) }]) 171 | 172 | assert @r.root[:mum][:kid] 173 | end 174 | end 175 | 176 | describe ":after_fire hook" do 177 | it "be called in #process_for after fire" do 178 | @class.after_fire do |r| 179 | r.root[:mum] << self.class.kid 180 | end 181 | 182 | # DISCUSS: maybe add a trigger test here? 183 | @r = @class.new(parent_controller, {}, 184 | [Proc.new { |root| root << widget(:mouse, :mum) }]) 185 | @r.process_for(:source => "root", :type => :noop) # calls after_fire hook 186 | 187 | assert @r.root[:mum][:kid] 188 | end 189 | end 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /test/test_case_methods.rb: -------------------------------------------------------------------------------- 1 | module Apotomo 2 | module TestCaseMethods 3 | def root_mock 4 | MouseWidget.new(parent_controller, :root) 5 | end 6 | 7 | def mouse(id=nil, &block) 8 | MouseWidget.new(parent_controller, id || :mouse).tap do |widget| 9 | widget.instance_eval &block if block_given? 10 | end 11 | end 12 | 13 | def mouse_mock(id='mouse', opts={}, &block) 14 | widget(:mouse, id, opts) 15 | end 16 | 17 | def mouse_class_mock(&block) 18 | klass = Class.new(MouseWidget) 19 | klass.instance_eval &block if block_given? 20 | klass 21 | end 22 | 23 | def mum_and_kid! 24 | @mum = mouse('mum') 25 | @kid = MouseWidget.new(@mum, 'kid') 26 | 27 | @mum.respond_to_event :squeak, :with => :answer_squeak 28 | @mum.respond_to_event :squeak, :from => 'kid', :with => :alert 29 | @mum.respond_to_event :footsteps, :with => :escape 30 | 31 | @kid.respond_to_event :footsteps, :with => :peek 32 | 33 | @mum.instance_eval do 34 | def list; @list ||= []; end 35 | def answer_squeak; self.list << 'answer squeak'; render :text => "squeak", :render_children => false; end 36 | def alert; self.list << 'be alerted'; render :text => "alert!", :render_children => false; end 37 | def escape; self.list << 'escape'; render :text => "escape", :render_children => false; end 38 | end 39 | 40 | @kid.instance_eval do 41 | def peek; root.list << 'peek'; render :text => "" end 42 | end 43 | 44 | @mum 45 | end 46 | 47 | def root_mum_and_kid! 48 | mum_and_kid! 49 | 50 | @root = Apotomo::Widget.new(parent_controller, 'root', :display) 51 | @root << @mum 52 | end 53 | 54 | def barn_controller! 55 | @controller = Class.new(ActionController::Base) do 56 | def self.default_url_options 57 | { :controller => :barn } 58 | end 59 | end.new 60 | @controller.extend(ActionController::UrlWriter) 61 | @controller.params = {} 62 | end 63 | 64 | module TestController 65 | def setup 66 | barn_controller! 67 | end 68 | 69 | # Creates a mock controller instance. Currently, each widget needs a parent controller instance due to some 70 | # sucky dependency in cells. 71 | def barn_controller! 72 | @controller = Class.new(ApotomoController) do 73 | def initialize(*) 74 | super 75 | self.request = ActionController::TestRequest.new 76 | end 77 | 78 | def self.name 79 | "BarnController" 80 | end 81 | 82 | def self.default_url_options 83 | { :controller => :barn } 84 | end 85 | end.new 86 | end 87 | 88 | def parent_controller 89 | @controller 90 | end 91 | 92 | def namespaced_controller 93 | controller = Farm::BarnController.new 94 | controller.request = ActionController::TestRequest.new 95 | controller 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /test/test_case_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'apotomo/test_case' 3 | 4 | class CommentsWidget < Apotomo::Widget 5 | end 6 | 7 | class CommentsWidgetTest < Apotomo::TestCase 8 | end 9 | 10 | class MouseWidgetTest < Apotomo::TestCase 11 | end 12 | 13 | class TestCaseTest < MiniTest::Spec 14 | describe "TestCase" do 15 | describe "responding to #root" do 16 | before do 17 | @klass = MouseWidgetTest 18 | @test = @klass.new(:widget).tap { |t| t.setup } 19 | @klass.has_widgets do |root| 20 | root << widget(:mouse, 'mum', :eating) 21 | end 22 | end 23 | 24 | it "respond to #root" do 25 | assert_equal ['root', 'mum'], @test.root.collect(&:name) 26 | end 27 | 28 | it "raise an error if no has_widgets block given" do 29 | exc = assert_raises RuntimeError do 30 | @test = Class.new(Apotomo::TestCase).new(:widget).tap { |t| t.setup } 31 | @test.root 32 | end 33 | assert_equal "Please setup a widget tree using has_widgets()", exc.message 34 | end 35 | 36 | # TODO: needed? why root but not self? 37 | it "memorize root" do 38 | @test.root.visible = false 39 | assert_equal false, @test.root.visible? 40 | end 41 | 42 | it "respond to #render_widget" do 43 | assert_equal "
    burp!
    \n", @test.render_widget('mum', :eat) 44 | assert_equal "
    burp!
    \n", @test.last_invoke 45 | end 46 | 47 | it "respond to #assert_select" do 48 | @test.render_widget('mum', :eat) 49 | 50 | @test.assert_select("div#mum", "burp!") 51 | exc = assert_raises MiniTest::Assertion do 52 | @test.assert_select("div#mummy", "burp!") 53 | end 54 | assert_match "Expected at least 1 element matching \"div#mummy\", found 0", exc.message 55 | end 56 | 57 | describe "using events" do 58 | before do 59 | @mum = @test.root['mum'] 60 | @mum.respond_to_event :footsteps, :with => :squeak 61 | @mum.instance_eval do 62 | def squeak(evt) 63 | render :text => evt.data 64 | end 65 | end 66 | end 67 | 68 | it "respond to #trigger" do 69 | assert_equal ["{}"], @test.trigger(:footsteps, 'mum') 70 | end 71 | 72 | it "pass options from #trigger to the evt" do 73 | assert_equal(["{:direction=>:kitchen}"] , @test.trigger(:footsteps, 'mum', :direction => :kitchen)) 74 | end 75 | 76 | it "respond to #assert_response" do 77 | @test.trigger(:footsteps, 'mum') 78 | 79 | assert @test.assert_response("{}") 80 | end 81 | end 82 | 83 | describe "#view_assigns" do 84 | it "be emtpy when nothing was set" do 85 | @test.render_widget('mum') 86 | 87 | assert_equal({}, @test.view_assigns) 88 | end 89 | 90 | it "return the instance variables from the last #render_widget" do 91 | @mum = @test.root['mum'] 92 | @mum.instance_eval do 93 | def sleep 94 | @duration = "8h" 95 | end 96 | end 97 | @test.render_widget('mum', :sleep) 98 | 99 | assert_equal({:duration => "8h"}, @test.view_assigns) 100 | end 101 | end 102 | end 103 | 104 | it "respond to #parent_controller and return a controller with correct #controller_path" do 105 | @test = Apotomo::TestCase.new(:widget).tap { |t| t.setup } 106 | 107 | assert_kind_of ActionController::Base, @test.parent_controller 108 | assert_equal "barn", @test.parent_controller.controller_path 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | 3 | ENV['RAILS_ENV'] = 'test' 4 | require "dummy/config/environment" 5 | require "rails/test_help" # sets up ActionController::TestCase's @routes 6 | 7 | require 'cells' 8 | require 'apotomo' 9 | 10 | Apotomo::Widget.append_view_path(File.expand_path(File.dirname(__FILE__) + "/widgets")) 11 | 12 | require "test_case_methods" # Load test support files. 13 | 14 | MiniTest::Spec.class_eval do 15 | include Apotomo::WidgetShortcuts 16 | include Apotomo::TestCaseMethods 17 | 18 | def assert_not(assertion) 19 | assert !assertion 20 | end 21 | end 22 | 23 | ActiveSupport::TestCase.class_eval do 24 | include Apotomo::WidgetShortcuts 25 | include Apotomo::TestCaseMethods 26 | 27 | def assert_not(assertion) 28 | assert !assertion 29 | end 30 | end 31 | 32 | class ApotomoController < ActionController::Base 33 | include Apotomo::Rails::ControllerMethods 34 | include Rails.application.routes.url_helpers 35 | 36 | def mum 37 | end 38 | end 39 | 40 | module Farm 41 | class BarnController < ApotomoController 42 | end 43 | end 44 | 45 | class MouseWidget < Apotomo::Widget 46 | def squeak 47 | render :text => "squeak!" 48 | end 49 | 50 | def eating 51 | render 52 | end 53 | 54 | def eat 55 | render 56 | end 57 | 58 | def display 59 | end 60 | end 61 | 62 | # Enable dynamic states so we can do Cell.class_eval { def ... } at runtime. 63 | Apotomo::Widget.class_eval do 64 | def action_method?(*); true; end 65 | end 66 | -------------------------------------------------------------------------------- /test/tree_node_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TreeNodeTest < MiniTest::Spec 4 | include Apotomo::TestCaseMethods::TestController 5 | 6 | describe "initialization" do 7 | before do 8 | @mum = mouse('mum') 9 | @mum << mouse_mock(:kid) 10 | @kid = @mum[:kid] 11 | @kid << mouse_mock(:grandchild) 12 | @grandchild = @kid[:grandchild] 13 | @mum << mouse_mock(:another_kid) 14 | @another_kid = @mum[:another_kid] 15 | @another_mum = mouse('another_mum') 16 | end 17 | 18 | it "respond to #setup_tree_node" do 19 | @another_mum.setup_tree_node(@mum) 20 | 21 | assert @another_mum, @mum[:another_mum] 22 | assert @mum, @another_mum.parent 23 | end 24 | 25 | describe "#to_s" do 26 | it "return its description" do 27 | assert_match /^Node ID\: \w+ Parent\: \w+ Children\: \d+ Total Nodes\: \d+$/, @mum.to_s 28 | end 29 | 30 | it "return correct Node ID" do 31 | @kid.stub :widget_id, 'awesome_widget' do 32 | assert_match /Node ID\: awesome_widget/, @kid.to_s 33 | end 34 | end 35 | 36 | it "return correct Node ID if it's non-String" do 37 | @kid.stub :widget_id, :awesome_widget do 38 | assert_match /Node ID\: awesome_widget/, @kid.to_s 39 | end 40 | end 41 | 42 | it "return correct Parent ID" do 43 | @mum.stub :name, 'awesome_mum' do 44 | assert_match /Parent\: awesome_mum/, @kid.to_s 45 | end 46 | end 47 | 48 | it "return correct Parent ID if it's non-String" do 49 | @mum.stub :name, :awesome_mum do 50 | assert_match /Parent\: awesome_mum/, @kid.to_s 51 | end 52 | end 53 | 54 | it "return correct Total Nodes" do 55 | @kid.stub :size, 123 do 56 | assert_match /Total Nodes\: 123/, @kid.to_s 57 | end 58 | end 59 | 60 | # TODO: test Children after #children_size will be extracted 61 | end 62 | 63 | describe "#add_widget" do 64 | it "add widget if it is not already added and return it" do 65 | mouse_sister = mouse(:mouse_sister) 66 | @mum.add_widget(mouse_sister) 67 | 68 | assert_equal mouse_sister, @mum[:mouse_sister] 69 | end 70 | 71 | it "raise an exception if widget is already added" do 72 | assert_raises RuntimeError do 73 | @mum.add_widget(@kid) 74 | end 75 | end 76 | 77 | it "raise an exception if widget with the same name is already added" do 78 | assert_raises RuntimeError do 79 | new_kid = mouse(:kid) 80 | @mum.add_widget(new_kid) 81 | end 82 | end 83 | end 84 | 85 | describe "#remove!" do 86 | describe "when try to remove a child" do 87 | it "remove child" do 88 | @mum.remove!(@kid) 89 | 90 | assert_nil @mum[:kid] 91 | end 92 | 93 | it "make child #root?'ed" do 94 | @mum.remove!(@kid) 95 | 96 | assert @kid.root? 97 | end 98 | 99 | it "return child" do 100 | assert_equal @kid, @mum.remove!(@kid) 101 | end 102 | end 103 | 104 | describe "when try to remove a foreign widget" do 105 | it "make widget #root?'ed" do 106 | @kid.remove!(@another_mum) 107 | assert @another_mum.root? 108 | end 109 | 110 | it "return widget" do 111 | assert_equal @another_kid, @kid.remove!(@another_kid) 112 | end 113 | end 114 | end 115 | 116 | describe "#parent=" do 117 | it "set instance as parent" do 118 | @kid.send(:parent=, @another_mum) 119 | 120 | assert_equal "another_mum", @kid.parent.name 121 | end 122 | end 123 | 124 | describe "#root!" do 125 | it "set instance as root" do 126 | @kid.send(:root!) 127 | 128 | assert_equal :kid, @grandchild.root.name 129 | end 130 | end 131 | 132 | describe "#root?" do 133 | it "return true if parent doesn't exist" do 134 | assert @mum.root? 135 | end 136 | 137 | it "return false if parent exists" do 138 | assert_not @kid.root? 139 | end 140 | end 141 | 142 | describe "#parent" do 143 | it "return parent if it exists" do 144 | assert_equal @mum, @kid.parent 145 | end 146 | 147 | it "return nil if it doesn't exist" do 148 | assert_equal nil, @mum.parent 149 | end 150 | end 151 | 152 | describe "#children" do 153 | it "return children unless a block given" do 154 | assert_equal [@kid, @another_kid], @mum.children 155 | end 156 | 157 | it "return children if a block given" do 158 | assert_equal [@kid, @another_kid], @mum.children { |child| child } 159 | end 160 | 161 | # TODO: check if block yielded 162 | end 163 | 164 | describe "#each" do 165 | it "return children" do 166 | assert_equal [@kid, @another_kid], @mum.each { |widget| widget } 167 | end 168 | 169 | # TODO: check if block yielded 170 | end 171 | 172 | describe "accessing children by #[]" do 173 | it "return child by its name" do 174 | assert_equal @kid, @mum[:kid] 175 | end 176 | 177 | it "return child by its number" do 178 | assert_equal @kid, @mum[0] 179 | assert_equal @another_kid, @mum[1] 180 | end 181 | end 182 | 183 | describe "#size" do 184 | it "return subnodes count + 1" do 185 | assert_equal 4, @mum.size 186 | assert_equal 2, @kid.size 187 | assert_equal 1, @grandchild.size 188 | assert_equal 1, @another_kid.size 189 | assert_equal 1, @another_mum.size 190 | end 191 | end 192 | 193 | describe "#root" do 194 | it "return root widget for a kid" do 195 | assert_equal @mum, @mum.root 196 | assert_equal @mum, @kid.root 197 | assert_equal @mum, @grandchild.root 198 | end 199 | end 200 | 201 | describe "#path" do 202 | it "return the path from the widget to root" do 203 | assert_equal "mum", @mum.path 204 | assert_equal "mum/kid", @kid.path 205 | assert_equal "mum/kid/grandchild", @grandchild.path 206 | end 207 | end 208 | 209 | # TODO: test #printTree 210 | 211 | # TODO: test #<=> 212 | 213 | # TODO: test #find_by_path 214 | 215 | end 216 | end 217 | -------------------------------------------------------------------------------- /test/widget_shortcuts_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MumWidget < MouseWidget 4 | end 5 | 6 | class MouseTabsWidget < Apotomo::Widget 7 | end 8 | 9 | class WidgetShortcutsTest < MiniTest::Spec 10 | describe "FactoryProxy" do 11 | before do 12 | @factory = Apotomo::WidgetShortcuts::FactoryProxy 13 | end 14 | 15 | # DISCISS: needed? 16 | describe "#constant_for" do 17 | before do 18 | @dsl = @factory.new(:class, :id) 19 | end 20 | 21 | it "constantize symbols" do 22 | assert_equal MouseWidget, @dsl.send(:constant_for, :mouse) 23 | end 24 | 25 | # DISCISS: needed? 26 | it "not try to singularize the widget class" do 27 | assert_equal MouseTabsWidget, @dsl.send(:constant_for, :mouse_tabs) 28 | end 29 | end 30 | 31 | describe "#widget and #<<" do 32 | before do 33 | @root = Apotomo::Widget.new(nil, :root) 34 | end 35 | 36 | it "create a widget instance with options and set them" do 37 | proxy = widget(:mum, :mummy, :eating, :color => 'grey', :type => :hungry) 38 | @root << proxy 39 | 40 | assert_kind_of MumWidget, @root[:mummy] 41 | assert_equal :mummy, @root[:mummy].name 42 | assert_equal({:color => "grey", :type => :hungry}, @root[:mummy].options) 43 | end 44 | 45 | it "create a widget instance without options" do 46 | @root << widget(:mum, :mummy) 47 | @mum = @root[:mummy] 48 | 49 | assert_kind_of MumWidget, @mum 50 | assert_equal :mummy, @mum.widget_id 51 | assert_equal({}, @mum.options) 52 | end 53 | 54 | it "create a widget instance with prefix argument only (id is equal to prefix)" do 55 | @root << widget(:mum) 56 | @mum = @root[:mum] 57 | 58 | assert_kind_of MumWidget, @mum 59 | assert_equal :mum, @mum.name 60 | assert_equal({}, @mum.options) 61 | end 62 | 63 | it "create a widget instance and yield itself" do 64 | # TODO: don't create a subwidget but use expectations 65 | ficken = widget(:mum) do |mum| 66 | mum << widget(:mouse, :kid) 67 | end 68 | @root << ficken 69 | 70 | assert_equal 2, @root[:mum].size 71 | assert_kind_of MouseWidget, @root[:mum][:kid] 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/widget_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | # TODO: there are *many* things in Apotomo::Widget *isn't* tested here 4 | 5 | class WidgetTest < MiniTest::Spec 6 | include Apotomo::TestCaseMethods::TestController 7 | 8 | describe "The constructor" do 9 | it "accept the parent_controller as first arg" do 10 | assert_kind_of ActionController::Base, @controller 11 | @mum = Apotomo::Widget.new(@controller, 'mum', :squeak) 12 | end 13 | end 14 | 15 | describe "Widget.has_widgets" do 16 | before do 17 | @mum = Class.new(MouseWidget) do 18 | has_widgets do |me| 19 | me << widget(:mouse, :baby) 20 | #MouseWidget.new(me, :baby) # this is also possible. 21 | end 22 | end.new(@controller, 'mum') 23 | 24 | @kid = Class.new(@mum.class).new(@controller, 'mum') 25 | end 26 | 27 | it "before the widget family at creation time" do 28 | assert_equal 1, @mum.children.size 29 | assert_kind_of MouseWidget, @mum[:baby] 30 | end 31 | 32 | it "inherit trees for now" do 33 | assert_equal 1, @mum.children.size 34 | assert_kind_of MouseWidget, @mum[:baby] 35 | end 36 | end 37 | 38 | 39 | describe "A widget" do 40 | before do 41 | @mum = Apotomo::Widget.new(@controller, 'mum', :squeak) 42 | end 43 | 44 | describe "responding to #address_for_event" do 45 | it "accept an event :type" do 46 | assert_equal({:source=>"mum", :type=>:squeak, :controller=>"barn"}, @mum.address_for_event(:squeak)) 47 | end 48 | 49 | it "accept a :source" do 50 | assert_equal({:source=>"kid", :type=>:squeak, :controller=>"barn"}, @mum.address_for_event(:squeak, :source => 'kid')) 51 | end 52 | 53 | it "accept arbitrary options" do 54 | assert_equal({:volume=>"loud", :source=>"mum", :type=>:squeak, :controller=>"barn"}, @mum.address_for_event(:squeak, :volume => 'loud')) 55 | end 56 | 57 | it "work with controller namespaces" do 58 | @mum = Apotomo::Widget.new(namespaced_controller, 'mum', :squeak) 59 | assert_equal({:source=>"mum", :type=>:squeak, :controller=>"farm/barn"}, @mum.address_for_event(:squeak)) 60 | end 61 | end 62 | 63 | describe "implementing visibility" do 64 | it "per default respond to #visible?" do 65 | assert @mum.visible? 66 | end 67 | 68 | it "expose a setter therefore" do 69 | @mum.visible = false 70 | assert_not @mum.visible? 71 | end 72 | end 73 | 74 | describe "#find_widget" do 75 | before do 76 | mum_and_kid! 77 | end 78 | 79 | it "find itself" do 80 | assert_equal @mum, @mum.find_widget('mum') 81 | end 82 | 83 | it "return nil for not-existant widgets" do 84 | assert_nil @mum.find_widget('pet') 85 | end 86 | 87 | it "find children" do 88 | assert_equal @kid, @mum.find_widget('kid') 89 | end 90 | 91 | it "find treat 'id' and :id the same" do 92 | assert_equal @mum.find_widget(:kid), @mum.find_widget('kid') 93 | end 94 | end 95 | 96 | it "respond to the WidgetShortcuts methods, like #widget" do 97 | assert_respond_to @mum, :widget 98 | end 99 | 100 | it "respond to #parent_controller and return the AC in root" do 101 | @mum << mouse_mock(:kid) 102 | assert_equal @controller, @mum.parent_controller 103 | assert_equal @controller, @mum[:kid].parent_controller 104 | end 105 | 106 | it "alias #widget_id to #name" do 107 | assert_equal @mum.name, @mum.widget_id 108 | end 109 | 110 | it "respond to .controller_path" do 111 | assert_equal "mouse", MouseWidget.controller_path 112 | end 113 | 114 | # internal_methods: 115 | it "not list internal methods in action_methods" do 116 | # FIXME: puts "WTF is wrong again with AC.action_methods godamn, I HATE this magic shit!" 117 | unless Cell.rails_version >= 3.1 118 | assert Class.new(Apotomo::Widget).action_methods.empty? 119 | end 120 | end 121 | 122 | it "list both local and inherited states in Widget.action_methods" do 123 | assert MouseWidget.action_methods.collect{ |m| m.to_s }.include?("squeak") 124 | assert Class.new(MouseWidget).action_methods.collect{ |m| m.to_s }.include?("squeak") 125 | end 126 | 127 | it "not list #display in internal_methods although it's defined in Object" do 128 | assert_not Apotomo::Widget.internal_methods.include?(:display) 129 | end 130 | end 131 | end 132 | 133 | 134 | class RenderWidgetTest < ActiveSupport::TestCase 135 | include Apotomo::TestCaseMethods::TestController 136 | 137 | describe "#render_widget" do 138 | it "allow passing widget id" do 139 | assert_equal "squeak!", mouse.render_widget('mouse', :squeak) 140 | end 141 | 142 | it "allow passing widget instance" do 143 | assert_equal 'squeak!', mouse.render_widget(mouse(:mum), :squeak) 144 | end 145 | 146 | it "use :display as standard state" do 147 | mum = mouse('Mum') do 148 | def display 149 | render :text => "#{widget_id}, that's me!" 150 | end 151 | end 152 | 153 | assert_equal "Mum, that's me!", mouse.render_widget(mum) 154 | end 155 | 156 | it "raise an exception when a non-existent widget id is passed" do 157 | e = assert_raises RuntimeError do 158 | mouse.render_widget('mummy') 159 | end 160 | 161 | assert_equal "Couldn't render non-existent widget `mummy`", e.message 162 | end 163 | 164 | it "pass options as state-args" do 165 | mum = mouse do 166 | def display(color="grey") 167 | render :text => "I'm #{color}" 168 | end 169 | end 170 | 171 | assert_equal("I'm grey", mouse.render_widget(mum), "default value in state-arg didn't work") 172 | assert_equal("I'm black", mouse.render_widget(mum, :display, "black")) 173 | end 174 | 175 | it "use #find_widget from self to find the passed widget id" do 176 | mum = mouse << mouse_mock(:kid) 177 | 178 | assert_equal "
    burp!
    \n", mum.render_widget(:kid, :eat) 179 | end 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /test/widgets/mouse/eat.erb: -------------------------------------------------------------------------------- 1 |
    burp!
    2 | -------------------------------------------------------------------------------- /test/widgets/mouse/eating.html.erb: -------------------------------------------------------------------------------- 1 |
    burp!
    -------------------------------------------------------------------------------- /test/widgets/mouse/educate.html.erb: -------------------------------------------------------------------------------- 1 | If you see <%= @who %> do <%= @what %>! -------------------------------------------------------------------------------- /test/widgets/mouse/feed.html.erb: -------------------------------------------------------------------------------- 1 | <%= url_for_event :click %> 2 | -------------------------------------------------------------------------------- /test/widgets/mouse/make_me_squeak.html.erb: -------------------------------------------------------------------------------- 1 | <%= widget_id %> 2 | -------------------------------------------------------------------------------- /test/widgets/mouse/snuggle.html.erb: -------------------------------------------------------------------------------- 1 |
    <%= render_widget 'kid' %>
    2 | --------------------------------------------------------------------------------