From 090930d7b260dd11412a329effeeb5b37f275fc6 Mon Sep 17 00:00:00 2001
From: Eric Davis <edavis@littlestreamsoftware.com>
Date: Wed, 10 Jun 2009 11:26:11 -0700
Subject: [PATCH 1/2] Converted Menus to a Tree structure to allow submenus.

* Required the rubytree gem
* Patched RubyTree's TreeNode to add some additional methods.
* Converted the menu rendering to walk the Tree of MenuItems to render
  each item
* Added a menu option for :parent_menu to make this menu a child of the parent
* Added a bunch of tests
* Made MenuItem a subclass of Tree::TreeNode in order to use it's methods
  directly
* Changed the exceptions in MenuItem#new to be ArgumentErrors instead of the
  generic RuntimeError
---
 config/environment.rb                              |    2 +
 lib/redmine/menu_manager.rb                        |  239 ++++++++++++++++---
 test/unit/lib/redmine/menu_manager/mapper_test.rb  |  166 ++++++++++++++
 .../lib/redmine/menu_manager/menu_helper_test.rb   |  161 +++++++++++++
 .../lib/redmine/menu_manager/menu_item_test.rb     |  108 +++++++++
 test/unit/lib/redmine/menu_manager_test.rb         |   28 +++
 test/unit/lib/redmine_test.rb                      |   84 +++++++
 7 files changed, 750 insertions(+), 38 deletions(-)
 create mode 100644 test/unit/lib/redmine/menu_manager/mapper_test.rb
 create mode 100644 test/unit/lib/redmine/menu_manager/menu_helper_test.rb
 create mode 100644 test/unit/lib/redmine/menu_manager/menu_item_test.rb
 create mode 100644 test/unit/lib/redmine/menu_manager_test.rb
 create mode 100644 test/unit/lib/redmine_test.rb

diff --git a/config/environment.rb b/config/environment.rb
index e6d4259..1df06f5 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -50,6 +50,8 @@ Rails::Initializer.run do |config|
   # It will automatically turn deliveries on
   config.action_mailer.perform_deliveries = false
 
+  config.gem 'rubytree', :lib => 'tree'
+  
   # Load any local configuration that is kept out of source control
   # (e.g. gems, patches).
   if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
diff --git a/lib/redmine/menu_manager.rb b/lib/redmine/menu_manager.rb
index d6688ff..debcdd1 100644
--- a/lib/redmine/menu_manager.rb
+++ b/lib/redmine/menu_manager.rb
@@ -15,6 +15,84 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 
+require 'tree' # gem install rubytree
+
+# Monkey patch the TreeNode to add on a few more methods :nodoc:
+module TreeNodePatch
+  def self.included(base)
+    base.class_eval do
+      attr_reader :last_items_count
+      
+      alias :old_initilize :initialize
+      def initialize(name, content = nil)
+        old_initilize(name, content)
+        @last_items_count = 0
+        extend(InstanceMethods)
+      end
+    end
+  end
+  
+  module InstanceMethods
+    # Adds the specified child node to the receiver node.  The child node's
+    # parent is set to be the receiver.  The child is added as the first child in
+    # the current list of children for the receiver node.
+    def prepend(child)
+      raise "Child already added" if @childrenHash.has_key?(child.name)
+
+      @childrenHash[child.name]  = child
+      @children = [child] + @children
+      child.parent = self
+      return child
+
+    end
+
+    # Adds the specified child node to the receiver node.  The child node's
+    # parent is set to be the receiver.  The child is added at the position
+    # into the current list of children for the receiver node.
+    def add_at(child, position)
+      raise "Child already added" if @childrenHash.has_key?(child.name)
+
+      @childrenHash[child.name]  = child
+      @children = @children.insert(position, child)
+      child.parent = self
+      return child
+
+    end
+
+    def add_last(child)
+      raise "Child already added" if @childrenHash.has_key?(child.name)
+
+      @childrenHash[child.name]  = child
+      @children <<  child
+      @last_items_count += 1
+      child.parent = self
+      return child
+
+    end
+
+    # Adds the specified child node to the receiver node.  The child node's
+    # parent is set to be the receiver.  The child is added as the last child in
+    # the current list of children for the receiver node.
+    def add(child)
+      raise "Child already added" if @childrenHash.has_key?(child.name)
+
+      @childrenHash[child.name]  = child
+      position = @children.size - @last_items_count
+      @children.insert(position, child)
+      child.parent = self
+      return child
+
+    end
+
+    # Will return the position (zero-based) of the current child in
+    # it's parent
+    def position
+      self.parent.children.index(self)
+    end
+  end
+end
+Tree::TreeNode.send(:include, TreeNodePatch)
+
 module Redmine
   module MenuManager
     module MenuController
@@ -79,35 +157,80 @@ module Redmine
       
       def render_menu(menu, project=nil)
         links = []
-        menu_items_for(menu, project) do |item, caption, url, selected|
-          links << content_tag('li', 
-            link_to(h(caption), url, item.html_options(:selected => selected)))
+        menu_items_for(menu, project) do |node|
+          links << render_menu_node(node, project)
         end
         links.empty? ? nil : content_tag('ul', links.join("\n"))
       end
 
+      def render_menu_node(node, project=nil)
+        caption, url, selected = extract_node_details(node, project)
+        if node.hasChildren?
+          html = []
+          html << '<li>'
+          html << render_single_menu_node(node, caption, url, selected) # parent
+          html << '  <ul>'
+          node.children.each do |child|
+            html << render_menu_node(child, project)
+          end
+          html << '  </ul>'
+          html << '</li>'
+          return html.join("\n")
+        else
+          return content_tag('li',
+                               render_single_menu_node(node, caption, url, selected))
+        end
+      end
+
+      def render_single_menu_node(item, caption, url, selected)
+        link_to(h(caption), url, item.html_options(:selected => selected))
+      end
+      
       def menu_items_for(menu, project=nil)
         items = []
-        Redmine::MenuManager.allowed_items(menu, User.current, project).each do |item|
-          unless item.condition && !item.condition.call(project)
-            url = case item.url
-            when Hash
-              project.nil? ? item.url : {item.param => project}.merge(item.url)
-            when Symbol
-              send(item.url)
-            else
-              item.url
-            end
-            caption = item.caption(project)
+        Redmine::MenuManager.items(menu).root.children.each do |node|
+          if allowed_node?(node, User.current, project)
             if block_given?
-              yield item, caption, url, (current_menu_item == item.name)
+              yield node
             else
-              items << [item, caption, url, (current_menu_item == item.name)]
+              items << node  # TODO: not used?
             end
           end
         end
         return block_given? ? nil : items
       end
+
+      def extract_node_details(node, project=nil)
+        item = node
+        url = case item.url
+        when Hash
+          project.nil? ? item.url : {item.param => project}.merge(item.url)
+        when Symbol
+          send(item.url)
+        else
+          item.url
+        end
+        caption = item.caption(project)
+        return [caption, url, (current_menu_item == item.name)]
+      end
+
+      # Checks if a user is allowed to access the menu item by:
+      #
+      # * Checking the conditions of the item
+      # * Checking the url target (project only)
+      def allowed_node?(node, user, project)
+        if node.condition && !node.condition.call(project)
+          # Condition that doesn't pass
+          return false
+        end
+
+        if project
+          return user && user.allowed_to?(node.url, project)
+        else
+          # outside a project, all menu items allowed
+          return true
+        end
+      end
     end
     
     class << self
@@ -122,17 +245,13 @@ module Redmine
       end
       
       def items(menu_name)
-        @items[menu_name.to_sym] || []
-      end
-      
-      def allowed_items(menu_name, user, project)
-        project ? items(menu_name).select {|item| user && user.allowed_to?(item.url, project)} : items(menu_name)
+        @items[menu_name.to_sym] || Tree::TreeNode.new(:root, {})
       end
     end
     
     class Mapper
       def initialize(menu, items)
-        items[menu] ||= []
+        items[menu] ||= Tree::TreeNode.new(:root, {})
         @menu = menu
         @menu_items = items[menu]
       end
@@ -151,36 +270,78 @@ module Redmine
       # * html_options: a hash of html options that are passed to link_to
       def push(name, url, options={})
         options = options.dup
-        
+
+        if options[:parent_menu]
+          subtree = self.find(options[:parent_menu])
+          if subtree
+            target_root = subtree
+          else
+            target_root = @menu_items.root
+          end
+
+        else
+          target_root = @menu_items.root
+        end
+
         # menu item position
-        if before = options.delete(:before)
-          position = @menu_items.collect(&:name).index(before)
+        if first = options.delete(:first)
+          target_root.prepend(MenuItem.new(name, url, options))
+        elsif before = options.delete(:before)
+
+          if exists?(before)
+            target_root.add_at(MenuItem.new(name, url, options), position_of(before))
+          else
+            target_root.add(MenuItem.new(name, url, options))
+          end
+
         elsif after = options.delete(:after)
-          position = @menu_items.collect(&:name).index(after)
-          position += 1 unless position.nil?
+
+          if exists?(after)
+            target_root.add_at(MenuItem.new(name, url, options), position_of(after) + 1)
+          else
+            target_root.add(MenuItem.new(name, url, options))
+          end
+          
         elsif options.delete(:last)
-          position = @menu_items.size
-          @@last_items_count[@menu] += 1
+          target_root.add_last(MenuItem.new(name, url, options))
+        else
+          target_root.add(MenuItem.new(name, url, options))
         end
-        # default position
-        position ||= @menu_items.size - @@last_items_count[@menu]
-        
-        @menu_items.insert(position, MenuItem.new(name, url, options))
       end
       
       # Removes a menu item
       def delete(name)
-        @menu_items.delete_if {|i| i.name == name}
+        if found = self.find(name)
+          @menu_items.remove!(found)
+        end
+      end
+
+      # Checks if a menu item exists
+      def exists?(name)
+        @menu_items.any? {|node| node.name == name}
+      end
+
+      def find(name)
+        @menu_items.find {|node| node.name == name}
+      end
+
+      def position_of(name)
+        @menu_items.each do |node|
+          if node.name == name
+            return node.position
+          end
+        end
       end
     end
     
-    class MenuItem
+    class MenuItem < Tree::TreeNode
       include Redmine::I18n
-      attr_reader :name, :url, :param, :condition
+      attr_reader :name, :url, :param, :condition, :parent_menu
       
       def initialize(name, url, options)
-        raise "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call)
-        raise "Invalid option :html for menu item '#{name}'" if options[:html] && !options[:html].is_a?(Hash)
+        raise ArgumentError, "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call)
+        raise ArgumentError, "Invalid option :html for menu item '#{name}'" if options[:html] && !options[:html].is_a?(Hash)
+        raise ArgumentError, "Cannot set the :parent_menu to be the same as this item" if options[:parent_menu] == name.to_sym
         @name = name
         @url = url
         @condition = options[:if]
@@ -189,6 +350,8 @@ module Redmine
         @html_options = options[:html] || {}
         # Adds a unique class to each menu item based on its name
         @html_options[:class] = [@html_options[:class], @name.to_s.dasherize].compact.join(' ')
+        @parent_menu = options[:parent_menu]
+        super @name.to_sym
       end
       
       def caption(project=nil)
diff --git a/test/unit/lib/redmine/menu_manager/mapper_test.rb b/test/unit/lib/redmine/menu_manager/mapper_test.rb
new file mode 100644
index 0000000..304ece6
--- /dev/null
+++ b/test/unit/lib/redmine/menu_manager/mapper_test.rb
@@ -0,0 +1,166 @@
+# Redmine - project management software
+# Copyright (C) 2006-2009  Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../../../../test_helper'
+
+class Redmine::MenuManager::MapperTest < Test::Unit::TestCase
+  context "Mapper#initialize" do
+    should "be tested"
+  end
+
+  def test_push_onto_root
+    menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+    menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
+
+    menu_mapper.exists?(:test_overview)
+  end
+
+  def test_push_onto_parent
+    menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+    menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
+    menu_mapper.push :test_child, { :controller => 'projects', :action => 'show'}, {:parent_menu => :test_overview}
+
+    assert menu_mapper.exists?(:test_child)
+    assert_equal :test_child, menu_mapper.find(:test_child).name
+  end
+
+  def test_push_onto_grandparent
+    menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+    menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
+    menu_mapper.push :test_child, { :controller => 'projects', :action => 'show'}, {:parent_menu => :test_overview}
+    menu_mapper.push :test_grandchild, { :controller => 'projects', :action => 'show'}, {:parent_menu => :test_child}
+
+    assert menu_mapper.exists?(:test_grandchild)
+    grandchild = menu_mapper.find(:test_grandchild)
+    assert_equal :test_grandchild, grandchild.name
+    assert_equal :test_child, grandchild.parent_menu
+  end
+
+  def test_push_first
+    menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+    menu_mapper.push :test_second, { :controller => 'projects', :action => 'show'}, {}
+    menu_mapper.push :test_third, { :controller => 'projects', :action => 'show'}, {}
+    menu_mapper.push :test_fourth, { :controller => 'projects', :action => 'show'}, {}
+    menu_mapper.push :test_fifth, { :controller => 'projects', :action => 'show'}, {}
+    menu_mapper.push :test_first, { :controller => 'projects', :action => 'show'}, {:first => true}
+
+    root = menu_mapper.find(:root)
+    assert_equal 5, root.children.size
+    {0 => :test_first, 1 => :test_second, 2 => :test_third, 3 => :test_fourth, 4 => :test_fifth}.each do |position, name|
+      assert_not_nil root.children[position]
+      assert_equal name, root.children[position].name
+    end
+  
+  end
+  
+  def test_push_before
+    menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+    menu_mapper.push :test_first, { :controller => 'projects', :action => 'show'}, {}
+    menu_mapper.push :test_second, { :controller => 'projects', :action => 'show'}, {}
+    menu_mapper.push :test_fourth, { :controller => 'projects', :action => 'show'}, {}
+    menu_mapper.push :test_fifth, { :controller => 'projects', :action => 'show'}, {}
+    menu_mapper.push :test_third, { :controller => 'projects', :action => 'show'}, {:before => :test_fourth}
+
+    root = menu_mapper.find(:root)
+    assert_equal 5, root.children.size
+    {0 => :test_first, 1 => :test_second, 2 => :test_third, 3 => :test_fourth, 4 => :test_fifth}.each do |position, name|
+      assert_not_nil root.children[position]
+      assert_equal name, root.children[position].name
+    end
+  
+  end
+
+  def test_push_after
+    menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+    menu_mapper.push :test_first, { :controller => 'projects', :action => 'show'}, {}
+    menu_mapper.push :test_second, { :controller => 'projects', :action => 'show'}, {}
+    menu_mapper.push :test_third, { :controller => 'projects', :action => 'show'}, {}
+    menu_mapper.push :test_fifth, { :controller => 'projects', :action => 'show'}, {}
+    menu_mapper.push :test_fourth, { :controller => 'projects', :action => 'show'}, {:after => :test_third}
+
+    
+    root = menu_mapper.find(:root)
+    assert_equal 5, root.children.size
+    {0 => :test_first, 1 => :test_second, 2 => :test_third, 3 => :test_fourth, 4 => :test_fifth}.each do |position, name|
+      assert_not_nil root.children[position]
+      assert_equal name, root.children[position].name
+    end
+  
+  end
+
+  def test_push_last
+    menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+    menu_mapper.push :test_first, { :controller => 'projects', :action => 'show'}, {}
+    menu_mapper.push :test_second, { :controller => 'projects', :action => 'show'}, {}
+    menu_mapper.push :test_third, { :controller => 'projects', :action => 'show'}, {}
+    menu_mapper.push :test_fifth, { :controller => 'projects', :action => 'show'}, {:last => true}
+    menu_mapper.push :test_fourth, { :controller => 'projects', :action => 'show'}, {}
+
+    root = menu_mapper.find(:root)
+    assert_equal 5, root.children.size
+    {0 => :test_first, 1 => :test_second, 2 => :test_third, 3 => :test_fourth, 4 => :test_fifth}.each do |position, name|
+      assert_not_nil root.children[position]
+      assert_equal name, root.children[position].name
+    end
+  
+  end
+  
+  def test_exists_for_child_node
+    menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+    menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
+    menu_mapper.push :test_child, { :controller => 'projects', :action => 'show'}, {:parent_menu => :test_overview }
+
+    assert menu_mapper.exists?(:test_child)
+  end
+
+  def test_exists_for_invalid_node
+    menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+    menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
+
+    assert !menu_mapper.exists?(:nothing)
+  end
+
+  def test_find
+    menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+    menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
+
+    item = menu_mapper.find(:test_overview)
+    assert_equal :test_overview, item.name
+    assert_equal({:controller => 'projects', :action => 'show'}, item.url)
+  end
+
+  def test_find_missing
+    menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+    menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
+
+    item = menu_mapper.find(:nothing)
+    assert_equal nil, item
+  end
+
+  def test_delete
+    menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+    menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
+    assert_not_nil menu_mapper.delete(:test_overview)
+
+    assert_nil menu_mapper.find(:test_overview)
+  end
+
+  def test_delete_missing
+    menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
+    assert_nil menu_mapper.delete(:test_missing)
+  end
+end
diff --git a/test/unit/lib/redmine/menu_manager/menu_helper_test.rb b/test/unit/lib/redmine/menu_manager/menu_helper_test.rb
new file mode 100644
index 0000000..6f259f4
--- /dev/null
+++ b/test/unit/lib/redmine/menu_manager/menu_helper_test.rb
@@ -0,0 +1,161 @@
+# Redmine - project management software
+# Copyright (C) 2006-2009  Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../../../../test_helper'
+
+
+
+class Redmine::MenuManager::MenuHelperTest < HelperTestCase
+  include Redmine::MenuManager::MenuHelper
+  include ActionController::Assertions::SelectorAssertions
+  fixtures :users, :members, :projects, :enabled_modules
+
+  # Used by assert_select
+  def html_document
+    HTML::Document.new(@response.body)
+  end
+  
+  def setup
+    super
+    @response = ActionController::TestResponse.new
+    # Stub the current menu item in the controller
+    def @controller.current_menu_item
+      :index
+    end
+  end
+  
+
+  context "MenuManager#current_menu_item" do
+    should "be tested"
+  end
+
+  context "MenuManager#render_main_menu" do
+    should "be tested"
+  end
+
+  context "MenuManager#render_menu" do
+    should "be tested"
+  end
+
+  context "MenuManager#menu_item_and_children" do
+    should "be tested"
+  end
+
+  context "MenuManager#extract_node_details" do
+    should "be tested"
+  end
+
+  def test_render_single_menu_node
+    node = Redmine::MenuManager::MenuItem.new(:testing, '/test', { })
+    @response.body = render_single_menu_node(node, 'This is a test', node.url, false)
+
+    assert_select("a.testing", "This is a test")
+  end
+
+  def test_render_menu_node
+    single_node = Redmine::MenuManager::MenuItem.new(:single_node, '/test', { })
+    @response.body = render_menu_node(single_node, nil)
+
+    assert_select("li") do
+      assert_select("a.single-node", "Single node")
+    end
+  end
+  
+  def test_render_menu_node_with_nested_items
+    parent_node = Redmine::MenuManager::MenuItem.new(:parent_node, '/test', { })
+    parent_node << Redmine::MenuManager::MenuItem.new(:child_one_node, '/test', { })
+    parent_node << Redmine::MenuManager::MenuItem.new(:child_two_node, '/test', { })
+    parent_node <<
+      Redmine::MenuManager::MenuItem.new(:child_three_node, '/test', { }) <<
+      Redmine::MenuManager::MenuItem.new(:child_three_inner_node, '/test', { })
+
+    @response.body = render_menu_node(parent_node, nil)
+
+    assert_select("li") do
+      assert_select("a.parent-node", "Parent node")
+      assert_select("ul") do
+        assert_select("li a.child-one-node", "Child one node")
+        assert_select("li a.child-two-node", "Child two node")
+        assert_select("li") do
+          assert_select("a.child-three-node", "Child three node")
+          assert_select("ul") do
+            assert_select("li a.child-three-inner-node", "Child three inner node")
+          end
+        end
+      end
+    end
+    
+  end
+
+  def test_menu_items_for_should_yield_all_items_if_passed_a_block
+    menu_name = :test_menu_items_for_should_yield_all_items_if_passed_a_block
+    Redmine::MenuManager.map menu_name do |menu|
+      menu.push(:a_menu, '/', { })
+      menu.push(:a_menu_2, '/', { })
+      menu.push(:a_menu_3, '/', { })
+    end
+
+    items_yielded = []
+    menu_items_for(menu_name) do |item|
+      items_yielded << item
+    end
+    
+    assert_equal 3, items_yielded.size
+  end
+
+  def test_menu_items_for_should_return_all_items
+    menu_name = :test_menu_items_for_should_return_all_items
+    Redmine::MenuManager.map menu_name do |menu|
+      menu.push(:a_menu, '/', { })
+      menu.push(:a_menu_2, '/', { })
+      menu.push(:a_menu_3, '/', { })
+    end
+
+    items = menu_items_for(menu_name)
+    assert_equal 3, items.size
+  end
+
+  def test_menu_items_for_should_skip_unallowed_items_on_a_project
+    menu_name = :test_menu_items_for_should_skip_unallowed_items_on_a_project
+    Redmine::MenuManager.map menu_name do |menu|
+      menu.push(:a_menu, {:controller => 'issues', :action => 'index' }, { })
+      menu.push(:a_menu_2, {:controller => 'issues', :action => 'index' }, { })
+      menu.push(:unallowed, {:controller => 'issues', :action => 'unallowed' }, { })
+    end
+
+    User.current = User.find(2)
+    
+    items = menu_items_for(menu_name, Project.find(1))
+    assert_equal 2, items.size
+  end
+  
+  def test_menu_items_for_should_skip_items_that_fail_the_conditions
+    menu_name = :test_menu_items_for_should_skip_items_that_fail_the_conditions
+    Redmine::MenuManager.map menu_name do |menu|
+      menu.push(:a_menu, {:controller => 'issues', :action => 'index' }, { })
+      menu.push(:unallowed,
+                {:controller => 'issues', :action => 'index' },
+                { :if => Proc.new { false }})
+    end
+
+    User.current = User.find(2)
+    
+    items = menu_items_for(menu_name, Project.find(1))
+    assert_equal 1, items.size
+  end
+
+end
diff --git a/test/unit/lib/redmine/menu_manager/menu_item_test.rb b/test/unit/lib/redmine/menu_manager/menu_item_test.rb
new file mode 100644
index 0000000..ee302fc
--- /dev/null
+++ b/test/unit/lib/redmine/menu_manager/menu_item_test.rb
@@ -0,0 +1,108 @@
+# Redmine - project management software
+# Copyright (C) 2006-2009  Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../../../../test_helper'
+
+module RedmineMenuTestHelper
+  # Helpers
+  def get_menu_item(menu_name, item_name)
+    Redmine::MenuManager.items(menu_name).find {|item| item.name == item_name.to_sym}
+  end
+end
+
+class Redmine::MenuManager::MenuItemTest < Test::Unit::TestCase
+  include RedmineMenuTestHelper
+
+  Redmine::MenuManager.map :test_menu do |menu|
+    menu.push(:parent_menu, '/test', { })
+    menu.push(:child_menu, '/test', { :parent_menu => :parent_menu})
+    menu.push(:child2_menu, '/test', { :parent_menu => :parent_menu})
+  end
+  
+  context "MenuItem#caption" do
+    should "be tested"
+  end
+
+  context "MenuItem#html_options" do
+    should "be tested"
+  end
+
+  # context new menu item
+  def test_new_menu_item_should_require_a_name
+    assert_raises ArgumentError do
+      Redmine::MenuManager::MenuItem.new
+    end
+  end
+
+  def test_new_menu_item_should_require_an_url
+    assert_raises ArgumentError do
+      Redmine::MenuManager::MenuItem.new(:test_missing_url)
+    end
+  end
+
+  def test_new_menu_item_should_require_the_options
+    assert_raises ArgumentError do
+      Redmine::MenuManager::MenuItem.new(:test_missing_options, '/test')
+    end
+  end
+
+  def test_new_menu_item_with_all_required_parameters
+    assert Redmine::MenuManager::MenuItem.new(:test_good_menu, '/test', {})
+  end
+
+  def test_new_menu_item_should_require_a_proc_to_use_for_the_if_condition
+    assert_raises ArgumentError do
+      Redmine::MenuManager::MenuItem.new(:test_error, '/test',
+                                         {
+                                           :if => ['not_a_proc']
+                                         })
+    end
+
+    assert Redmine::MenuManager::MenuItem.new(:test_good_if, '/test',
+                                              {
+                                                :if => Proc.new{}
+                                              })
+  end
+
+  def test_new_menu_item_should_allow_a_hash_for_extra_html_options
+    assert_raises ArgumentError do
+      Redmine::MenuManager::MenuItem.new(:test_error, '/test',
+                                         {
+                                           :html => ['not_a_hash']
+                                         })
+    end
+
+    assert Redmine::MenuManager::MenuItem.new(:test_good_html, '/test',
+                                              {
+                                                :html => { :onclick => 'doSomething'}
+                                              })
+  end
+
+  def test_new_should_not_allow_setting_the_parent_menu_item_to_the_current_item
+    assert_raises ArgumentError do
+      Redmine::MenuManager::MenuItem.new(:test_error, '/test', { :parent_menu => :test_error })
+    end
+  end
+
+  def test_has_children
+    parent_item = get_menu_item(:test_menu, :parent_menu)
+    assert parent_item.hasChildren?
+    assert_equal 2, parent_item.children.size
+    assert_equal get_menu_item(:test_menu, :child_menu), parent_item.children[0]
+    assert_equal get_menu_item(:test_menu, :child2_menu), parent_item.children[1]
+  end
+end
diff --git a/test/unit/lib/redmine/menu_manager_test.rb b/test/unit/lib/redmine/menu_manager_test.rb
new file mode 100644
index 0000000..8c6ecda
--- /dev/null
+++ b/test/unit/lib/redmine/menu_manager_test.rb
@@ -0,0 +1,28 @@
+# Redmine - project management software
+# Copyright (C) 2006-2009  Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../../../test_helper'
+
+class Redmine::MenuManagerTest < Test::Unit::TestCase
+  context "MenuManager#map" do
+    should "be tested"
+  end
+
+  context "MenuManager#items" do
+    should "be tested"
+  end
+end
diff --git a/test/unit/lib/redmine_test.rb b/test/unit/lib/redmine_test.rb
new file mode 100644
index 0000000..5150da1
--- /dev/null
+++ b/test/unit/lib/redmine_test.rb
@@ -0,0 +1,84 @@
+# Redmine - project management software
+# Copyright (C) 2006-2009  Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../../test_helper'
+
+module RedmineMenuTestHelper
+  # Assertions
+  def assert_number_of_items_in_menu(menu_name, count)
+    assert Redmine::MenuManager.items(menu_name).size >= count, "Menu has less than #{count} items"
+  end
+
+  def assert_menu_contains_item_named(menu_name, item_name)
+    assert Redmine::MenuManager.items(menu_name).collect(&:name).include?(item_name.to_sym), "Menu did not have an item named #{item_name}"
+  end
+
+  # Helpers
+  def get_menu_item(menu_name, item_name)
+    Redmine::MenuManager.items(menu_name).find {|item| item.name == item_name.to_sym}
+  end
+end
+
+class RedmineTest < Test::Unit::TestCase
+  include RedmineMenuTestHelper
+
+  def test_top_menu
+    assert_number_of_items_in_menu :top_menu, 5
+    assert_menu_contains_item_named :top_menu, :home
+    assert_menu_contains_item_named :top_menu, :my_page
+    assert_menu_contains_item_named :top_menu, :projects
+    assert_menu_contains_item_named :top_menu, :administration
+    assert_menu_contains_item_named :top_menu, :help
+  end
+
+  def test_account_menu
+    assert_number_of_items_in_menu :account_menu, 4
+    assert_menu_contains_item_named :account_menu, :login
+    assert_menu_contains_item_named :account_menu, :register
+    assert_menu_contains_item_named :account_menu, :my_account
+    assert_menu_contains_item_named :account_menu, :logout
+  end
+
+  def test_application_menu
+    assert_number_of_items_in_menu :application_menu, 0
+  end
+
+  def test_admin_menu
+    assert_number_of_items_in_menu :admin_menu, 0
+  end
+
+  def test_project_menu
+    assert_number_of_items_in_menu :project_menu, 12
+    assert_menu_contains_item_named :project_menu, :overview
+    assert_menu_contains_item_named :project_menu, :activity
+    assert_menu_contains_item_named :project_menu, :roadmap
+    assert_menu_contains_item_named :project_menu, :issues
+    assert_menu_contains_item_named :project_menu, :new_issue
+    assert_menu_contains_item_named :project_menu, :news
+    assert_menu_contains_item_named :project_menu, :documents
+    assert_menu_contains_item_named :project_menu, :wiki
+    assert_menu_contains_item_named :project_menu, :boards
+    assert_menu_contains_item_named :project_menu, :files
+    assert_menu_contains_item_named :project_menu, :repository
+    assert_menu_contains_item_named :project_menu, :settings
+  end
+
+  def test_new_issue_should_have_root_as_a_parent
+    new_issue = get_menu_item(:project_menu, :new_issue)
+    assert_equal :root, new_issue.parent.name
+  end
+end
-- 
1.6.5

