Browse Source

Init version of datetime field.

Martin Edenhofer 10 years ago
parent
commit
eaf2c8334a

+ 239 - 138
app/assets/javascripts/app/controllers/_application_controller_form.js.coffee

@@ -95,7 +95,7 @@ class App.ControllerForm extends App.Controller
       for eventSelector, callback of @events
         do (eventSelector, callback) =>
           evs = eventSelector.split(' ')
-          fieldset.find( evs[1] ).bind(evs[0], (e) => callback(e) )
+          fieldset.find( evs[1] ).bind( evs[0], (e) => callback(e) )
 
     # return form
     return fieldset
@@ -264,13 +264,13 @@ class App.ControllerForm extends App.Controller
           { name: 'inactive', value: false }
         ]
 
-      # update boolean types
-      for record in attribute.options
-        record.value = '{boolean}::' + record.value
+      # set data type
+      if attribute.name
+        attribute.name = '{boolean}' + attribute.name
 
       # finde selected item of list
       for record in attribute.options
-        if record.value is '{boolean}::' + attribute.value
+        if record.value is attribute.value
           record.selected = 'selected'
 
       # return item
@@ -283,7 +283,6 @@ class App.ControllerForm extends App.Controller
     # date
     else if attribute.tag is 'date'
       attribute.type = 'text'
-      item = $( App.view('generic/date')( attribute: attribute ) )
       #item.datetimepicker({
       #  format: 'Y.m.d'
       #});
@@ -291,10 +290,163 @@ class App.ControllerForm extends App.Controller
     # date
     else if attribute.tag is 'datetime'
       attribute.type = 'text'
-      item = $( App.view('generic/date')( attribute: attribute ) )
-      #item.datetimepicker({
-      #  format: 'Y.m.d H:i'
-      #});
+
+      # set data type
+      if attribute.name
+        attribute.nameRaw = attribute.name
+        attribute.name    = '{datetime}' + attribute.name
+      if attribute.value
+        if typeof( attribute.value ) is 'string'
+          unixtime = new Date( Date.parse( attribute.value ) )
+        else
+          unixtime = new Date( attribute.value )
+        year     = unixtime.getYear() + 1900
+        month    = unixtime.getMonth() + 1
+        day      = unixtime.getDate()
+        hour     = unixtime.getHours()
+        minute   = unixtime.getMinutes()
+      item = $( App.view('generic/datetime')(
+        attribute: attribute
+        year:      year
+        month:     month
+        day:       day
+        hour:      hour
+        minute:    minute
+      ) )
+      item.find('.js-today').bind('click', (e) ->
+        e.preventDefault()
+        name = $(@).closest('.form-group').find('[data-name]').attr('data-name')
+        today = new Date();
+        item.closest('.form-group').find("[name=\"{datetime}#{name}___day\"]").val( today.getDate() )
+        item.closest('.form-group').find("[name=\"{datetime}#{name}___month\"]").val( today.getMonth()+1 )
+        item.closest('.form-group').find("[name=\"{datetime}#{name}___year\"]").val( today.getFullYear() )
+        item.closest('.form-group').find("[name=\"{datetime}#{name}___hour\"]").val( today.getHours() )
+        item.closest('.form-group').find("[name=\"{datetime}#{name}___minute\"]").val( today.getMinutes() )
+      )
+
+      setNewTime = (diff, el) ->
+        name = $(el).closest('.form-group').find('[data-name]').attr('data-name')
+
+        day    = item.closest('.form-group').find("[name=\"{datetime}#{name}___day\"]").val()
+        month  = item.closest('.form-group').find("[name=\"{datetime}#{name}___month\"]").val()
+        year   = item.closest('.form-group').find("[name=\"{datetime}#{name}___year\"]").val()
+        hour   = item.closest('.form-group').find("[name=\"{datetime}#{name}___hour\"]").val()
+        minute = item.closest('.form-group').find("[name=\"{datetime}#{name}___minute\"]").val()
+        format = (number) ->
+          if parseInt(number) < 10
+            number = "0#{number}"
+          number
+        #console.log('ph', diff, "#{year}-#{format(month)}-#{format(day)}T#{format(hour)}:#{format(minute)}:00Z")
+        time = new Date( Date.parse( "#{year}-#{format(month)}-#{format(day)}T#{format(hour)}:#{format(minute)}:00Z" ) )
+        time.setMinutes( time.getMinutes() + diff + time.getTimezoneOffset() )
+        #console.log('T', time, time.getHours(), time.getMinutes())
+        item.closest('.form-group').find("[name=\"{datetime}#{name}___day\"]").val( time.getDate() )
+        item.closest('.form-group').find("[name=\"{datetime}#{name}___month\"]").val( time.getMonth()+1 )
+        item.closest('.form-group').find("[name=\"{datetime}#{name}___year\"]").val( time.getFullYear() )
+        item.closest('.form-group').find("[name=\"{datetime}#{name}___hour\"]").val( time.getHours() )
+        item.closest('.form-group').find("[name=\"{datetime}#{name}___minute\"]").val( time.getMinutes() )
+
+      item.find('.js-plus-hour').bind('click', (e) ->
+        e.preventDefault()
+        setNewTime(60, @)
+      )
+      item.find('.js-minus-hour').bind('click', (e) ->
+        e.preventDefault()
+        setNewTime(-60, @)
+      )
+      item.find('.js-plus-day').bind('click', (e) ->
+        e.preventDefault()
+        setNewTime(60 * 24, @)
+      )
+      item.find('.js-minus-day').bind('click', (e) ->
+        e.preventDefault()
+        setNewTime(-60 * 24, @)
+      )
+      item.find('.js-plus-week').bind('click', (e) ->
+        e.preventDefault()
+        setNewTime(60 * 24 * 7, @)
+      )
+      item.find('.js-minus-week').bind('click', (e) ->
+        e.preventDefault()
+        setNewTime(-60 * 24 * 7, @)
+      )
+
+      item.find('input').bind('keyup blur focus', (e) ->
+
+        # do validation
+        name = $(@).attr('name')
+        if name
+          fieldPrefix = name.split('___')[0]
+
+        # remove old validation
+        item.find('.has-error').removeClass('has-error')
+        item.closest('.form-group').find('.help-inline').html('')
+
+        day    = item.closest('.form-group').find("[name=\"#{fieldPrefix}___day\"]").val()
+        month  = item.closest('.form-group').find("[name=\"#{fieldPrefix}___month\"]").val()
+        year   = item.closest('.form-group').find("[name=\"#{fieldPrefix}___year\"]").val()
+        hour   = item.closest('.form-group').find("[name=\"#{fieldPrefix}___hour\"]").val()
+        minute = item.closest('.form-group').find("[name=\"#{fieldPrefix}___minute\"]").val()
+
+        # validate exists
+        errors = {}
+        if !day
+          errors.day = 'missing'
+        if !month
+          errors.month = 'missing'
+        if !year
+          errors.year = 'missing'
+        if !hour
+          errors.hour = 'missing'
+        if !minute
+          errors.minute = 'missing'
+
+        # ranges
+        if day
+          daysInMonth = 31
+          if month && year
+            daysInMonth = new Date(year, month, 0).getDate();
+          console.log('222', month, year, daysInMonth)
+
+          if parseInt(day).toString() is 'NaN'
+            errors.day = 'invalid'
+          else if parseInt(day) > daysInMonth || parseInt(day) < 1
+            errors.day = 'invalid'
+
+        if month
+          if parseInt(month).toString() is 'NaN'
+            errors.month = 'invalid'
+          else if parseInt(month) > 12 || parseInt(month) < 1
+            errors.month = 'invalid'
+
+        if year
+          if parseInt(year).toString() is 'NaN'
+            errors.year = 'invalid'
+          else if parseInt(year) > 2100 || parseInt(year) < 2001
+            errors.year = 'invalid'
+
+        if hour
+          if parseInt(hour).toString() is 'NaN'
+            errors.hour = 'invalid'
+          else if parseInt(hour) > 23 || parseInt(hour) < 0
+            errors.hour = 'invalid'
+
+        if minute
+          if parseInt(minute).toString() is 'NaN'
+            errors.minute = 'invalid'
+          else if parseInt(minute) > 59
+            errors.minute = 'invalid'
+
+        if !_.isEmpty(errors)
+          for key, value of errors
+            item.closest('.form-group').addClass('has-error')
+            item.closest('.form-group').find("[name=\"#{fieldPrefix}___#{key}\"]").addClass('has-error')
+            #item.closest('.form-group').find('.help-inline').text( value )
+
+          e.preventDefault()
+          e.stopPropagation()
+          return
+      )
 
     # timezone
     else if attribute.tag is 'timezone'
@@ -615,95 +767,6 @@ class App.ControllerForm extends App.Controller
             if listItem.value is "#{ attribute.name }::#{key}"
               addItem( "#{ attribute.name }::#{key}", listItem.name, item.find('.add a'), value )
 
-    # select
-    else if attribute.tag is 'input_select'
-      item = $('<div class="input_select"></div>')
-
-      # select shown attributes
-      loopData = {}
-      if @params && @params[ attribute.name ]
-        loopData = @params[ attribute.name ]
-      loopData[''] = ''
-
-      # show each attribote
-      counter = 0
-      for key of loopData
-        counter =+ 1
-
-        # clone to keep it untouched for next loop
-        select = _.clone( attribute )
-        input  = _.clone( attribute )
-
-        # set field ids - not needed in this case
-        select.id = ''
-        input.id  = ''
-
-        # rename to be able to identify this option later
-        select.name = '{input_select}::' + select.name
-        input.name  = '{input_select}::' + input.name
-
-        # set sub attributes
-        for keysub of attribute.select
-          select[keysub] = attribute.select[keysub]
-        for keysub of attribute.input
-          input[keysub] = attribute.input[keysub]
-
-        # set hide for + options
-        itemClass = ''
-        if key is ''
-          itemClass = 'hide'
-          select['nulloption'] = true
-
-        # set selected value
-        select.value = key
-        input.value  = loopData[ key ]
-
-        # build options list based on config
-        @_getConfigOptionList( select )
-
-        # build options list based on relation
-        @_getRelationOptionList( select )
-
-        # add null selection if needed
-        @_addNullOption( select )
-
-        # sort attribute.options
-        @_sortOptions( select )
-
-        # finde selected/checked item of list
-        @_selectedOptions( select )
-
-        pearItem = $("<div class=" + itemClass + "></div>")
-        pearItem.append $( App.view('generic/select')( attribute: select ) )
-        pearItem.append $( App.view('generic/input')( attribute: input ) )
-        itemRemote = $('<a href="#" class="input_select_remove icon-minus"></a>')
-        itemRemote.bind('click', (e) ->
-          e.preventDefault()
-          $(@).parent().remove()
-        )
-        pearItem.append( itemRemote )
-        item.append( pearItem )
-
-        if key is ''
-          itemAdd = $('<div class="add"><a href="#" class="icon-plus"></a></div>')
-          itemAdd.bind('click', (e) ->
-            e.preventDefault()
-
-            # copy
-            newElement = $(@).prev().clone()
-            newElement.removeClass('hide')
-
-            # bind on remove
-            newElement.find('.input_select_remove').bind('click', (e) ->
-              e.preventDefault()
-              $(@).parent().remove()
-            )
-
-            # prepend
-            $(@).parent().find('.add').before( newElement )
-          )
-          item.append( itemAdd )
-
     # checkbox
     else if attribute.tag is 'checkbox'
       item = $( App.view('generic/checkbox')( attribute: attribute ) )
@@ -1792,6 +1855,8 @@ class App.ControllerForm extends App.Controller
     for key in name
       el.find('[name="' + key + '"]').closest('.form-group').removeClass('hide')
       el.find('[name="' + key + '"]').removeClass('is-hidden')
+      el.find('[data-name="' + key + '"]').closest('.form-group').removeClass('hide')
+      el.find('[data-name="' + key + '"]').removeClass('is-hidden')
 
   _hide: (name, el = @el) ->
     if !_.isArray(name)
@@ -1799,6 +1864,8 @@ class App.ControllerForm extends App.Controller
     for key in name
       el.find('[name="' + key + '"]').closest('.form-group').addClass('hide')
       el.find('[name="' + key + '"]').addClass('is-hidden')
+      el.find('[data-name="' + key + '"]').closest('.form-group').addClass('hide')
+      el.find('[data-name="' + key + '"]').addClass('is-hidden')
 
   _mandantory: (name, el = @el) ->
     if !_.isArray(name)
@@ -1819,8 +1886,13 @@ class App.ControllerForm extends App.Controller
       if attribute.shown_if
         hit = false
         for refAttribute, refValue of attribute.shown_if
-          if params[refAttribute] && params[refAttribute].toString() is refValue.toString()
-            hit = true
+          if params[refAttribute]
+            if _.isArray( refValue )
+              for item in refValue
+                if params[refAttribute].toString() is item.toString()
+                  hit = true
+            else if params[refAttribute].toString() is refValue.toString()
+              hit = true
         if hit
           ui._show(attribute.name)
         else
@@ -1831,8 +1903,13 @@ class App.ControllerForm extends App.Controller
       if attribute.required_if
         hit = false
         for refAttribute, refValue of attribute.required_if
-          if params[refAttribute] && params[refAttribute].toString() is refValue.toString()
-            hit = true
+          if params[refAttribute]
+            if _.isArray( refValue )
+              for item in refValue
+                if params[refAttribute].toString() is item.toString()
+                  hit = true
+            else if params[refAttribute].toString() is refValue.toString()
+              hit = true
         if hit
           ui._mandantory(attribute.name)
         else
@@ -2066,33 +2143,70 @@ class App.ControllerForm extends App.Controller
     # get form elements
     array = lookupForm.serializeArray()
 
-    # 1:1 and boolean params
+    # array to names
     for key in array
 
       # check if item is-hidden and should not be used
       if lookupForm.find('[name="' + key.name + '"]').hasClass('is-hidden')
+        param[key.name] = undefined
         continue
 
-      # collect all other params
+      # collect all params, push it to an array if already exists
       if param[key.name]
         if typeof param[key.name] is 'string'
           param[key.name] = [ param[key.name], key.value]
         else
           param[key.name].push key.value
       else
-
-        # check boolean
-        attributeType = key.value.split '::'
-        if attributeType[0] is '{boolean}'
-          if attributeType[1] is 'true'
-            key.value = true
-          else
-            key.value = false
-#        else if attributeType[0] is '{boolean}'
-
         param[key.name] = key.value
 
-    # check :: fields
+    # data type conversion
+    for key of param
+
+      # get boolean
+      if key.substr(0,9) is '{boolean}'
+        newKey          = key.substr( 9, key.length )
+        param[ newKey ] = param[ key ]
+        delete param[ key ]
+        if param[ newKey ] && param[ newKey ].toString() is 'true'
+          param[ newKey ] = true
+        else
+          param[ newKey ] = false
+
+      # get {datetime}
+      else if key.substr(0,10) is '{datetime}'
+        newKey    = key.substr( 10, key.length )
+        namespace = newKey.split '___'
+
+        if !param[ namespace[0] ]
+          datetimeKey = "{datetime}#{namespace[0]}___"
+          year        = param[ "#{datetimeKey}year" ]
+          month       = param[ "#{datetimeKey}month" ]
+          day         = param[ "#{datetimeKey}day" ]
+          hour        = param[ "#{datetimeKey}hour" ]
+          minute      = param[ "#{datetimeKey}minute" ]
+          timezone    = (new Date()).getTimezoneOffset()/60
+          if year && month && day && hour && minute
+            format = (number) ->
+              if parseInt(number) < 10
+                number = "0#{number}"
+              number
+            try
+              time = new Date( Date.parse( "#{year}-#{format(month)}-#{format(day)}T#{format(hour)}:#{format(minute)}:00Z" ) )
+              time.setMinutes( time.getMinutes() + time.getTimezoneOffset() )
+              param[ namespace[0] ] = time.toISOString()
+            catch err
+              console.log('ERR', err)
+
+        #console.log('T', time, time.getHours(), time.getMinutes())
+
+          delete param[ "#{datetimeKey}year" ]
+          delete param[ "#{datetimeKey}month" ]
+          delete param[ "#{datetimeKey}day" ]
+          delete param[ "#{datetimeKey}hour" ]
+          delete param[ "#{datetimeKey}minute" ]
+
+    # split :: fields, build objects
     inputSelectObject = {}
     for key of param
       parts = key.split '::'
@@ -2109,25 +2223,7 @@ class App.ControllerForm extends App.Controller
         inputSelectObject[ parts[0] ][ parts[1] ][ parts[2] ] = param[ key ]
         delete param[ key ]
 
-    # check {input_select}
-    for key of param
-      attributeType = key.split '::'
-      name = attributeType[1]
-#      console.log 'split', key, attributeType, param[ name ]
-      if attributeType[0] is '{input_select}' && !param[ name ]
-
-        # array need to be converted
-        inputSelectData = param[ key ]
-        inputSelectObject[ name ] = {}
-        for x in [0..inputSelectData.length] by 2
-#          console.log 'for by 111', x, inputSelectData, inputSelectData[x], inputSelectData[ x + 1 ]
-          if inputSelectData[ x ]
-            inputSelectObject[ name ][ inputSelectData[x] ] = inputSelectData[ x + 1 ]
-
-        # remove {input_select} items
-        delete param[ key ]
-
-    # set new {input_select} items
+    # set new object params
     for key of inputSelectObject
       param[ key ] = inputSelectObject[ key ]
 
@@ -2203,8 +2299,13 @@ class App.ControllerForm extends App.Controller
 
     # show new errors
     for key, msg of data.errors
-      lookupForm.find('[name="' + key + '"]').parents('div .form-group').addClass('has-error')
-      lookupForm.find('[name="' + key + '"]').parent().find('.help-inline').html(msg)
+      item = lookupForm.find('[name="' + key + '"]').closest('.form-group')
+      item.addClass('has-error')
+      item.find('.help-inline').html(msg)
+
+      item = lookupForm.find('[data-name="' + key + '"]').closest('.form-group')
+      item.addClass('has-error')
+      item.find('.help-inline').html(msg)
 
     # set autofocus
-    lookupForm.find('.has-error').find('input, textarea').first().focus()
+    lookupForm.find('.has-error').find('input, textarea, select').first().focus()

+ 19 - 0
app/assets/javascripts/app/views/generic/datetime.jst.eco

@@ -0,0 +1,19 @@
+<div class="horizontal" data-name="<%= @attribute.nameRaw %>">
+  <input type="text" maxlength="2" name="<%= @attribute.name %>___day" value="<%= @day %>" placeholder="dd" style="width: 50px;">
+  .
+  <input type="text" maxlength="2" name="<%= @attribute.name %>___month" value="<%= @month %>" placeholder="mm" style="width: 50px;">
+  .
+  <input type="text" maxlength="4" name="<%= @attribute.name %>___year" value="<%= @year %>" placeholder="yyyy" style="width: 70px;">
+  <input type="text" maxlength="2" name="<%= @attribute.name %>___hour" value="<%= @hour %>" placeholder="00" style="width: 50px;">
+  :
+  <input type="text" maxlength="2" name="<%= @attribute.name %>___minute" value="<%= @minute %>" placeholder="00" style="width: 50px;">
+</div>
+<div>
+<a class="js-today"><%- @T('now') %></a>
+|
+<a class="js-minus-hour"><%- @T('-1') %></a> <a class="js-plus-hour"><%- @T('+1') %></a> <%- @T('hour') %>
+|
+<a class="js-minus-day"><%- @T('-1') %></a> <a class="js-plus-day"><%- @T('+1') %></a> <%- @T('day') %>
+|
+<a class="js-minus-week"><%- @T('-7') %></a> <a class="js-plus-week"><%- @T('+7') %></a> <%- @T('days') %>
+</div>

+ 5 - 0
app/assets/stylesheets/zammad.css.scss

@@ -757,6 +757,11 @@ textarea,
     border-color: red !important;
   }
 
+  input.has-error {
+    box-shadow: none;
+    border-color: red !important;
+  }
+
   .help-inline:not(:empty) {
     color: red;
     padding: 2px;

+ 1 - 43
app/controllers/tests_controller.rb

@@ -2,48 +2,6 @@
 
 class TestsController < ApplicationController
 
-  # GET /tests/core
-  def core
-    respond_to do |format|
-      format.html # index.html.erb
-    end
-  end
-
-  # GET /tests/ui
-  def ui
-    respond_to do |format|
-      format.html # index.html.erb
-    end
-  end
-
-  # GET /tests/from
-  def form
-    respond_to do |format|
-      format.html # index.html.erb
-    end
-  end
-
-  # GET /tests/from_extended
-  def form
-    respond_to do |format|
-      format.html # index.html.erb
-    end
-  end
-
-  # GET /tests/table
-  def table
-    respond_to do |format|
-      format.html # index.html.erb
-    end
-  end
-
-  # GET /tests/html_utils
-  def html_utils
-    respond_to do |format|
-      format.html # index.html.erb
-    end
-  end
-
   # GET /test/wait
   def wait
     sleep params[:sec].to_i
@@ -51,4 +9,4 @@ class TestsController < ApplicationController
     render :json => result
   end
 
-end
+end

+ 9 - 8
config/routes/test.rb

@@ -1,12 +1,13 @@
 Zammad::Application.routes.draw do
 
-  match '/tests-core',          :to => 'tests#core',          :via => :get
-  match '/tests-ui',            :to => 'tests#ui',            :via => :get
-  match '/tests-model',         :to => 'tests#model',         :via => :get
-  match '/tests-form',          :to => 'tests#form',          :via => :get
-  match '/tests-form-extended', :to => 'tests#form_extended', :via => :get
-  match '/tests-table',         :to => 'tests#table',         :via => :get
-  match '/tests-html-utils',    :to => 'tests#html_utils',    :via => :get
-  match '/tests/wait/:sec',     :to => 'tests#wait',          :via => :get
+  match '/tests-core',            :to => 'tests#core',            :via => :get
+  match '/tests-ui',              :to => 'tests#ui',              :via => :get
+  match '/tests-model',           :to => 'tests#model',           :via => :get
+  match '/tests-form',            :to => 'tests#form',            :via => :get
+  match '/tests-form-extended',   :to => 'tests#form_extended',   :via => :get
+  match '/tests-form-validation', :to => 'tests#form_validation', :via => :get
+  match '/tests-table',           :to => 'tests#table',           :via => :get
+  match '/tests-html-utils',      :to => 'tests#html_utils',      :via => :get
+  match '/tests/wait/:sec',       :to => 'tests#wait',            :via => :get
 
 end

+ 30 - 7
db/seeds.rb

@@ -1345,10 +1345,11 @@ Ticket::StateType.create_if_not_exists( :id => 7, :name => 'removed', :updated_b
 
 Ticket::State.create_if_not_exists( :id => 1, :name => 'new', :state_type_id => Ticket::StateType.where(:name => 'new').first.id )
 Ticket::State.create_if_not_exists( :id => 2, :name => 'open', :state_type_id => Ticket::StateType.where(:name => 'open').first.id )
-Ticket::State.create_if_not_exists( :id => 3, :name => 'pending', :state_type_id => Ticket::StateType.where(:name => 'pending reminder').first.id  )
+Ticket::State.create_if_not_exists( :id => 3, :name => 'pending reminder', :state_type_id => Ticket::StateType.where(:name => 'pending reminder').first.id  )
 Ticket::State.create_if_not_exists( :id => 4, :name => 'closed', :state_type_id  => Ticket::StateType.where(:name => 'closed').first.id  )
 Ticket::State.create_if_not_exists( :id => 5, :name => 'merged', :state_type_id  => Ticket::StateType.where(:name => 'merged').first.id  )
-Ticket::State.create_if_not_exists( :id => 6, :name => 'removed', :state_type_id  => Ticket::StateType.where(:name => 'removed').first.id  )
+Ticket::State.create_if_not_exists( :id => 6, :name => 'removed', :state_type_id  => Ticket::StateType.where(:name => 'removed').first.id, :active => false )
+Ticket::State.create_if_not_exists( :id => 7, :name => 'pending close', :state_type_id => Ticket::StateType.where(:name => 'pending action').first.id, :next_state_id => 5 )
 
 Ticket::Priority.create_if_not_exists( :name => '1 low' )
 Ticket::Priority.create_if_not_exists( :name => '2 normal' )
@@ -1427,8 +1428,9 @@ Overview.create_if_not_exists(
   :prio       => 1010,
   :role_id    => overview_role.id,
   :condition  => {
-    'tickets.state_id' => [3],
-    'tickets.owner_id' => 'current_user.id',
+    'tickets.state_id'     => [3],
+    'tickets.owner_id'     => 'current_user.id',
+    'tickets.pending_time' => { 'direction' => 'before', 'count'=> 1, 'area' => 'minute' },
   },
   :order => {
     :by        => 'created_at',
@@ -1483,13 +1485,34 @@ Overview.create_if_not_exists(
   },
 )
 
+Overview.create_if_not_exists(
+  :name       => 'All pending reached Tickets',
+  :link       => 'all_pending_reached',
+  :prio       => 1035,
+  :role_id    => overview_role.id,
+  :condition  => {
+    'tickets.state_id'     => [3],
+    'tickets.pending_time' => { 'direction' => 'before', 'count'=> 1, 'area' => 'minute' },
+  },
+  :order => {
+    :by        => 'created_at',
+    :direction => 'ASC',
+  },
+  :view => {
+    :d => [ 'title', 'customer', 'group', 'created_at' ],
+    :s => [ 'title', 'customer', 'group', 'created_at' ],
+    :m => [ 'number', 'title', 'customer', 'group', 'created_at' ],
+    :view_mode_default => 's',
+  },
+)
+
 Overview.create_if_not_exists(
   :name       => 'Escalated Tickets',
   :link       => 'all_escalated',
   :prio       => 1040,
   :role_id    => overview_role.id,
   :condition  => {
-    'tickets.escalation_time' =>{ 'direction' => 'before', 'count'=> 5, 'area' => 'minute' },
+    'tickets.escalation_time' => { 'direction' => 'before', 'count'=> 5, 'area' => 'minute' },
   },
   :order => {
     :by        => 'escalation_time',
@@ -1510,8 +1533,8 @@ Overview.create_if_not_exists(
   :prio       => 1000,
   :role_id    => overview_role.id,
   :condition  => {
-    'tickets.state_id' => [ 1,2,3,4,6 ],
-    'tickets.customer_id'     => 'current_user.id',
+    'tickets.state_id'    => [ 1,2,3,4,6 ],
+    'tickets.customer_id' => 'current_user.id',
   },
   :order => {
     :by        => 'created_at',

+ 149 - 0
public/assets/tests/form-validation.js

@@ -0,0 +1,149 @@
+test( "form validation check", function() {
+
+  $('#forms').append('<hr><h1>form params check</h1><form id="form1"></form>')
+
+  var el       = $('#form1')
+  var defaults = {}
+  var form     = new App.ControllerForm({
+    el:    el,
+    model: {
+      configure_attributes: [
+        { name: 'input1', display: 'Input1', tag: 'input', type: 'text', limit: 100, null: false },
+        { name: 'password1', display: 'Password1', tag: 'input', type: 'password', limit: 100, null: false },
+        { name: 'textarea1', display: 'Textarea1', tag: 'textarea', rows: 6, limit: 100, null: false, upload: true },
+        { name: 'select1', display: 'Select1', tag: 'select', null: false, nulloption: true, options: { true: 'internal', false: 'public' } },
+        { name: 'selectmulti1', display: 'SelectMulti1', tag: 'select', null: false, nulloption: true, multiple: true, options: { true: 'internal', false: 'public' } },
+        { name: 'autocompletion1', display: 'AutoCompletion1', tag: 'autocompletion', null: false, options: { true: 'internal', false: 'public' }, source: [ { label: "Choice1", value: "value1", id: "id1" }, { label: "Choice2", value: "value2", id: "id2" }, ], minLength: 1 },
+        { name: 'richtext1', display: 'Richtext1', tag: 'richtext', maxlength: 100, null: false, type: 'richtext', multiline: true, upload: true, default: defaults['richtext1']  },
+        { name: 'datetime1', display: 'Datetime1', tag: 'datetime', null: false, default: defaults['datetime1']  },
+        { name: 'active1', display: 'Active1',  tag: 'boolean', type: 'boolean', default: defaults['active1'], null: false },
+      ],
+    },
+    params: defaults,
+  });
+  equal( el.find('[name="input1"]').val(), '', 'check input1 value')
+  equal( el.find('[name="input1"]').prop('required'), true, 'check input1 required')
+//  equal( el.find('[name="input1"]').is(":focus"), true, 'check input1 focus')
+
+  equal( el.find('[name="password1"]').val(), '', 'check password1 value')
+  equal( el.find('[name="password1_confirm"]').val(), '', 'check password1 value')
+  equal( el.find('[name="password1"]').prop('required'), true, 'check password1 required')
+
+  equal( el.find('[name="textarea1"]').val(), '', 'check textarea1 value')
+  equal( el.find('[name="textarea1"]').prop('required'), true, 'check textarea1 required')
+
+  equal( el.find('[name="select1"]').val(), '', 'check select1 value')
+  equal( el.find('[name="select1"]').prop('required'), true, 'check select1 required')
+
+  equal( el.find('[name="selectmulti1"]').val(), null, 'check selectmulti1 value')
+  equal( el.find('[name="selectmulti1"]').prop('required'), true, 'check selectmulti1 required')
+
+  equal( el.find('[name="autocompletion1"]').val(), '', 'check autocompletion1 value')
+  equal( el.find('[name="autocompletion1"]').prop('required'), true, 'check autocompletion1 required')
+
+  equal( el.find('[data-name="richtext1"]').val(), '', 'check richtext1 value')
+  //equal( el.find('[data-name="richtext1"]').prop('required'), true, 'check richtext1 required')
+
+
+
+  params = App.ControllerForm.params( el )
+  errors = form.validate(params)
+
+  test_errors = {
+    input1:          "is required",
+    password1:       "is required",
+    textarea1:       "is required",
+    select1:         "is required",
+    selectmulti1:    "is required",
+    autocompletion1: "is required",
+    richtext1:       "is required",
+    datetime1:       "is required",
+  }
+  deepEqual( errors, test_errors, 'validation errors check' )
+
+  App.ControllerForm.validate( { errors: errors, form: el } )
+
+  equal( el.find('[name="input1"]').closest('.form-group').hasClass('has-error'), true, 'check input1 has-error')
+  equal( el.find('[name="input1"]').closest('.form-group').find('.help-inline').text(), 'is required', 'check input1 error message')
+
+  equal( el.find('[name="password1"]').closest('.form-group').hasClass('has-error'), true, 'check password1 has-error')
+  equal( el.find('[name="password1"]').closest('.form-group').find('.help-inline').text(), 'is required', 'check password1 error message')
+
+  equal( el.find('[name="textarea1"]').closest('.form-group').hasClass('has-error'), true, 'check textarea1 has-error')
+  equal( el.find('[name="textarea1"]').closest('.form-group').find('.help-inline').text(), 'is required', 'check textarea1 error message')
+
+  equal( el.find('[name="select1"]').closest('.form-group').hasClass('has-error'), true, 'check select1 has-error')
+  equal( el.find('[name="select1"]').closest('.form-group').find('.help-inline').text(), 'is required', 'check select1 error message')
+
+  equal( el.find('[name="selectmulti1"]').closest('.form-group').hasClass('has-error'), true, 'check selectmulti1 has-error')
+  equal( el.find('[name="selectmulti1"]').closest('.form-group').find('.help-inline').text(), 'is required', 'check selectmulti1 error message')
+
+  equal( el.find('[name="autocompletion1"]').closest('.form-group').hasClass('has-error'), true, 'check autocompletion1 has-error')
+  equal( el.find('[name="autocompletion1"]').closest('.form-group').find('.help-inline').text(), 'is required', 'check autocompletion1 error message')
+
+  equal( el.find('[data-name="richtext1"]').closest('.form-group').hasClass('has-error'), true, 'check richtext1 has-error')
+  equal( el.find('[data-name="richtext1"]').closest('.form-group').find('.help-inline').text(), 'is required', 'check richtext1 error message')
+
+  equal( el.find('[data-name="datetime1"]').closest('.form-group').hasClass('has-error'), true, 'check datetime1 has-error')
+  equal( el.find('[data-name="datetime1"]').closest('.form-group').find('.help-inline').text(), 'is required', 'check datetime1 error message')
+
+});
+
+test( "datetime validation check", function() {
+
+  $('#forms').append('<hr><h1>datetime validation check</h1><form id="form2"></form>')
+
+  var el       = $('#form2')
+  var defaults = {}
+  var form     = new App.ControllerForm({
+    el:    el,
+    model: {
+      configure_attributes: [
+        { name: 'datetime1', display: 'Datetime1', tag: 'datetime', null: false, default: defaults['datetime1']  },
+      ],
+    },
+    params: defaults,
+  });
+
+  params = App.ControllerForm.params( el )
+  errors = form.validate(params)
+  test_errors = {
+    datetime1: "is required",
+  }
+  deepEqual( errors, test_errors, 'validation errors check' )
+  App.ControllerForm.validate( { errors: errors, form: el } )
+
+  equal( el.find('[data-name="datetime1"]').closest('.form-group').hasClass('has-error'), true, 'check datetime1 has-error')
+  equal( el.find('[data-name="datetime1"]').closest('.form-group').find('.help-inline').text(), 'is required', 'check datetime1 error message')
+
+  el.find('[name="{datetime}datetime1___day"]').val('1')
+  el.find('[name="{datetime}datetime1___month"]').val('1')
+  el.find('[name="{datetime}datetime1___year"]').val('2015')
+  el.find('[name="{datetime}datetime1___hour"]').val('12')
+  el.find('[name="{datetime}datetime1___minute"]').val('42')
+  params = App.ControllerForm.params( el )
+  errors = form.validate(params)
+  test_errors = undefined
+//    datetime1: "invalid",
+//  }
+  deepEqual( errors, test_errors, 'validation errors check' )
+  App.ControllerForm.validate( { errors: errors, form: el } )
+  equal( el.find('[data-name="datetime1"]').closest('.form-group').hasClass('has-error'), false, 'check datetime1 has-error')
+  equal( el.find('[data-name="datetime1"]').closest('.form-group').find('.help-inline').text(), '', 'check datetime1 error message')
+
+  el.find('[name="{datetime}datetime1___day"]').val('47')
+  el.find('[name="{datetime}datetime1___month"]').val('1')
+  el.find('[name="{datetime}datetime1___year"]').val('2015')
+  el.find('[name="{datetime}datetime1___hour"]').val('12')
+  el.find('[name="{datetime}datetime1___minute"]').val('42')
+  params = App.ControllerForm.params( el )
+  errors = form.validate(params)
+  test_errors = {
+    datetime1: "is required",
+  }
+  deepEqual( errors, test_errors, 'validation errors check' )
+  App.ControllerForm.validate( { errors: errors, form: el } )
+  equal( el.find('[data-name="datetime1"]').closest('.form-group').hasClass('has-error'), true, 'check datetime1 has-error')
+  equal( el.find('[data-name="datetime1"]').closest('.form-group').find('.help-inline').text(), '', 'check datetime1 error message')
+
+});

+ 186 - 14
public/assets/tests/form.js

@@ -15,6 +15,7 @@ test( "form elements check", function() {
     selectmultioption1: false,
     selectmultioption2: [ false, true ],
     richtext2: 'lalu <l> lalu',
+    datetime1: Date.parse('2015-01-11T12:40:00Z'),
   }
   new App.ControllerForm({
     el:        el,
@@ -34,6 +35,8 @@ test( "form elements check", function() {
         { name: 'selectmultioption2', display: 'SelectMultiOption2', tag: 'select', null: false, multiple: true, options: [{ value: true, name: 'A' }, { value: 1, name: 'B'}, { value: false, name: 'C' }], default: defaults['selectmultioption2'] },
         { name: 'richtext1', display: 'Richtext1', tag: 'richtext', limit: 100, null: true, upload: true, default: defaults['richtext1']  },
         { name: 'richtext2', display: 'Richtext2', tag: 'richtext', limit: 100, null: true, upload: true, default: defaults['richtext2']  },
+        { name: 'datetime1', display: 'Datetime1', tag: 'datetime', null: true, default: defaults['datetime1']  },
+        { name: 'datetime2', display: 'Datetime2', tag: 'datetime', null: true, default: defaults['datetime2']  },
       ]
     },
     autofocus: true
@@ -115,6 +118,9 @@ test( "form params check", function() {
     richtext6: '<div>lalu <b>b</b> lalu</div>',
     richtext7: "<div>&nbsp;<div>&nbsp;\n</div>  \n</div>",
     richtext8: '<div>lalu <i>b</i> lalu</div>',
+    datetime1: new Date( Date.parse('2015-01-11T12:40:00Z') ),
+    active1: true,
+    active2: false,
   }
   new App.ControllerForm({
     el:        el,
@@ -142,6 +148,10 @@ test( "form params check", function() {
         { name: 'richtext6', display: 'Richtext6', tag: 'richtext', maxlength: 100, null: true, type: 'textonly', multiline: true, upload: true, default: defaults['richtext6']  },
         { name: 'richtext7', display: 'Richtext7', tag: 'richtext', maxlength: 100, null: true, type: 'textonly', multiline: false, default: defaults['richtext7']  },
         { name: 'richtext8', display: 'Richtext8', tag: 'richtext', maxlength: 100, null: true, type: 'textonly', multiline: false, default: defaults['richtext8']  },
+        { name: 'datetime1', display: 'Datetime1', tag: 'datetime', null: true, default: defaults['datetime1']  },
+        { name: 'datetime2', display: 'Datetime2', tag: 'datetime', null: true, default: defaults['datetime2']  },
+        { name: 'active1', display: 'Active1',  tag: 'boolean', type: 'boolean', default: defaults['active1'], null: false },
+        { name: 'active2', display: 'Active2',  tag: 'boolean', type: 'boolean', default: defaults['active2'], null: false },
       ],
     },
     params: defaults,
@@ -220,6 +230,9 @@ test( "form params check", function() {
     richtext6: '<div>lalu <b>b</b> lalu</div>',
     richtext7: '',
     richtext8: '<div>lalu <i>b</i> lalu</div>',
+    datetime1: '2015-01-11T12:40:00.000Z',
+    active1: true,
+    active2: false,
   }
   deepEqual( params, test_params, 'form param check' );
 
@@ -405,6 +418,7 @@ test( "form dependend fields check", function() {
   var test_params = {
     input1: "",
     input2: "some used default",
+    input3: undefined,
     select1: "false",
     select2: "false",
     selectmulti2: [ "true", "false" ],
@@ -416,6 +430,7 @@ test( "form dependend fields check", function() {
   params = App.ControllerForm.params( el )
   test_params = {
     input1: "",
+    input2: undefined,
     input3: "some used default",
     select1: "true",
     select2: "false",
@@ -425,6 +440,143 @@ test( "form dependend fields check", function() {
   deepEqual( params, test_params, 'form param check' );
 });
 
+test( "form handler check with and without fieldset", function() {
+//    deepEqual( item, test.value, 'group set/get tests' );
+
+// mix default and params -> check it -> add note
+// test auto completion
+// show/hide fields base on field values -> bind changed event
+// form validation
+// form params check
+
+// add signature only if form_state is empty
+  $('#forms').append('<hr><h1>form handler check with and without fieldset</h1><form id="form5"></form>')
+  var el = $('#form5')
+  var defaults = {
+    select1: 'a',
+    select2: '',
+  }
+
+  var formChanges = function(params, attribute, attributes, classname, form, ui) {
+    console.log('FROM', form)
+    if (params['select1'] === 'b') {
+      console.log('lala', params)
+      var item = {
+        name:    'select2',
+        display: 'Select2',
+        tag:     'select',
+        null:    true,
+        options: { 1:'1', 2:'2', 3:'3' },
+        default: 3,
+      };
+      var newElement = ui.formGenItem( item, classname, form )
+      form.find('[name="select2"]').closest('.form-group').replaceWith( newElement )
+    }
+    if (params['select1'] === 'a') {
+      console.log('lala', params)
+      var item = {
+        name:    'select2',
+        display: 'Select2',
+        tag:     'select',
+        null:    true,
+        options: { 1:'1', 2:'2', 3:'3' },
+        default: 1,
+      };
+      var newElement = ui.formGenItem( item, classname, form )
+      form.find('[name="select2"]').closest('.form-group').replaceWith( newElement )
+    }
+  }
+
+  new App.ControllerForm({
+    el:        el,
+    model:     {
+      configure_attributes: [
+        { name: 'select1', display: 'Select1', tag: 'select', null: true, options: { a: 'a', b: 'b' }, default: 'b'},
+        { name: 'select2', display: 'Select2', tag: 'select', null: true, options: { 1:'1', 2:'2', 3:'3' }, default: 2 },
+      ],
+    },
+    params: defaults,
+    handlers: [
+      formChanges,
+    ],
+    //noFieldset: true,
+  });
+  equal( el.find('[name="select1"]').val(), 'a', 'check select1 value')
+  equal( el.find('[name="select1"]').prop('required'), false, 'check select1 required')
+
+  equal( el.find('[name="select2"]').val(), '1', 'check select2 value')
+  equal( el.find('[name="select2"]').prop('required'), false, 'check select2 required')
+
+  var params = App.ControllerForm.params( el )
+  var test_params = {
+    select1: 'a',
+    select2: '1',
+  }
+  deepEqual( params, test_params, 'form param check' );
+  el.find('[name="select1"]').val('b')
+  el.find('[name="select1"]').trigger('change')
+  params = App.ControllerForm.params( el )
+  test_params = {
+    select1: 'b',
+    select2: '3',
+  }
+  deepEqual( params, test_params, 'form param check' );
+  el.find('[name="select1"]').val('a')
+  el.find('[name="select1"]').trigger('change')
+  params = App.ControllerForm.params( el )
+  test_params = {
+    select1: 'a',
+    select2: '1',
+  }
+  deepEqual( params, test_params, 'form param check' );
+
+  // test with noFieldset
+  el.empty()
+  new App.ControllerForm({
+    el:        el,
+    model:     {
+      configure_attributes: [
+        { name: 'select1', display: 'Select1', tag: 'select', null: true, options: { a: 'a', b: 'b' }, default: 'b'},
+        { name: 'select2', display: 'Select2', tag: 'select', null: true, options: { 1:'1', 2:'2', 3:'3' }, default: 2 },
+      ],
+    },
+    params: defaults,
+    handlers: [
+      formChanges,
+    ],
+    noFieldset: true,
+  });
+  equal( el.find('[name="select1"]').val(), 'a', 'check select1 value')
+  equal( el.find('[name="select1"]').prop('required'), false, 'check select1 required')
+
+  equal( el.find('[name="select2"]').val(), '1', 'check select2 value')
+  equal( el.find('[name="select2"]').prop('required'), false, 'check select2 required')
+
+  var params = App.ControllerForm.params( el )
+  var test_params = {
+    select1: 'a',
+    select2: '1',
+  }
+  deepEqual( params, test_params, 'form param check' );
+  el.find('[name="select1"]').val('b')
+  el.find('[name="select1"]').trigger('change')
+  params = App.ControllerForm.params( el )
+  test_params = {
+    select1: 'b',
+    select2: '3',
+  }
+  deepEqual( params, test_params, 'form param check' );
+  el.find('[name="select1"]').val('a')
+  el.find('[name="select1"]').trigger('change')
+  params = App.ControllerForm.params( el )
+  test_params = {
+    select1: 'a',
+    select2: '1',
+  }
+  deepEqual( params, test_params, 'form param check' );
+
+});
+
 test( "form postmaster filter", function() {
 
 // check match area
@@ -455,8 +607,8 @@ test( "form postmaster filter", function() {
     },
   ] )
 
-  $('#forms').append('<hr><h1>form postmaster filter</h1><form id="form5"></form>')
-  var el = $('#form5')
+  $('#forms').append('<hr><h1>form postmaster filter</h1><form id="form6"></form>')
+  var el = $('#form6')
   var defaults = {
     input2: 'some name',
     match: {
@@ -493,8 +645,8 @@ test( "form postmaster filter", function() {
     set: {
       'x-zammad-ticket-owner': 'owner',
       'x-zammad-ticket-customer': 'customer',
-      'x-zammad-ticket-priority_id': "2",
-      'x-zammad-ticket-group_id': "1",
+      'x-zammad-ticket-priority_id': '2',
+      'x-zammad-ticket-group_id': '1',
     },
   };
   deepEqual( params, test_params, 'form param check' );
@@ -512,7 +664,7 @@ test( "form postmaster filter", function() {
           },
           set: {
             'x-zammad-ticket-owner': 'owner',
-            'x-zammad-ticket-group_id': "1",
+            'x-zammad-ticket-group_id': '1',
           },
         };
         deepEqual( params, test_params, 'form param check' );
@@ -524,8 +676,8 @@ test( "form postmaster filter", function() {
 });
 
 test( "form selector", function() {
-  $('#forms').append('<hr><h1>form selector</h1><div><form id="form6"></form></div>')
-  var el = $('#form6')
+  $('#forms').append('<hr><h1>form selector</h1><div><form id="form7"></form></div>')
+  var el = $('#form7')
   var defaults = {
     input2: 'some name66',
   }
@@ -555,10 +707,12 @@ test( "form selector", function() {
 });
 
 test( "form required_if + shown_if", function() {
-  $('#forms').append('<hr><h1>form required_if + shown_if</h1><div><form id="form7"></form></div>')
-  var el = $('#form7')
+  $('#forms').append('<hr><h1>form required_if + shown_if</h1><div><form id="form8"></form></div>')
+  var el = $('#form8')
   var defaults = {
     input2: 'some name66',
+    input3: 'some name77',
+    input4: 'some name88',
   }
   new App.ControllerForm({
     el:        el,
@@ -566,6 +720,8 @@ test( "form required_if + shown_if", function() {
       configure_attributes: [
         { name: 'input1', display: 'Input1', tag: 'input', type: 'text', limit: 100, null: true, default: 'some not used default33' },
         { name: 'input2', display: 'Input2', tag: 'input', type: 'text', limit: 100, null: true, default: 'some used default', required_if: { active: true }, shown_if: { active: true } },
+        { name: 'input3', display: 'Input3', tag: 'input', type: 'text', limit: 100, null: true, default: 'some used default', required_if: { active: [true,false] }, shown_if: { active: [true,false] } },
+        { name: 'input4', display: 'Input4', tag: 'input', type: 'text', limit: 100, null: true, default: 'some used default', required_if: { active: [55,66] }, shown_if: { active: [55,66] } },
         { name: 'active', display: 'Active',  tag: 'boolean', type: 'boolean', 'default': true, null: false },
       ],
     },
@@ -574,33 +730,49 @@ test( "form required_if + shown_if", function() {
   test_params = {
     input1: "some not used default33",
     input2: "some name66",
-    active: true
+    input3: "some name77",
+    input4: undefined,
+    active: true,
   };
   params = App.ControllerForm.params( el )
   deepEqual( params, test_params, 'form param check via $("#form")' );
   equal( el.find('[name="input2"]').attr('required'), 'required', 'check required attribute of input2 ')
   equal( el.find('[name="input2"]').is(":visible"), true, 'check visible attribute of input2 ')
+  equal( el.find('[name="input3"]').attr('required'), 'required', 'check required attribute of input3 ')
+  equal( el.find('[name="input3"]').is(":visible"), true, 'check visible attribute of input3 ')
+  equal( el.find('[name="input4"]').is(":visible"), false, 'check visible attribute of input4 ')
+
 
-  el.find('[name="active"]').val('{boolean}::false').trigger('change')
+  el.find('[name="{boolean}active"]').val('false').trigger('change')
   test_params = {
     input1: "some not used default33",
-    active: false
+    input2: undefined,
+    input3: undefined,
+    input4: undefined,
+    active: false,
   };
   params = App.ControllerForm.params( el )
   deepEqual( params, test_params, 'form param check via $("#form")' );
   equal( el.find('[name="input2"]').attr('required'), undefined, 'check required attribute of input2 ')
   equal( el.find('[name="input2"]').is(":visible"), false, 'check visible attribute of input2 ')
+  equal( el.find('[name="input3"]').is(":visible"), false, 'check visible attribute of input3 ')
+  equal( el.find('[name="input4"]').is(":visible"), false, 'check visible attribute of input4 ')
 
 
-  el.find('[name="active"]').val('{boolean}::true').trigger('change')
+  el.find('[name="{boolean}active"]').val('true').trigger('change')
   test_params = {
     input1: "some not used default33",
     input2: "some name66",
-    active: true
+    input3: "some name77",
+    input4: undefined,
+    active: true,
   };
   params = App.ControllerForm.params( el )
   deepEqual( params, test_params, 'form param check via $("#form")' );
   equal( el.find('[name="input2"]').attr('required'), 'required', 'check required attribute of input2 ')
   equal( el.find('[name="input2"]').is(":visible"), true, 'check visible attribute of input2 ')
+  equal( el.find('[name="input3"]').attr('required'), 'required', 'check required attribute of input3 ')
+  equal( el.find('[name="input3"]').is(":visible"), true, 'check visible attribute of input3 ')
+  equal( el.find('[name="input4"]').is(":visible"), false, 'check visible attribute of input4 ')
 
 });

+ 22 - 0
test/browser/aab_unit_test.rb

@@ -112,6 +112,28 @@ class AAbUnitTest < TestCase
     ]
     browser_single_test(tests)
   end
+  def test_form_validation
+    tests = [
+      {
+        :name     => 'start',
+        :instance => browser_instance,
+        :url      => browser_url + '/tests-form-validation',
+        :action   => [
+          {
+            :execute => 'wait',
+            :value   => 8,
+          },
+          {
+            :execute      => 'match',
+            :css          => '.result .failed',
+            :value        => '0',
+            :match_result => true,
+          },
+        ],
+      },
+    ]
+    browser_single_test(tests)
+  end
   def test_table
     tests = [
       {