siesta 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,7 +3,7 @@ require_dependency "siesta/application_controller"
3
3
  module Siesta
4
4
  class SiestaController < ApplicationController
5
5
  def index
6
- render layout: nil
6
+ render :layout => nil
7
7
  end
8
8
  end
9
9
  end
@@ -10,7 +10,7 @@ module Siesta
10
10
 
11
11
  def include_test_harness
12
12
  if Siesta.config.auto_organizing
13
- content_tag(:script, test_harness, { type: 'text/javascript' }, false)
13
+ content_tag(:script, test_harness, { :type => 'text/javascript' }, false)
14
14
  else
15
15
  javascript_include_tag 'test_harness'
16
16
  end
@@ -25,7 +25,7 @@ module Siesta
25
25
  @suite ||= TestSuite.new(File.join(Rails.root, Siesta.config.spec_dir))
26
26
 
27
27
  groups = @suite.groups.inject([]) do |c, g|
28
- c << { group: g.name, items: g.items.map(&:url) }
28
+ c << { :group => g.name, :items => g.items.map(&:url) }
29
29
  end
30
30
 
31
31
  <<-SCRIPTS
@@ -1,3 +1,3 @@
1
1
  Siesta::Engine.routes.draw do
2
- resources :siesta
2
+ resources :siesta, :only => [:index]
3
3
  end
@@ -14,7 +14,7 @@ module Siesta
14
14
 
15
15
  def items
16
16
  @items ||= Dir.glob(File.join(path, '*.t.js')).inject([]) do |c, f|
17
- c << Item.new(path: f, group: self, suite: suite) unless File.directory?(f)
17
+ c << Item.new(:path => f, :group => self, :suite => suite) unless File.directory?(f)
18
18
 
19
19
  c
20
20
  end
@@ -41,12 +41,12 @@ module Siesta
41
41
 
42
42
  def groups
43
43
  @groups ||= Dir.glob(File.join(path, '**', '*')).inject([]) do |c, f|
44
- c << Group.new(path: f, suite: self) if File.directory?(f)
44
+ c << Group.new(:path => f, :suite => self) if File.directory?(f)
45
45
 
46
46
  c
47
47
  end
48
48
 
49
- @groups << Group.new(path: path, suite: self)
49
+ @groups << Group.new(:path => path, :suite => self)
50
50
  end
51
51
  end
52
52
  end
@@ -1,3 +1,3 @@
1
1
  module Siesta
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
@@ -1,6 +1,6 @@
1
1
  /*
2
2
 
3
- Siesta 1.1.0
3
+ Siesta 1.1.5
4
4
  Copyright(c) 2009-2012 Bryntum AB
5
5
  http://bryntum.com/contact
6
6
  http://bryntum.com/products/siesta/license
@@ -6114,7 +6114,9 @@ Class('Siesta.Content.Manager', {
6114
6114
  require : true
6115
6115
  },
6116
6116
 
6117
- urls : Joose.I.Object
6117
+ urls : Joose.I.Object,
6118
+
6119
+ maxLoads : 5
6118
6120
  },
6119
6121
 
6120
6122
 
@@ -6142,29 +6144,42 @@ Class('Siesta.Content.Manager', {
6142
6144
  var errorCount = 0
6143
6145
 
6144
6146
  var total = 0
6145
- Joose.O.each(urls, function () { total++ })
6147
+ var urlsArray = []
6148
+ Joose.O.each(urls, function (value, url) { total++; urlsArray.push(url) })
6146
6149
 
6147
- if (total)
6148
- Joose.O.each(urls, function (value, url) {
6150
+ if (total) {
6151
+
6152
+ var loadSingle = function (url) {
6153
+ if (!url) return
6149
6154
 
6150
6155
  me.load(url, function (content) {
6151
6156
  if (errorCount) return
6152
6157
 
6153
6158
  urls[ url ] = content
6154
6159
 
6155
- if (++loadCount == total) callback && callback()
6160
+ if (++loadCount == total)
6161
+ callback && callback()
6162
+ else
6163
+ loadSingle(urlsArray.shift())
6156
6164
 
6157
6165
  }, ignoreErrors ? function () {
6158
6166
 
6159
- if (++loadCount == total) callback && callback()
6167
+ if (++loadCount == total)
6168
+ callback && callback()
6169
+ else
6170
+ loadSingle(urlsArray.shift())
6160
6171
 
6161
6172
  } : function () {
6162
6173
  errorCount++
6163
6174
 
6164
6175
  errback && errback(url)
6165
6176
  })
6166
- })
6167
- else
6177
+ }
6178
+
6179
+ // running only `maxLoads` "loading threads" at the same time
6180
+ for (var i = 0; i < this.maxLoads; i++) loadSingle(urlsArray.shift())
6181
+
6182
+ } else
6168
6183
  callback && callback()
6169
6184
  },
6170
6185
 
@@ -6206,6 +6221,8 @@ Class('Siesta.Result.Diagnostic', {
6206
6221
  isa : Siesta.Result,
6207
6222
 
6208
6223
  has : {
6224
+ isWarning : false,
6225
+
6209
6226
  isSimulatedEvent : false,
6210
6227
 
6211
6228
  // Used by simulated events
@@ -6217,8 +6234,7 @@ Class('Siesta.Result.Diagnostic', {
6217
6234
  methods : {
6218
6235
 
6219
6236
  toString : function () {
6220
- var message = '# ' + this.description;
6221
- return message;
6237
+ return '# ' + this.description
6222
6238
  }
6223
6239
  }
6224
6240
  });
@@ -6400,6 +6416,7 @@ Role('Siesta.Test.More', {
6400
6416
  'onload',
6401
6417
  'onerror',
6402
6418
  'StartTest',
6419
+ 'startTest',
6403
6420
  // will be reported in IE8 after overriding
6404
6421
  'setTimeout',
6405
6422
  'clearTimeout'
@@ -6636,7 +6653,7 @@ Role('Siesta.Test.More', {
6636
6653
 
6637
6654
  desc = desc || 'throwsOk';
6638
6655
 
6639
- var e = this.global.StartTest.exceptionCatcher(func)
6656
+ var e = this.getExceptionCatcher()(func)
6640
6657
 
6641
6658
  // assuming no one will throw undefined exception..
6642
6659
  if (e === undefined) {
@@ -6648,7 +6665,7 @@ Role('Siesta.Test.More', {
6648
6665
  return
6649
6666
  }
6650
6667
 
6651
- if (e instanceof this.global.StartTest.testError)
6668
+ if (e instanceof this.getTestErrorClass())
6652
6669
  //IE uses non-standard 'description' property for error msg
6653
6670
  e = e.message || e.description
6654
6671
 
@@ -6703,7 +6720,7 @@ Role('Siesta.Test.More', {
6703
6720
  func = [ desc, desc = func][ 0 ]
6704
6721
  }
6705
6722
 
6706
- var e = this.global.StartTest.exceptionCatcher(func)
6723
+ var e = this.getExceptionCatcher()(func)
6707
6724
 
6708
6725
  if (e === undefined)
6709
6726
  this.pass(desc)
@@ -6892,9 +6909,9 @@ Role('Siesta.Test.More', {
6892
6909
  *
6893
6910
  * This method has a synonym with singular name: `expectGlobal`
6894
6911
  *
6895
- * @param {String} name1 The name of global property
6896
- * @param {String} name2 The name of global property
6897
- * @param {String} nameN The name of global property
6912
+ * @param {String/RegExp} name1 The name of global property or the regular expression to match several properties
6913
+ * @param {String/RegExp} name2 The name of global property or the regular expression to match several properties
6914
+ * @param {String/RegExp} nameN The name of global property or the regular expression to match several properties
6898
6915
  */
6899
6916
  expectGlobals : function () {
6900
6917
  this.expectedGlobals.push.apply(this.expectedGlobals, arguments)
@@ -6908,9 +6925,9 @@ Role('Siesta.Test.More', {
6908
6925
  *
6909
6926
  * You can enable this assertion to automatically happen at the end of each test, using {@link Siesta.Harness#autoCheckGlobals autoCheckGlobals} option of the harness.
6910
6927
  *
6911
- * @param {String} name1 The name of global property
6912
- * @param {String} name2 The name of global property
6913
- * @param {String} nameN The name of global property
6928
+ * @param {String/RegExp} name1 The name of global property or the regular expression to match several properties
6929
+ * @param {String/RegExp} name2 The name of global property or the regular expression to match several properties
6930
+ * @param {String/RegExp} nameN The name of global property or the regular expression to match several properties
6914
6931
  */
6915
6932
  verifyGlobals : function () {
6916
6933
  if (this.disableGlobalsCheck) {
@@ -6922,16 +6939,33 @@ Role('Siesta.Test.More', {
6922
6939
  this.expectGlobals.apply(this, arguments)
6923
6940
 
6924
6941
  var me = this
6925
- var expectedGlobals = {}
6926
- var failed = false
6942
+ var expectedStrings = {}
6943
+ var expectedRegExps = []
6927
6944
 
6928
- Joose.A.each(this.expectedGlobals.concat(this.browserGlobals), function (name) { expectedGlobals[ name ] = true })
6945
+ Joose.A.each(this.expectedGlobals.concat(this.browserGlobals), function (value) {
6946
+ if (me.typeOf(value) == 'RegExp')
6947
+ expectedRegExps.push(value)
6948
+ else
6949
+ expectedStrings[ value ] = true
6950
+ })
6929
6951
 
6930
6952
  this.diag('Global variables')
6931
6953
 
6954
+ var failed = false
6955
+
6932
6956
  for (var name in this.global) {
6957
+ if (expectedStrings[ name ]) continue
6933
6958
 
6934
- if (!expectedGlobals[ name ]) {
6959
+ var isExpected = false
6960
+
6961
+ for (var i = 0; i < expectedRegExps.length; i++) {
6962
+ if (expectedRegExps[ i ].test(name)) {
6963
+ isExpected = true
6964
+ break
6965
+ }
6966
+ }
6967
+
6968
+ if (!isExpected) {
6935
6969
  me.fail('Unexpected global found', 'Global name: ' + name + ', value: ' + Siesta.Util.Serializer.stringify(this.global[name]))
6936
6970
 
6937
6971
  failed = true
@@ -6968,7 +7002,7 @@ Role('Siesta.Test.More', {
6968
7002
  else
6969
7003
  this.fail(desc, annotation, result);
6970
7004
  },
6971
-
7005
+
6972
7006
 
6973
7007
  /**
6974
7008
  * Waits for passed checker method to return true (or any non-false value, like for example DOM element or array), and calls the callback when this happens.
@@ -6980,8 +7014,9 @@ Role('Siesta.Test.More', {
6980
7014
  * @param {Function} callback A function to call when the condition has been met. Will receive a result from checker function.
6981
7015
  * @param {Object} scope The scope for the callback
6982
7016
  * @param {Int} timeout The maximum amount of time (in milliseconds) to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value.
7017
+ * @param {Int} interval The polling interval (in milliseconds)
6983
7018
  */
6984
- waitFor : function (method, callback, scope, timeout) {
7019
+ waitFor : function (method, callback, scope, timeout, interval) {
6985
7020
  var description = this.typeOf(method) == 'Number' ? (method + ' ms') : ' condition to be fullfilled';
6986
7021
  var assertionName = 'waitFor';
6987
7022
 
@@ -6992,11 +7027,17 @@ Role('Siesta.Test.More', {
6992
7027
  callback = options.callback;
6993
7028
  scope = options.scope;
6994
7029
  timeout = options.timeout;
7030
+ interval = options.interval
7031
+
6995
7032
  description = options.description;
6996
7033
  assertionName = options.assertionName || assertionName;
7034
+
6997
7035
  }
6998
7036
 
6999
- var async = this.beginAsync(),
7037
+ interval = interval || this.waitForPollInterval
7038
+ timeout = timeout || this.waitForTimeout
7039
+
7040
+ var async = this.beginAsync(timeout + 2 * interval),
7000
7041
  me = this;
7001
7042
 
7002
7043
  var sourceLine = me.getSourceLine();
@@ -7015,13 +7056,12 @@ Role('Siesta.Test.More', {
7015
7056
  if (this.typeOf(method) == 'Number') {
7016
7057
  pollTimeout = originalSetTimeout(function() {
7017
7058
  me.endAsync(async);
7018
- callback.call(scope || me);
7059
+ me.processCallbackFromTest(callback, [], scope || me)
7060
+
7019
7061
  }, method);
7020
7062
 
7021
7063
  me.finalizeWaiting(waitAssertion, true, 'Waited ' + method + ' ms');
7022
7064
  } else {
7023
-
7024
- timeout = timeout || this.waitForTimeout
7025
7065
 
7026
7066
  var startDate = new Date()
7027
7067
 
@@ -7058,9 +7098,10 @@ Role('Siesta.Test.More', {
7058
7098
  me.endAsync(async);
7059
7099
 
7060
7100
  me.finalizeWaiting(waitAssertion, true, 'Waited ' + time + ' ms for ' + description);
7061
- callback.call(scope || me, result);
7101
+
7102
+ me.processCallbackFromTest(callback, [ result ], scope || me)
7062
7103
  } else
7063
- pollTimeout = originalSetTimeout(pollFunc, me.waitForPollInterval)
7104
+ pollTimeout = originalSetTimeout(pollFunc, interval)
7064
7105
  }
7065
7106
 
7066
7107
  pollFunc()
@@ -7072,8 +7113,8 @@ Role('Siesta.Test.More', {
7072
7113
  // returns "true" if "next" is used,
7073
7114
  analyzeChainStep : function (func) {
7074
7115
  var sources = func.toString()
7075
- var firstArg = sources.match(/function\s*[^(]*\((.*?)(?:,|\))/)[ 1 ]
7076
-
7116
+ var firstArg = sources.match(/function\s*[^(]*\(\s*(.*?)\s*(?:,|\))/)[ 1 ]
7117
+
7077
7118
  if (!firstArg) return false
7078
7119
 
7079
7120
  var body = sources.match(/\{([\s\S]*)\}/)[ 1 ]
@@ -7158,11 +7199,13 @@ Role('Siesta.Test.More', {
7158
7199
  observeTest : this
7159
7200
  })
7160
7201
 
7202
+ var sourceLine = me.getSourceLine();
7203
+
7161
7204
  // inline any arrays in the arguments into one array
7162
- var steps = Array.prototype.concat.apply([], arguments)
7205
+ var steps = Array.prototype.concat.apply([], arguments)
7163
7206
 
7164
- var len = steps.length
7165
- var args = []
7207
+ var len = steps.length
7208
+ var args = []
7166
7209
 
7167
7210
  Joose.A.each(steps, function (step, index) {
7168
7211
 
@@ -7170,10 +7213,29 @@ Role('Siesta.Test.More', {
7170
7213
 
7171
7214
  queue.addAsyncStep({
7172
7215
  processor : function (data) {
7173
- var async = me.beginAsync()
7216
+ var isWaitStep = step.action == 'wait' || step.waitFor
7217
+
7218
+ if (!isWaitStep) {
7219
+ var timeout = step.timeout || me.defaultTimeout
7220
+
7221
+ // + 100 to allow `waitFor` steps (which will be waiting the `timeout` time) to
7222
+ // generate their own failures
7223
+ var async = me.beginAsync(timeout + 100, function () {
7224
+ me.fail(
7225
+ 'The step in `t.chain()` call did not complete within required timeframe, chain can not proceed',
7226
+ {
7227
+ sourceLine : sourceLine,
7228
+ annotation : 'Step number: ' + (index + 1) + ' (1-based)' + (sourceLine ? '\nAt line : ' + sourceLine : ''),
7229
+ ownTextOnly : true
7230
+ }
7231
+ )
7232
+
7233
+ return true
7234
+ })
7235
+ }
7174
7236
 
7175
7237
  var nextFunc = function () {
7176
- me.endAsync(async)
7238
+ if (!isWaitStep) me.endAsync(async)
7177
7239
 
7178
7240
  args = Array.prototype.slice.call(arguments);
7179
7241
 
@@ -7194,9 +7256,23 @@ Role('Siesta.Test.More', {
7194
7256
  // and finalize the async frame manually, as the "nextFunc" for last step will never be called
7195
7257
  isLast && me.endAsync(async)
7196
7258
 
7259
+ } else if (me.typeOf(step) == 'String') {
7260
+ var action = new Siesta.Test.Action.Eval({
7261
+ actionString : step,
7262
+ next : nextFunc,
7263
+ test : me
7264
+ })
7265
+
7266
+ action.process()
7267
+
7197
7268
  } else {
7198
7269
  if (!step.args) step.args = args
7199
7270
 
7271
+ // Don't pass target to next step if it is a waitFor action, it does not make sense and messes up the arguments
7272
+ if (!isLast && (steps[ index + 1 ].waitFor || steps[ index + 1 ].action == 'wait')) {
7273
+ step.passTargetToNext = false;
7274
+ }
7275
+
7200
7276
  step.next = nextFunc
7201
7277
  step.test = me
7202
7278
 
@@ -7303,6 +7379,9 @@ Class('Siesta.Test', {
7303
7379
  results : Joose.I.Array,
7304
7380
 
7305
7381
  run : { required : true },
7382
+ startTestAnchor : null,
7383
+ exceptionCatcher : null,
7384
+ testErrorClass : null,
7306
7385
 
7307
7386
  harness : { required : true },
7308
7387
 
@@ -7428,9 +7507,16 @@ Class('Siesta.Test', {
7428
7507
  * @return {Object} Object with properties `{ ready : true/false, reason : 'description' }`
7429
7508
  */
7430
7509
  isReady: function() {
7510
+ // this should allow us to wait until the presense of "run" function
7511
+ // it will become available after call to StartTest method
7512
+ // which some users may call asynchronously, after some delay
7513
+ // see https://www.assembla.com/spaces/bryntum/tickets/379
7514
+ // in this case test can not be configured using object as 1st argument for StartTest
7515
+ this.run = this.run || this.getStartTestAnchor().args && this.getStartTestAnchor().args[ 0 ]
7516
+
7431
7517
  return {
7432
- ready : true,
7433
- reason : ''
7518
+ ready : this.typeOf(this.run) == 'Function',
7519
+ reason : 'No code provided to test'
7434
7520
  }
7435
7521
  },
7436
7522
 
@@ -7561,7 +7647,7 @@ Class('Siesta.Test', {
7561
7647
  var gotDesc = params.gotDesc || 'Got'
7562
7648
  var needDesc = params.needDesc || 'Need'
7563
7649
 
7564
- if (assertionName || sourceLine) strings.push(
7650
+ if (!params.ownTextOnly && (assertionName || sourceLine)) strings.push(
7565
7651
  'Failed assertion ' + (assertionName ? '[' + assertionName + '] ' : '') + this.formatSourceLine(sourceLine)
7566
7652
  )
7567
7653
 
@@ -7630,23 +7716,83 @@ Class('Siesta.Test', {
7630
7716
  },
7631
7717
 
7632
7718
 
7719
+ getStartTestAnchor : function () {
7720
+ return this.startTestAnchor
7721
+ },
7722
+
7723
+
7724
+ getExceptionCatcher : function () {
7725
+ return this.exceptionCatcher
7726
+ },
7727
+
7728
+
7729
+ getTestErrorClass : function () {
7730
+ return this.testErrorClass
7731
+ },
7732
+
7733
+
7734
+ processCallbackFromTest : function (callback, args, scope) {
7735
+ var me = this
7736
+
7737
+ if (this.transparentEx) {
7738
+ callback.apply(scope || this.global, args || [])
7739
+ } else {
7740
+ var e = this.getExceptionCatcher()(function(){
7741
+ callback.apply(scope || me.global, args || [])
7742
+ })
7743
+
7744
+ if (e) {
7745
+ this.failWithException(e)
7746
+
7747
+ // flow should be interrupted - exception detected
7748
+ return false
7749
+ }
7750
+ }
7751
+
7752
+ // flow can be continued
7753
+ return true
7754
+ },
7755
+
7756
+
7633
7757
  getStackTrace : function (e) {
7634
7758
  if (Object(e) !== e) return null
7635
7759
  if (!e.stack) return null
7636
7760
 
7637
- var text = e.stack
7638
- var traceLineRegex = /\((.*?)\)@(.*?):(\d+)\n/g;
7761
+ var text = e.stack + '';
7762
+ var isFirefox = /^@/.test(text)
7763
+ var lines = text.split('\n')
7764
+
7765
+ var result = []
7639
7766
  var match
7640
7767
 
7641
- var result = []
7768
+ for (var i = 0; i < lines.length; i++) {
7769
+ if (!lines[ i ]) continue
7770
+
7771
+ if (!i) {
7772
+ if (isFirefox)
7773
+ result.push(e + '')
7774
+ else {
7775
+ result.push(lines[ i ])
7776
+ continue;
7777
+ }
7778
+ }
7642
7779
 
7643
- for (var i = 0; match = traceLineRegex.exec(text); i++)
7644
- if (i)
7645
- // other lines
7646
- result.push('at line ' + match[ 3 ] + ' of ' + match[ 2 ])
7647
- else
7648
- // first line
7649
- result.push(match[ 1 ] + ' at line ' + match[ 3 ] + ' of ' + match[ 2 ])
7780
+ if (isFirefox) {
7781
+ match = /@(.*?):(\d+)/.exec(lines[ i ]);
7782
+
7783
+ // the format of stack trace in Firefox has changed, 080_exception_parsing should fail
7784
+ if (!match) return null
7785
+
7786
+ result.push(' at line ' + match[ 2 ] + ' of ' + match[ 1 ])
7787
+ } else {
7788
+ match = /\s*at\s(.*?):(\d+):(\d+)/.exec(lines[ i ]);
7789
+
7790
+ // the format of stack trace in Chrome has changed, 080_exception_parsing should fail
7791
+ if (!match) return null
7792
+
7793
+ result.push(' at line ' + match[ 2 ] + ', character ' + match[ 3 ] + ', of ' + match[ 1 ])
7794
+ }
7795
+ }
7650
7796
 
7651
7797
  if (!result.length) return null
7652
7798
 
@@ -7730,7 +7876,7 @@ Class('Siesta.Test', {
7730
7876
  * @param {String} desc The description of the assertion
7731
7877
  */
7732
7878
  is : function (got, expected, desc) {
7733
- if (got instanceof this.global.Date) {
7879
+ if (expected && got instanceof this.global.Date) {
7734
7880
  this.isDateEqual(got, expected, desc);
7735
7881
  } else if (got == expected)
7736
7882
  this.pass(desc)
@@ -7863,7 +8009,7 @@ Class('Siesta.Test', {
7863
8009
 
7864
8010
  /**
7865
8011
  * This method starts the "asynchronous frame". The test will wait for all asynchronous frames to complete before it will finalize.
7866
- * The frame can be finished with the {@link #endAsync} call.
8012
+ * The frame should be finished with the {@link #endAsync} call within the provided `time`, otherwise a failure will be reported.
7867
8013
  *
7868
8014
  * For example:
7869
8015
  *
@@ -7878,27 +8024,42 @@ Class('Siesta.Test', {
7878
8024
  *
7879
8025
  *
7880
8026
  * @param {Number} time The maximum time (in ms) to wait until force the finalization of this async frame. Optional. Default time is 15000 ms.
8027
+ * @param {Function} errback Optional. The function to call in case the call to {@link #endAsync} was not detected withing `time`. If function
8028
+ * will return any "truthy" value, the failure will not be reported (you can report own failure with this errback).
8029
+ *
7881
8030
  * @return {Object} The frame object, which can be used in {@link #endAsync} call
7882
8031
  */
7883
- beginAsync : function (time) {
8032
+ beginAsync : function (time, errback) {
8033
+ time = time || this.defaultTimeout
8034
+
7884
8035
  var me = this
7885
8036
  var originalSetTimeout = this.originalSetTimeout
7886
8037
 
8038
+ var index = this.timeoutsCount++
8039
+
7887
8040
  // in NodeJS `setTimeout` returns an object and not a simple ID, so we try hard to store that object under unique index
7888
8041
  // also using `setTimeout` from the scope of test - as timeouts in different scopes in browsers are mis-synchronized
7889
8042
  // can't just use `this.originalSetTimeout` because of scoping issues
7890
- var timeoutId = originalSetTimeout(function () {
7891
- me.endAsync(index)
7892
- }, time || this.defaultTimeout)
7893
-
7894
- var index = this.timeoutsCount++
8043
+ var timeoutId = originalSetTimeout(function () {
8044
+
8045
+ if (me.hasAsyncFrame(index)) {
8046
+ if (!errback || !errback.call(me, me)) me.fail('No matching `endAsync` call within ' + time + 'ms')
8047
+
8048
+ me.endAsync(index)
8049
+ }
8050
+ }, time)
7895
8051
 
7896
- this.timeoutIds[ index ] = timeoutId
8052
+ this.timeoutIds[ index ] = timeoutId
7897
8053
 
7898
8054
  return index
7899
8055
  },
7900
8056
 
7901
8057
 
8058
+ hasAsyncFrame : function (index) {
8059
+ return this.timeoutIds.hasOwnProperty(index)
8060
+ },
8061
+
8062
+
7902
8063
  /**
7903
8064
  * This method finalize the "asynchronous frame" started with {@link #beginAsync}.
7904
8065
  *
@@ -7931,7 +8092,6 @@ Class('Siesta.Test', {
7931
8092
 
7932
8093
 
7933
8094
  clearTimeouts : function () {
7934
- var me = this
7935
8095
  var originalClearTimeout = this.originalClearTimeout
7936
8096
 
7937
8097
  Joose.O.each(this.timeoutIds, function (value, id) {
@@ -8008,10 +8168,14 @@ Class('Siesta.Test', {
8008
8168
  originalClearTimeout : this.originalClearTimeout
8009
8169
  })
8010
8170
 
8011
- var exception = this.global.StartTest.exceptionCatcher(function(){
8171
+ var exception = this.getExceptionCatcher()(function(){
8012
8172
  code(todo)
8013
8173
  })
8014
8174
 
8175
+ todo.global = null
8176
+ todo.originalSetTimeout = null
8177
+ todo.originalClearTimeout = null
8178
+
8015
8179
  if (exception !== undefined) this.diag("TODO section threw an exception: [" + exception + "]")
8016
8180
  },
8017
8181
 
@@ -8049,7 +8213,8 @@ Class('Siesta.Test', {
8049
8213
  this.harness.onTestStart(this)
8050
8214
 
8051
8215
  /**
8052
- * This event is fired when the individual test case starts. When started, test may still be waiting for the
8216
+ * This event is fired when the individual test case starts. When *started*, test may still be waiting for the {@link #isReady} conditions
8217
+ * to be fullfilled. Once all conditions will be fullfilled, test will be *launched*.
8053
8218
  *
8054
8219
  * This event bubbles up to the {@link Siesta.Harness harness}, you can observe it on harness as well.
8055
8220
  *
@@ -8061,7 +8226,7 @@ Class('Siesta.Test', {
8061
8226
  this.fireEvent('teststart', this);
8062
8227
 
8063
8228
  if (alreadyFailedWithException) {
8064
- this.failWithException(alreadyFailedWithException)
8229
+ this.failWithException(alreadyFailedWithException)
8065
8230
 
8066
8231
  return
8067
8232
  }
@@ -8141,7 +8306,7 @@ Class('Siesta.Test', {
8141
8306
  else
8142
8307
  // in browser (where `timeoutId` is a number) - to the `idsToIndex` hash
8143
8308
  me.idsToIndex[ timeoutId ] = index
8144
-
8309
+
8145
8310
  return me.timeoutIds[ index ] = timeoutId
8146
8311
  }
8147
8312
 
@@ -8181,10 +8346,13 @@ Class('Siesta.Test', {
8181
8346
  global.clearTimeout = originalClearTimeout
8182
8347
  }
8183
8348
 
8184
- originalSetTimeout = me.originalSetTimeout = null
8185
- originalClearTimeout = me.originalClearTimeout = null
8349
+ originalSetTimeout = me.originalSetTimeout = null
8350
+ originalClearTimeout = me.originalClearTimeout = null
8186
8351
 
8187
- me.global = global = null
8352
+ me.global = global = null
8353
+ me.run = run = null
8354
+ me.exceptionCatcher = me.testErrorClass = null
8355
+ me.startTestAnchor = null
8188
8356
  }
8189
8357
 
8190
8358
  var run = this.run
@@ -8192,7 +8360,7 @@ Class('Siesta.Test', {
8192
8360
  if (this.transparentEx)
8193
8361
  run(me)
8194
8362
  else
8195
- var e = global.StartTest.exceptionCatcher(function(){
8363
+ var e = this.getExceptionCatcher()(function(){
8196
8364
  run(me)
8197
8365
  })
8198
8366
 
@@ -8214,11 +8382,12 @@ Class('Siesta.Test', {
8214
8382
  if (force) this.clearTimeouts()
8215
8383
 
8216
8384
  if (!Joose.O.isEmpty(this.timeoutIds)) {
8217
- if (!this.__timeoutWarning && this.overrideSetTimeout && this.lastActivityDate &&
8218
- new Date() - this.lastActivityDate > this.defaultTimeout)
8219
- {
8385
+ if (
8386
+ !this.__timeoutWarning && this.overrideSetTimeout && this.lastActivityDate &&
8387
+ new Date() - this.lastActivityDate > this.defaultTimeout * 2
8388
+ ) {
8220
8389
  this.diag('Your test is still considered to be running, if this is unexpected please see console for more information');
8221
- this.warn('Warning, your test has not finalized, most likely since a timer (setTimeout) is still active ' +
8390
+ this.warn('Your test [' + this.url + '] has not finalized, most likely since a timer (setTimeout) is still active. ' +
8222
8391
  'If this is the expected behavior, try setting "overrideSetTimeout : false" on your Harness configuration.');
8223
8392
  this.__timeoutWarning = true;
8224
8393
  }
@@ -8259,25 +8428,6 @@ Class('Siesta.Test', {
8259
8428
  this.fireEvent('testfinalize', this);
8260
8429
 
8261
8430
  this.callback && this.callback()
8262
-
8263
- // // attempting to clear all references to scope, but with delay, to allow
8264
- // // other potentially delayed actions to access `global`
8265
- // var me = this
8266
- //
8267
- // var originalSetTimeout = me.originalSetTimeout
8268
- //
8269
- // // setTimeout from the scope of harness
8270
- // originalSetTimeout(function () {
8271
- // if (me.overrideSetTimeout) {
8272
- // me.global.setTimeout = me.originalSetTimeout
8273
- // me.global.clearTimeout = me.originalClearTimeout
8274
- // }
8275
- //
8276
- // originalSetTimeout = me.originalSetTimeout = null
8277
- // me.originalClearTimeout = null
8278
- //
8279
- // me.global = null
8280
- // }, 700)
8281
8431
  },
8282
8432
 
8283
8433
 
@@ -8454,9 +8604,10 @@ Class('Siesta.Test', {
8454
8604
 
8455
8605
 
8456
8606
  warn : function (message) {
8457
- // TODO add "warning" diagnostic message with red background or somethin
8458
-
8459
- if (typeof console != 'undefined' && console.warn) console.warn(message)
8607
+ this.addResult(new Siesta.Result.Diagnostic({
8608
+ description : message,
8609
+ isWarning : true
8610
+ }))
8460
8611
  }
8461
8612
  }
8462
8613
 
@@ -8471,6 +8622,21 @@ Role('Siesta.Test.Todo', {
8471
8622
 
8472
8623
  methods : {
8473
8624
 
8625
+ getExceptionCatcher : function () {
8626
+ return this.parent.getExceptionCatcher()
8627
+ },
8628
+
8629
+
8630
+ getTestErrorClass : function () {
8631
+ return this.parent.getTestErrorClass()
8632
+ },
8633
+
8634
+
8635
+ getStartTestAnchor : function () {
8636
+ return this.parent.getStartTestAnchor()
8637
+ },
8638
+
8639
+
8474
8640
  addResult : function (result) {
8475
8641
  if (result instanceof Siesta.Result.Assertion) result.isTodo = true
8476
8642
 
@@ -8539,6 +8705,10 @@ Class('Siesta.Test.Action', {
8539
8705
  has : {
8540
8706
  args : null,
8541
8707
 
8708
+ /**
8709
+ * @cfg {String} desc When provided, once step is completed, a passing assertion with this text will be added to a test.
8710
+ * This configuration option can be useful to indicate the progress of "wait" steps
8711
+ */
8542
8712
  desc : null,
8543
8713
  test : { required : true },
8544
8714
  next : { required : true },
@@ -8598,6 +8768,8 @@ Class('Siesta.Test.Action.Done', {
8598
8768
 
8599
8769
  process : function () {
8600
8770
  this.test.done(this.delay)
8771
+
8772
+ this.next()
8601
8773
  }
8602
8774
  }
8603
8775
  });
@@ -8673,14 +8845,14 @@ Class('Siesta.Test.Action.Wait', {
8673
8845
  *
8674
8846
  * A number of milliseconds to wait before continuing.
8675
8847
  */
8676
- delay : 1000,
8848
+ delay : 1000,
8677
8849
 
8678
8850
  /**
8679
8851
  * @cfg {Number} timeout
8680
8852
  *
8681
8853
  * The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value.
8682
8854
  */
8683
- timeout : null,
8855
+ timeout : null,
8684
8856
 
8685
8857
  /**
8686
8858
  * @cfg {Array} args
@@ -8725,25 +8897,24 @@ Class('Siesta.Test.Action.Wait', {
8725
8897
  this.args = [ waitFor ];
8726
8898
  waitFor = '';
8727
8899
  }
8728
-
8900
+
8901
+ if (waitFor == null) {
8902
+ this.args = [ this.delay ];
8903
+ waitFor = '';
8904
+ }
8905
+
8729
8906
  if (this.test.typeOf(this.args) !== "Array") {
8730
8907
  this.args = [ this.args ];
8731
8908
  }
8732
8909
 
8733
- if (waitFor != null) {
8734
- // also allow full method names
8735
- waitFor = waitFor.replace(/^waitFor/, '')
8736
- var methodName = 'waitFor' + Joose.S.uppercaseFirst(waitFor);
8737
-
8738
- if (!test[methodName]){
8739
- throw 'Could not find a waitFor method named ' + methodName;
8740
- }
8741
- test[methodName].apply(test, this.args.concat(this.next, test, this.timeout || test.waitForTimeout));
8742
- } else {
8743
- var originalSetTimeout = test.originalSetTimeout;
8910
+ // also allow full method names
8911
+ waitFor = waitFor.replace(/^waitFor/, '')
8912
+ var methodName = 'waitFor' + Joose.S.uppercaseFirst(waitFor);
8744
8913
 
8745
- originalSetTimeout(this.next, this.delay);
8914
+ if (!test[methodName]){
8915
+ throw 'Could not find a waitFor method named ' + methodName;
8746
8916
  }
8917
+ test[methodName].apply(test, this.args.concat(this.next, test, this.timeout || test.waitForTimeout));
8747
8918
  }
8748
8919
  }
8749
8920
  });
@@ -8753,6 +8924,115 @@ Joose.A.each(['wait', 'delay'], function(name) {
8753
8924
  });;
8754
8925
  /**
8755
8926
 
8927
+ @class Siesta.Test.Action.Eval
8928
+ @extends Siesta.Test.Action
8929
+
8930
+ This action can be included in the `t.chain` steps only with a plain string. Siesta will examine the passed string,
8931
+ and call an apropriate method of the test class. String should have the following format:
8932
+
8933
+ methodName(params)
8934
+
8935
+ Method name is anything until the first parenthes. Method name may have an optional prefix `t.`.
8936
+ Everything in between of outermost parentheses will be treated as parameters for method call. For example:
8937
+
8938
+ t.chain(
8939
+ // string should look like a usual method call,
8940
+ // but arguments can't reference any variables
8941
+ // strings should be quoted, to include quoting symbol in string use double slash: \\
8942
+ 't.click("combo[type=some\\"Type] => .x-form-trigger")',
8943
+
8944
+ // leading "t." is optional, but quoting is not
8945
+ 'waitForComponent("combo[type=someType]")',
8946
+
8947
+ // JSON objects are ok, but they should be a valid JSON - ie object properties should be quoted
8948
+ 'myClick([ 10, 10 ], { "foo" : "bar" })',
8949
+ )
8950
+
8951
+ * **Note** You can pass the JSON objects as arguments, but they should be serialized as valid JSON - ie object properties should be quoted.
8952
+
8953
+ * **Note** A callback for next step in chain will be always appended to provided parameters. Make sure it is placed in a correct spot!
8954
+ For example if method signature is `t.someMethod(param1, param2, callback)` and you are calling this method as:
8955
+
8956
+ t.chain(
8957
+ `t.someMethod("text")`
8958
+ )
8959
+ it will fail - callback will be provided in place of `param2`. Instead call it as:
8960
+
8961
+ t.chain(
8962
+ `t.someMethod("text", null)`
8963
+ )
8964
+
8965
+ This action may save you few keystrokes, when you need to perform some action with static arguments (known prior the action).
8966
+
8967
+ */
8968
+ Class('Siesta.Test.Action.Eval', {
8969
+
8970
+ isa : Siesta.Test.Action,
8971
+
8972
+ has : {
8973
+ /**
8974
+ * @cfg {Object} options
8975
+ *
8976
+ * Any options that will be used when simulating the event. For information about possible
8977
+ * config options, please see: https://developer.mozilla.org/en-US/docs/DOM/event.initMouseEvent
8978
+ */
8979
+ actionString : null
8980
+ },
8981
+
8982
+
8983
+ methods : {
8984
+
8985
+ process : function () {
8986
+ var test = this.test
8987
+ var parsed = this.parseActionString(this.actionString)
8988
+
8989
+ if (parsed.error) {
8990
+ test.fail(parsed.error)
8991
+ this.next()
8992
+ return
8993
+ }
8994
+
8995
+ var methodName = parsed.methodName
8996
+
8997
+ if (!methodName || test.typeOf(test[ methodName ]) != 'Function') {
8998
+ test.fail("Invalid method name: " + methodName)
8999
+ this.next()
9000
+ return
9001
+ }
9002
+
9003
+ parsed.params.push(this.next)
9004
+
9005
+ test[ methodName ].apply(test, parsed.params)
9006
+ },
9007
+
9008
+
9009
+ parseActionString : function (actionString) {
9010
+ var match = /^\s*(.+?)\(\s*(.*)\s*\)\s*$/.exec(actionString)
9011
+
9012
+ if (!match) return {
9013
+ error : "Wrong format of the action string: " + actionString
9014
+ }
9015
+
9016
+ var methodName = match[ 1 ].replace(/^t\./, '')
9017
+
9018
+ try {
9019
+ var params = JSON.parse('[' + match[ 2 ] + ']')
9020
+ } catch (e) {
9021
+ return {
9022
+ error : "Can't parse arguments: " + match[ 2 ]
9023
+ }
9024
+ }
9025
+
9026
+ return {
9027
+ methodName : methodName,
9028
+ params : params
9029
+ }
9030
+ }
9031
+ }
9032
+ });
9033
+ ;
9034
+ /**
9035
+
8756
9036
  @class Siesta.Harness
8757
9037
 
8758
9038
  `Siesta.Harness` is an abstract base harness class in Siesta hierarchy. This class provides no UI,
@@ -8836,7 +9116,7 @@ Class('Siesta.Harness', {
8836
9116
 
8837
9117
  has : {
8838
9118
  /**
8839
- * @cfg {String} title The title of the test suite. Can contain HTML.
9119
+ * @cfg {String} title The title of the test suite. Can contain HTML. When provided in the test file descriptor - will change the name of test in the harness UI.
8840
9120
  */
8841
9121
  title : null,
8842
9122
 
@@ -8849,8 +9129,6 @@ Class('Siesta.Harness', {
8849
9129
  testClass : Siesta.Test,
8850
9130
  contentManagerClass : Siesta.Content.Manager,
8851
9131
 
8852
- testsByURL : Joose.I.Object,
8853
-
8854
9132
  // fields of test descriptor:
8855
9133
  // - id - either `url` or wbs + group - computed
8856
9134
  // - url
@@ -8872,7 +9150,12 @@ Class('Siesta.Harness', {
8872
9150
  descriptors : Joose.I.Array,
8873
9151
  descriptorsById : Joose.I.Object,
8874
9152
 
9153
+ launchCounter : 0,
9154
+
9155
+ launches : Joose.I.Object,
9156
+
8875
9157
  scopesByURL : Joose.I.Object,
9158
+ testsByURL : Joose.I.Object,
8876
9159
 
8877
9160
  /**
8878
9161
  * @cfg {Boolean} transparentEx When set to `true` harness will not try to catch any exception, thrown from the test code.
@@ -8935,7 +9218,7 @@ Class('Siesta.Harness', {
8935
9218
  * This option can be also specified in the test file descriptor.
8936
9219
  */
8937
9220
  expectedGlobals : Joose.I.Array,
8938
- // will be populated by `setup`
9221
+ // will be populated by `populateCleanScopeGlobals`
8939
9222
  cleanScopeGlobals : Joose.I.Array,
8940
9223
 
8941
9224
  /**
@@ -9035,6 +9318,17 @@ Class('Siesta.Harness', {
9035
9318
  */
9036
9319
  keepResults : true,
9037
9320
 
9321
+ /**
9322
+ * @cfg {Number} keepNLastResults
9323
+ *
9324
+ * Only meaningful when {@link #keepResults} is set to `false`. Indicates the number of the test results which still should be kept, for user examination.
9325
+ * Results are cleared when their total number exceed this value, based on FIFO order.
9326
+ */
9327
+ keepNLastResults : 2,
9328
+
9329
+ lastResultsURLs : Joose.I.Array,
9330
+ lastResultsByURL : Joose.I.Object,
9331
+
9038
9332
  /**
9039
9333
  * @cfg {Boolean} overrideSetTimeout When set to `false`, the tests will not override "setTimeout" from the context of each test
9040
9334
  * for asynchronous code tracking. User will need to use `beginAsync/endAsync` calls to indicate that test is still running.
@@ -9225,13 +9519,14 @@ Class('Siesta.Harness', {
9225
9519
  },
9226
9520
 
9227
9521
  /**
9228
- * This method will launch a test suite. It accepts a variable number of *test file descriptors*. A test file descritor is one of the following:
9522
+ * This method will launch a test suite. It accepts a variable number of *test file descriptors* or an array of such. A test file descritor is one of the following:
9229
9523
  *
9230
9524
  * - a string, containing a test file url
9231
9525
  * - an object containing the `url` property `{ url : '...', option1 : 'value1', option2 : 'value2' }`. The `url` property should point to the test file.
9232
9526
  * Other properties can contain values of some configuration options of the harness (marked accordingly). In this case, they will **override** the corresponding values,
9233
9527
  * provided to harness or parent descriptor.
9234
- * - an object `{ group : 'groupName', items : [], expanded : true, option1 : 'value1' }` specifying the folder of test files. The `items` property can contain an array of test file descriptors.
9528
+ * - an object `{ group : 'groupName', items : [], expanded : true, option1 : 'value1' }` specifying the folder of test files. The `expanded` property
9529
+ * sets the initial state of the folder - "collapsed/expanded". The `items` property can contain an array of test file descriptors.
9235
9530
  * Other properties will override the applicable harness options **for all child descriptors**.
9236
9531
  *
9237
9532
  * Groups (folder) may contain nested groups. Number of nesting levels is not limited.
@@ -9294,19 +9589,19 @@ Class('Siesta.Harness', {
9294
9589
  *
9295
9590
  * Values from this object takes the highest priority and will override any other configuration.
9296
9591
  *
9297
- * @param {Mixed} descriptor1
9592
+ * @param {Array/Mixed} descriptor1 or an array of descriptors
9298
9593
  * @param {Mixed} descriptor2
9299
9594
  * @param {Mixed} descriptorN
9300
9595
  */
9301
9596
  start : function () {
9302
9597
  // a bit hackish - used by Selenium reporter..
9303
- var me = __ACTIVE_HARNESS__ = this
9598
+ var me = Siesta.my.activeHarness = this
9304
9599
 
9305
9600
  this.mainPreset = new Siesta.Content.Preset({
9306
9601
  preload : this.processPreloadArray(this.preload)
9307
9602
  })
9308
9603
 
9309
- var descriptors = this.descriptors = Joose.A.map(arguments, function (desc, index) {
9604
+ var descriptors = this.descriptors = Joose.A.map(Array.prototype.concat.apply([], arguments), function (desc, index) {
9310
9605
  return me.normalizeDescriptor(desc, me, index)
9311
9606
  })
9312
9607
 
@@ -9334,6 +9629,8 @@ Class('Siesta.Harness', {
9334
9629
  if (desc.preset != me.mainPreset) presets.push(desc.preset)
9335
9630
 
9336
9631
  testScriptsPreset.addResource(desc.url)
9632
+
9633
+ me.deleteTestByURL(desc.url)
9337
9634
  })
9338
9635
 
9339
9636
  // cache either everything (this.cachePreload) or only the test files (to be able to show missing files / show content)
@@ -9359,8 +9656,9 @@ Class('Siesta.Harness', {
9359
9656
 
9360
9657
  // if testConfig contains the "preload" or "alsoPreload" key - then we need to update the preset of the descriptor
9361
9658
  if (testConfig && (testConfig.preload || testConfig.alsoPreload)) desc.preset = me.getDescriptorPreset(desc)
9362
- } else
9363
- desc.isMissing = true
9659
+ } else
9660
+ // allow subclasses to define there own logic when found missing test file
9661
+ me.markMissingFile(desc)
9364
9662
 
9365
9663
  me.normalizeScopeProvider(desc)
9366
9664
  })
@@ -9377,6 +9675,11 @@ Class('Siesta.Harness', {
9377
9675
  },
9378
9676
 
9379
9677
 
9678
+ markMissingFile : function (desc) {
9679
+ desc.isMissing = true
9680
+ },
9681
+
9682
+
9380
9683
  flattenDescriptors : function (descriptors, includeFolders) {
9381
9684
  var flatten = []
9382
9685
  var me = this
@@ -9568,10 +9871,10 @@ Class('Siesta.Harness', {
9568
9871
 
9569
9872
 
9570
9873
  getSeedingCode : function (desc) {
9571
- return 'StartTest = function () { StartTest.args = arguments };' +
9874
+ return 'StartTest = startTest = function () { StartTest.args = arguments };' +
9572
9875
  // for older IE - the try/catch should be from the same context as the exception
9573
9876
  'StartTest.exceptionCatcher = function (func) { var ex; try { func() } catch (e) { ex = e; }; return ex; };' +
9574
- 'StartTest.testError = Error;'
9877
+ 'StartTest.testErrorClass = Error;'
9575
9878
  },
9576
9879
 
9577
9880
 
@@ -9584,12 +9887,54 @@ Class('Siesta.Harness', {
9584
9887
  },
9585
9888
 
9586
9889
 
9890
+ keepTestResult : function (url) {
9891
+ // already keeping
9892
+ if (this.lastResultsByURL[ url ]) {
9893
+ var indexOf = -1
9894
+
9895
+ Joose.A.each(this.lastResultsURLs, function (resultUrl, i) {
9896
+ if (resultUrl == url) { indexOf = i; return false }
9897
+ })
9898
+
9899
+ this.lastResultsURLs.splice(indexOf, 1)
9900
+ this.lastResultsURLs.push(url)
9901
+
9902
+ return
9903
+ }
9904
+
9905
+ this.lastResultsURLs.push(url)
9906
+ this.lastResultsByURL[ url ] = true
9907
+
9908
+ if (this.lastResultsURLs.length > this.keepNLastResults) this.releaseTestResult()
9909
+ },
9910
+
9911
+
9912
+ releaseTestResult : function () {
9913
+ if (this.lastResultsURLs.length <= this.keepNLastResults) return
9914
+
9915
+ var url = this.lastResultsURLs.shift()
9916
+
9917
+ delete this.lastResultsByURL[ url ]
9918
+
9919
+ var test = this.getTestByURL(url)
9920
+
9921
+ if (test && test.isFinished()) this.cleanupScopeForURL(url)
9922
+ },
9923
+
9924
+
9925
+ isKeepingResultForURL : function (url) {
9926
+ return this.lastResultsByURL[ url ]
9927
+ },
9928
+
9929
+
9587
9930
  setupScope : function (desc) {
9588
9931
  var url = desc.url
9589
9932
  var scopeProvideClass = eval(desc.scopeProvider)
9590
9933
 
9591
9934
  this.cleanupScopeForURL(url)
9592
9935
 
9936
+ this.keepTestResult(url)
9937
+
9593
9938
  return this.scopesByURL[ url ] = new scopeProvideClass(this.getScopeProviderConfigFor(desc))
9594
9939
  },
9595
9940
 
@@ -9705,7 +10050,7 @@ Class('Siesta.Harness', {
9705
10050
  var args = startTestAnchor && startTestAnchor.args
9706
10051
 
9707
10052
  // pick either 1st or 2nd argument depending which one is a function
9708
- var runFunc = args && (typeof args[ 0 ] == 'function' && args[ 0 ] || args[ 1 ])
10053
+ var runFunc = args && (typeof args[ 0 ] == 'function' ? args[ 0 ] : args[ 1 ])
9709
10054
 
9710
10055
  me.launchTest({
9711
10056
  testHolder : testHolder,
@@ -9716,6 +10061,8 @@ Class('Siesta.Harness', {
9716
10061
  preloadErrors : preloadErrors,
9717
10062
  onErrorHandler : onErrorHandler,
9718
10063
 
10064
+ startTestAnchor : startTestAnchor,
10065
+
9719
10066
  runFunc : runFunc
9720
10067
  }, callback)
9721
10068
  });
@@ -9736,15 +10083,18 @@ Class('Siesta.Harness', {
9736
10083
  // after the scope setup, the `onerror` handler might be cleared - installing it again
9737
10084
  if (!this.getDescriptorConfig(desc, 'transparentEx')) scopeProvider.addOnErrorHandler(options.onErrorHandler)
9738
10085
 
9739
- var testConfig = me.getNewTestConfiguration(desc, scopeProvider, options.contentManager, options.urlOptions, options.runFunc)
10086
+ var testConfig = me.getNewTestConfiguration(desc, scopeProvider, options.contentManager, options.urlOptions, options.runFunc, options.startTestAnchor)
9740
10087
 
9741
10088
  testConfig.callback = function () {
9742
- if (!me.keepResults) me.cleanupScopeForURL(url)
10089
+ if (!me.keepResults) {
10090
+ if (!me.isKeepingResultForURL(url)) me.cleanupScopeForURL(url)
10091
+ }
9743
10092
 
9744
10093
  callback && callback()
9745
10094
  }
9746
10095
 
9747
- var test = options.testHolder.test = me.testsByURL[ url ] = new testClass(testConfig)
10096
+ var test = options.testHolder.test = new testClass(testConfig)
10097
+ this.saveTestWithURL(url, test)
9748
10098
 
9749
10099
  scopeProvider.scope.setTimeout(function() {
9750
10100
  //console.timeEnd('launch')
@@ -9755,7 +10105,7 @@ Class('Siesta.Harness', {
9755
10105
  },
9756
10106
 
9757
10107
 
9758
- getNewTestConfiguration : function (desc, scopeProvider, contentManager, options, runFunc) {
10108
+ getNewTestConfiguration : function (desc, scopeProvider, contentManager, options, runFunc, startTestAnchor) {
9759
10109
  var scope = scopeProvider.scope
9760
10110
 
9761
10111
  var config = {
@@ -9763,6 +10113,11 @@ Class('Siesta.Harness', {
9763
10113
 
9764
10114
  harness : this,
9765
10115
  run : runFunc,
10116
+
10117
+ startTestAnchor : startTestAnchor,
10118
+
10119
+ exceptionCatcher : startTestAnchor.exceptionCatcher,
10120
+ testErrorClass : startTestAnchor.testErrorClass,
9766
10121
 
9767
10122
  expectedGlobals : this.cleanScopeGlobals.concat(this.getDescriptorConfig(desc, 'expectedGlobals')),
9768
10123
  autoCheckGlobals : this.getDescriptorConfig(desc, 'autoCheckGlobals'),
@@ -9797,18 +10152,34 @@ Class('Siesta.Harness', {
9797
10152
  },
9798
10153
 
9799
10154
 
10155
+ getTestByURL : function (url) {
10156
+ return this.testsByURL[ url ]
10157
+ },
10158
+
10159
+
10160
+ saveTestWithURL : function (url, test) {
10161
+ this.testsByURL[ url ] = test
10162
+ },
10163
+
10164
+
10165
+ deleteTestByURL : function (url) {
10166
+ delete this.testsByURL[ url ]
10167
+ },
10168
+
10169
+
9800
10170
  allPassed : function () {
9801
10171
  var allPassed = true
9802
10172
  var me = this
9803
10173
 
9804
10174
  Joose.A.each(this.flattenDescriptors(this.descriptors), function (descriptor) {
9805
- var test = me.testsByURL[ descriptor.url ]
10175
+ // if at least one test is missing then something is wrong
10176
+ if (descriptor.isMissing) { allPassed = false; return false }
10177
+
10178
+ var test = me.getTestByURL(descriptor.url)
9806
10179
 
9807
10180
  // ignore missing tests (could be skipped by test filtering
9808
10181
  if (!test) return
9809
10182
 
9810
- if (descriptor.isMissing) { allPassed = false; return false }
9811
-
9812
10183
  allPassed = allPassed && test.isPassed()
9813
10184
  })
9814
10185
 
@@ -9825,6 +10196,11 @@ Class('Siesta.Harness', {
9825
10196
  if (!this[ methodName ]) throw "Can't generate report - missing the `" + methodName + "` method"
9826
10197
 
9827
10198
  return this[ methodName ](options)
10199
+ },
10200
+
10201
+
10202
+ typeOf : function (object) {
10203
+ return Object.prototype.toString.call(object).replace(/^\[object /, '').replace(/\]$/, '')
9828
10204
  }
9829
10205
  }
9830
10206
  // eof methods
@@ -18824,6 +19200,15 @@ delegate = function (event) {
18824
19200
  }
18825
19201
  };
18826
19202
  })(window);
19203
+
19204
+ jQuery.fn.center = function () {
19205
+ this.css("position","absolute");
19206
+ this.css("top", Math.max(0, (($(window).height() - this.outerHeight()) / 2) +
19207
+ $(window).scrollTop()) + "px");
19208
+ this.css("left", Math.max(0, (($(window).width() - this.outerWidth()) / 2) +
19209
+ $(window).scrollLeft()) + "px");
19210
+ return this;
19211
+ }
18827
19212
  ;
18828
19213
  if (typeof JooseX != "undefined" && !JooseX.SimpleRequest) {;
18829
19214
  Class("JooseX.SimpleRequest", {
@@ -19048,6 +19433,8 @@ Role('Siesta.Test.Action.Role.HasTarget', {
19048
19433
  */
19049
19434
  target : { required : false },
19050
19435
 
19436
+ normalizedTarget : null,
19437
+
19051
19438
  /**
19052
19439
  * @cfg {Object} el
19053
19440
  *
@@ -19055,7 +19442,7 @@ Role('Siesta.Test.Action.Role.HasTarget', {
19055
19442
  */
19056
19443
 
19057
19444
  /**
19058
- * @cfg {Boolean} passTargetToNext Whether to pass
19445
+ * @cfg {Boolean} passTargetToNext Whether to pass the target further on chain as the first argument
19059
19446
  */
19060
19447
  passTargetToNext : true
19061
19448
  },
@@ -19068,12 +19455,12 @@ Role('Siesta.Test.Action.Role.HasTarget', {
19068
19455
  var me = this
19069
19456
  var prevNext = this.next
19070
19457
 
19071
- // Needs to be 'resolved' a action instantiate time since the action may cause the selector not to be found
19072
- // e.g. unchecking a checkbox
19073
- var realTarget = me.test.normalizeActionTarget(me.getTarget());
19458
+ // // Needs to be 'resolved' at action instantiate time since the action may cause the selector not to be found
19459
+ // // e.g. unchecking a checkbox
19460
+ // var realTarget = me.test.normalizeActionTarget(me.getTarget());
19074
19461
 
19075
19462
  this.next = function () {
19076
- prevNext.call(this, realTarget);
19463
+ prevNext.call(this, me.normalizedTarget);
19077
19464
  }
19078
19465
  }
19079
19466
  },
@@ -19095,11 +19482,11 @@ Role('Siesta.Test.Action.Role.HasTarget', {
19095
19482
  var test = this.test;
19096
19483
  var target = this.target || test.getElementAtCursor();
19097
19484
 
19098
- if (test.typeOf(target) === 'Function') {
19099
- return this.__cachedTarget__ = target.call(test, this);
19100
- }
19485
+ if (test.typeOf(target) === 'Function') target = target.call(test, this);
19486
+
19487
+ this.normalizedTarget = test.normalizeActionTarget(target)
19101
19488
 
19102
- return this.__cachedTarget__ = target;
19489
+ return this.__cachedTarget__ = target
19103
19490
  }
19104
19491
  }
19105
19492
  });
@@ -19191,30 +19578,119 @@ Siesta.Test.ActionRegistry.registerAction('longpress', Siesta.Test.Action.LongPr
19191
19578
  ;
19192
19579
  /**
19193
19580
 
19194
- @class Siesta.Test.Action.MouseDown
19581
+ @class Siesta.Test.Action.Tap
19195
19582
  @extends Siesta.Test.Action
19196
19583
  @mixin Siesta.Test.Action.Role.HasTarget
19197
19584
 
19198
- This action can be included in a `t.chain` call with "mouseDown" shortcut:
19585
+ This action can be included in the `t.chain` call with "tap" shortcut:
19199
19586
 
19200
19587
  t.chain(
19201
19588
  {
19202
- action : 'mouseDown',
19589
+ action : 'tap',
19203
19590
  target : someDOMElement
19204
19591
  }
19205
19592
  )
19206
19593
 
19207
- This action will perform a {@link Siesta.Test.Browser#MouseDown MouseDown} on the provided {@link #target}.
19594
+ This action will perform a {@link Siesta.Test.Browser#tap tap} on the provided {@link #target}.
19208
19595
 
19209
19596
  */
19210
- Class('Siesta.Test.Action.MouseDown', {
19597
+ Class('Siesta.Test.Action.Tap', {
19211
19598
 
19212
19599
  isa : Siesta.Test.Action,
19213
19600
 
19214
19601
  does : Siesta.Test.Action.Role.HasTarget,
19215
19602
 
19216
19603
  has : {
19217
- requiredTestMethod : 'mouseDown'
19604
+ requiredTestMethod : 'tap'
19605
+ },
19606
+
19607
+
19608
+ methods : {
19609
+
19610
+ process : function () {
19611
+ this.test.tap(this.getTarget(), this.next)
19612
+ }
19613
+ }
19614
+ });
19615
+
19616
+
19617
+ Siesta.Test.ActionRegistry.registerAction('tap', Siesta.Test.Action.Tap);
19618
+ /**
19619
+
19620
+ @class Siesta.Test.Action.DoubleTap
19621
+ @extends Siesta.Test.Action
19622
+ @mixin Siesta.Test.Action.Role.HasTarget
19623
+
19624
+ This action will perform a {@link Siesta.Test.Browser#doubleClick double tap} on the provided {@link #target}.
19625
+
19626
+ This action can be included in the `t.chain` call with "doubletap" or "doubleTap" shortcuts:
19627
+
19628
+ t.chain(
19629
+ {
19630
+ action : 'doubletap',
19631
+ target : someDOMElement
19632
+ }
19633
+ )
19634
+
19635
+
19636
+ */
19637
+ Class('Siesta.Test.Action.DoubleTap', {
19638
+
19639
+ isa : Siesta.Test.Action,
19640
+
19641
+ does : Siesta.Test.Action.Role.HasTarget,
19642
+
19643
+ has : {
19644
+ requiredTestMethod : 'doubleTap'
19645
+ },
19646
+
19647
+
19648
+ methods : {
19649
+
19650
+ process : function () {
19651
+ this.test.doubleTap(this.getTarget(), this.next)
19652
+ }
19653
+ }
19654
+ });
19655
+
19656
+
19657
+ Siesta.Test.ActionRegistry.registerAction('doubletap', Siesta.Test.Action.DoubleTap)
19658
+ ;
19659
+ /**
19660
+
19661
+ @class Siesta.Test.Action.MouseDown
19662
+ @extends Siesta.Test.Action
19663
+ @mixin Siesta.Test.Action.Role.HasTarget
19664
+
19665
+ This action can be included in a `t.chain` call with "mouseDown" shortcut:
19666
+
19667
+ t.chain(
19668
+ {
19669
+ action : 'mouseDown',
19670
+ target : someDOMElement,
19671
+ options : { shiftKey : true } // Optionally hold shiftkey
19672
+ }
19673
+ )
19674
+
19675
+ This action will perform a {@link Siesta.Test.Browser#MouseDown MouseDown} on the provided {@link #target}.
19676
+
19677
+ */
19678
+ Class('Siesta.Test.Action.MouseDown', {
19679
+
19680
+ isa : Siesta.Test.Action,
19681
+
19682
+ does : Siesta.Test.Action.Role.HasTarget,
19683
+
19684
+ has : {
19685
+ requiredTestMethod : 'mouseDown',
19686
+
19687
+ /**
19688
+ * @cfg {Object} options
19689
+ *
19690
+ * Any options that will be used when simulating the event. For information about possible
19691
+ * config options, please see: https://developer.mozilla.org/en-US/docs/DOM/event.initMouseEvent
19692
+ */
19693
+ options : null
19218
19694
  },
19219
19695
 
19220
19696
 
@@ -19222,7 +19698,7 @@ Class('Siesta.Test.Action.MouseDown', {
19222
19698
 
19223
19699
  process : function () {
19224
19700
  // This method is synchronous
19225
- this.test.mouseDown(this.getTarget());
19701
+ this.test.mouseDown(this.getTarget(), this.options);
19226
19702
 
19227
19703
  setTimeout(this.next, 100);
19228
19704
  }
@@ -19244,7 +19720,8 @@ This action can be included in a `t.chain` call with "mouseUp" shortcut:
19244
19720
  t.chain(
19245
19721
  {
19246
19722
  action : 'mouseUp',
19247
- target : someDOMElement
19723
+ target : someDOMElement,
19724
+ options : { shiftKey : true } // Optionally hold shiftkey
19248
19725
  }
19249
19726
  )
19250
19727
 
@@ -19258,7 +19735,15 @@ Class('Siesta.Test.Action.MouseUp', {
19258
19735
  does : Siesta.Test.Action.Role.HasTarget,
19259
19736
 
19260
19737
  has : {
19261
- requiredTestMethod : 'mouseUp'
19738
+ requiredTestMethod : 'mouseUp',
19739
+
19740
+ /**
19741
+ * @cfg {Object} options
19742
+ *
19743
+ * Any options that will be used when simulating the event. For information about possible
19744
+ * config options, please see: https://developer.mozilla.org/en-US/docs/DOM/event.initMouseEvent
19745
+ */
19746
+ options : null
19262
19747
  },
19263
19748
 
19264
19749
 
@@ -19266,7 +19751,7 @@ Class('Siesta.Test.Action.MouseUp', {
19266
19751
 
19267
19752
  process : function () {
19268
19753
  // This method is synchronous
19269
- this.test.mouseUp(this.getTarget());
19754
+ this.test.mouseUp(this.getTarget(), this.options);
19270
19755
 
19271
19756
  setTimeout(this.next, 100);
19272
19757
  }
@@ -19288,7 +19773,8 @@ This action can be included in the `t.chain` call with "click" shortcut:
19288
19773
  t.chain(
19289
19774
  {
19290
19775
  action : 'click',
19291
- target : someDOMElement
19776
+ target : someDOMElement,
19777
+ options : { shiftKey : true } // Optionally hold shiftkey
19292
19778
  }
19293
19779
  )
19294
19780
 
@@ -19302,21 +19788,29 @@ Class('Siesta.Test.Action.Click', {
19302
19788
  does : Siesta.Test.Action.Role.HasTarget,
19303
19789
 
19304
19790
  has : {
19305
- requiredTestMethod : 'click'
19791
+ requiredTestMethod : 'click',
19792
+
19793
+ /**
19794
+ * @cfg {Object} options
19795
+ *
19796
+ * Any options that will be used when simulating the event. For information about possible
19797
+ * config options, please see: https://developer.mozilla.org/en-US/docs/DOM/event.initMouseEvent
19798
+ */
19799
+ options : null
19306
19800
  },
19307
19801
 
19308
19802
 
19309
19803
  methods : {
19310
19804
 
19311
19805
  process : function () {
19312
- this.test.click(this.getTarget(), this.next)
19806
+ this.test.click(this.getTarget(), this.next, null, this.options);
19313
19807
  }
19314
19808
  }
19315
19809
  });
19316
19810
 
19317
19811
 
19318
- Siesta.Test.ActionRegistry.registerAction('click', Siesta.Test.Action.Click)
19319
- Siesta.Test.ActionRegistry.registerAction('tap', Siesta.Test.Action.Click);
19812
+ Siesta.Test.ActionRegistry.registerAction('click', Siesta.Test.Action.Click);
19813
+ ;
19320
19814
  /**
19321
19815
 
19322
19816
  @class Siesta.Test.Action.DoubleClick
@@ -19329,8 +19823,9 @@ This action can be included in the `t.chain` call with "doubleclick" or "doubleC
19329
19823
 
19330
19824
  t.chain(
19331
19825
  {
19332
- action : 'click',
19333
- target : someDOMElement
19826
+ action : 'doubleclick',
19827
+ target : someDOMElement,
19828
+ options : { shiftKey : true } // Optionally hold shiftkey
19334
19829
  }
19335
19830
  )
19336
19831
 
@@ -19343,21 +19838,28 @@ Class('Siesta.Test.Action.DoubleClick', {
19343
19838
  does : Siesta.Test.Action.Role.HasTarget,
19344
19839
 
19345
19840
  has : {
19346
- requiredTestMethod : 'doubleClick'
19841
+ requiredTestMethod : 'doubleClick',
19842
+
19843
+ /**
19844
+ * @cfg {Object} options
19845
+ *
19846
+ * Any options that will be used when simulating the event. For information about possible
19847
+ * config options, please see: https://developer.mozilla.org/en-US/docs/DOM/event.initMouseEvent
19848
+ */
19849
+ options : null
19347
19850
  },
19348
19851
 
19349
19852
 
19350
19853
  methods : {
19351
19854
 
19352
19855
  process : function () {
19353
- this.test.doubleClick(this.getTarget(), this.next)
19856
+ this.test.doubleClick(this.getTarget(), this.next, null, this.options)
19354
19857
  }
19355
19858
  }
19356
19859
  });
19357
19860
 
19358
19861
 
19359
19862
  Siesta.Test.ActionRegistry.registerAction('doubleclick', Siesta.Test.Action.DoubleClick)
19360
- Siesta.Test.ActionRegistry.registerAction('doubletap', Siesta.Test.Action.DoubleClick)
19361
19863
  ;
19362
19864
  /**
19363
19865
 
@@ -19372,7 +19874,8 @@ This action can be included in the `t.chain` call with "rightclick" or "rightCli
19372
19874
  t.chain(
19373
19875
  {
19374
19876
  action : 'rightclick',
19375
- target : someDOMElement
19877
+ target : someDOMElement,
19878
+ options : { shiftKey : true } // Optionally hold shiftkey
19376
19879
  }
19377
19880
  )
19378
19881
 
@@ -19385,7 +19888,15 @@ Class('Siesta.Test.Action.RightClick', {
19385
19888
  does : Siesta.Test.Action.Role.HasTarget,
19386
19889
 
19387
19890
  has : {
19388
- requiredTestMethod : 'rightClick'
19891
+ requiredTestMethod : 'rightClick',
19892
+
19893
+ /**
19894
+ * @cfg {Object} options
19895
+ *
19896
+ * Any options that will be used when simulating the event. For information about possible
19897
+ * config options, please see: https://developer.mozilla.org/en-US/docs/DOM/event.initMouseEvent
19898
+ */
19899
+ options : null
19389
19900
  },
19390
19901
 
19391
19902
 
@@ -19445,6 +19956,7 @@ Class('Siesta.Test.Action.Type', {
19445
19956
  // By default use the current focused element as target
19446
19957
  this.target = this.target || this.test.global.document.activeElement;
19447
19958
 
19959
+ // additional "getTarget" to allow functions as "target" value
19448
19960
  this.test.type(this.getTarget(), this.text, this.next)
19449
19961
  }
19450
19962
  }
@@ -19831,7 +20343,7 @@ Role('Siesta.Test.Simulate.Mouse', {
19831
20343
 
19832
20344
  // Normalize target
19833
20345
  if (!this.isArray(target)) {
19834
- target = this.detectCenter(this.normalizeElement(target));
20346
+ target = this.detectCenter(this.normalizeElement(target), 'moveMouseTo');
19835
20347
  }
19836
20348
  this.moveMouse(this.currentPosition, target, callback, scope);
19837
20349
  },
@@ -19943,12 +20455,12 @@ Role('Siesta.Test.Simulate.Mouse', {
19943
20455
  queue.run(function () {
19944
20456
  me.endAsync(a);
19945
20457
 
19946
- callback && callback.call(scope || me);
20458
+ callback && me.processCallbackFromTest(callback, null, scope || me)
19947
20459
  })
19948
20460
  },
19949
20461
 
19950
20462
 
19951
- normalizeClickTarget : function (el) {
20463
+ normalizeClickTarget : function (el, clickMethod) {
19952
20464
  var doc = this.global.document
19953
20465
  var xy
19954
20466
 
@@ -19957,13 +20469,12 @@ Role('Siesta.Test.Simulate.Mouse', {
19957
20469
  if (this.isArray(el)) {
19958
20470
  xy = el;
19959
20471
  el = doc.elementFromPoint(xy[0], xy[1]) || doc.body;
19960
- options = { clientX : xy[0], clientY : xy[1] };
19961
20472
  } else {
19962
20473
  el = this.normalizeElement(el)
19963
20474
  doc = el.ownerDocument
19964
- xy = this.detectCenter(el);
20475
+ xy = this.detectCenter(el, clickMethod);
19965
20476
  el = doc.elementFromPoint(xy[0], xy[1]) || doc.body;
19966
- options = { clientX : xy[0], clientY : xy[1] };
20477
+ el && this.$(el).is(':visible');
19967
20478
  }
19968
20479
 
19969
20480
  if (!el) {
@@ -19978,14 +20489,18 @@ Role('Siesta.Test.Simulate.Mouse', {
19978
20489
  },
19979
20490
 
19980
20491
 
19981
- genericMouseClick : function (el, callback, scope, clickMethod) {
20492
+ genericMouseClick : function (el, callback, scope, options, clickMethod) {
19982
20493
  if (jQuery.isFunction(el)) {
19983
20494
  scope = callback;
19984
20495
  callback = el;
19985
20496
  el = null;
19986
20497
  }
19987
20498
 
19988
- var data = this.normalizeClickTarget(el)
20499
+ var data = this.normalizeClickTarget(el, clickMethod);
20500
+
20501
+ data.options = data.options || {};
20502
+
20503
+ $.extend(data.options, options);
19989
20504
 
19990
20505
  // the asynchronous case
19991
20506
  if (this.moveCursorBetweenPoints && callback) {
@@ -20016,10 +20531,11 @@ Role('Siesta.Test.Simulate.Mouse', {
20016
20531
  *
20017
20532
  * @param {Siesta.Test.ActionTarget} (optional) el One of the {@link Siesta.Test.ActionTarget} values to convert to DOM element
20018
20533
  * @param {Function} callback (optional) A function to call when the condition has been met.
20019
- * @param {Object} scope (optional) The scope for the callback
20534
+ * @param {Object} scope (optional) The scope for the callback
20535
+ * @param {Object} options (optional) Any options to use for the simulated DOM event
20020
20536
  */
20021
- click: function (el, callback, scope) {
20022
- this.genericMouseClick(el, callback, scope, 'simulateMouseClick')
20537
+ click: function (el, callback, scope, options) {
20538
+ this.genericMouseClick(el, callback, scope, options, 'simulateMouseClick')
20023
20539
  },
20024
20540
 
20025
20541
 
@@ -20043,10 +20559,11 @@ Role('Siesta.Test.Simulate.Mouse', {
20043
20559
  *
20044
20560
  * @param {Siesta.Test.ActionTarget} (optional) el One of the {@link Siesta.Test.ActionTarget} values to convert to DOM element
20045
20561
  * @param {Function} callback (optional) A function to call when the condition has been met.
20046
- * @param {Object} scope (optional) The scope for the callback
20562
+ * @param {Object} scope (optional) The scope for the callback
20563
+ * @param {Object} options (optional) Any options to use for the simulated DOM event
20047
20564
  */
20048
- rightClick: function (el, callback, scope) {
20049
- this.genericMouseClick(el, callback, scope, 'simulateRightClick')
20565
+ rightClick: function (el, callback, scope, options) {
20566
+ this.genericMouseClick(el, callback, scope, options, 'simulateRightClick')
20050
20567
  },
20051
20568
 
20052
20569
 
@@ -20070,10 +20587,11 @@ Role('Siesta.Test.Simulate.Mouse', {
20070
20587
  *
20071
20588
  * @param {Siesta.Test.ActionTarget} (optional) el One of the {@link Siesta.Test.ActionTarget} values to convert to DOM element
20072
20589
  * @param {Function} callback (optional) A function to call when the condition has been met.
20073
- * @param {Object} scope (optional) The scope for the callback
20590
+ * @param {Object} scope (optional) The scope for the callback
20591
+ * @param {Object} options (optional) Any options to use for the simulated DOM event
20074
20592
  */
20075
- doubleClick: function (el, callback, scope) {
20076
- this.genericMouseClick(el, callback, scope, 'simulateDoubleClick')
20593
+ doubleClick: function (el, callback, scope, options) {
20594
+ this.genericMouseClick(el, callback, scope, options, 'simulateDoubleClick')
20077
20595
  },
20078
20596
 
20079
20597
  /**
@@ -20173,7 +20691,7 @@ Role('Siesta.Test.Simulate.Mouse', {
20173
20691
  queue.run(function () {
20174
20692
  me.endAsync(async);
20175
20693
 
20176
- callback && callback.call(scope || me);
20694
+ callback && me.processCallbackFromTest(callback, null, scope || me)
20177
20695
  })
20178
20696
  },
20179
20697
 
@@ -20212,7 +20730,7 @@ Role('Siesta.Test.Simulate.Mouse', {
20212
20730
  queue.run(function () {
20213
20731
  me.endAsync(async);
20214
20732
 
20215
- callback && callback.call(scope || me);
20733
+ callback && me.processCallbackFromTest(callback, null, scope || me)
20216
20734
  })
20217
20735
  },
20218
20736
 
@@ -20255,7 +20773,7 @@ Role('Siesta.Test.Simulate.Mouse', {
20255
20773
  queue.run(function () {
20256
20774
  me.endAsync(async);
20257
20775
 
20258
- callback && callback.call(scope || me);
20776
+ callback && me.processCallbackFromTest(callback, null, scope || me)
20259
20777
  })
20260
20778
  },
20261
20779
 
@@ -20328,7 +20846,7 @@ Role('Siesta.Test.Simulate.Mouse', {
20328
20846
  if (this.isArray(source)) {
20329
20847
  sourceXY = source;
20330
20848
  } else {
20331
- sourceXY = this.detectCenter(this.normalizeElement(source));
20849
+ sourceXY = this.detectCenter(this.normalizeElement(source), 'dragTo');
20332
20850
  }
20333
20851
 
20334
20852
  // Normalize target
@@ -20371,7 +20889,7 @@ Role('Siesta.Test.Simulate.Mouse', {
20371
20889
  if (this.isArray(source)) {
20372
20890
  sourceXY = source;
20373
20891
  } else {
20374
- sourceXY = this.detectCenter(this.normalizeElement(source));
20892
+ sourceXY = this.detectCenter(this.normalizeElement(source), 'dragBy');
20375
20893
  }
20376
20894
  targetXY = [ sourceXY[0] + delta[0], sourceXY[1] + delta[1] ];
20377
20895
 
@@ -20465,16 +20983,21 @@ Role('Siesta.Test.Simulate.Mouse', {
20465
20983
  queue.run(function () {
20466
20984
  me.endAsync(async)
20467
20985
 
20468
- callback && callback.call(scope || me)
20986
+ callback && me.processCallbackFromTest(callback, null, scope || me)
20469
20987
  });
20470
20988
  },
20471
20989
 
20472
- detectCenter : function(el) {
20990
+ detectCenter : function (el, actionName, skipWarning) {
20473
20991
  var hidden = !this.isElementVisible(el);
20474
20992
 
20475
20993
  // Trigger mouseover in case source is hidden, possibly shown only when hovering over it (its x/y cannot be determined if display:none)
20476
20994
  if (hidden) {
20477
- this.simulateEvent(el, "mouseover", { clientX: 0, clientY: 0});
20995
+ this.simulateEvent(el, "mouseover", { clientX: 0, clientY: 0 });
20996
+
20997
+ if (!skipWarning && !this.isElementVisible(el)) this.fail(
20998
+ (actionName ? "Target element of action [" + actionName + "]" : "Target element of some action") +
20999
+ " is not visible: " + (el.id ? '#' + el.id : el)
21000
+ )
20478
21001
  }
20479
21002
  var center = this.findCenter(el);
20480
21003
  if (hidden) {
@@ -20817,21 +21340,22 @@ Role('Siesta.Test.Simulate.Keyboard', {
20817
21340
  * @param {Object} scope (optional) the scope for the callback
20818
21341
  */
20819
21342
  type: function (el, text, callback, scope) {
20820
- el = this.normalizeElement(el || this.getElementAtCursor());
21343
+ el = this.normalizeElement(el || this.global.document.activeElement);
20821
21344
 
20822
21345
  // Some browsers (IE/FF) do not overwrite selected text, do it manually.
20823
21346
  var selText = this.getSelectedText(el);
21347
+
20824
21348
  if (selText) {
20825
21349
  el.value = el.value.replace(selText, '');
20826
21350
  }
20827
21351
 
21352
+ var me = this
21353
+
20828
21354
  if (el.readOnly || el.disabled) {
20829
- callback && callback.call(scope || me)
21355
+ callback && me.processCallbackFromTest(callback, null, scope || me)
20830
21356
 
20831
21357
  return;
20832
21358
  }
20833
-
20834
- var me = this
20835
21359
 
20836
21360
  // Extract normal chars, or special keys in brackets such as [TAB], [RIGHT] or [ENTER]
20837
21361
  var keys = (text + '').match(/\[([^\])]+\])|([^\[])/g) || [];
@@ -20881,7 +21405,7 @@ Role('Siesta.Test.Simulate.Keyboard', {
20881
21405
  queue.run(function () {
20882
21406
  me.endAsync(async)
20883
21407
 
20884
- callback && callback.call(scope || me)
21408
+ callback && me.processCallbackFromTest(callback, null, scope || me)
20885
21409
  })
20886
21410
  },
20887
21411
 
@@ -21168,6 +21692,37 @@ Role('Siesta.Test.ExtJSCore', {
21168
21692
  return {
21169
21693
  ready : true
21170
21694
  }
21695
+ },
21696
+
21697
+ // Overridden to deal with the different event firing mechanisms in Ext JS 3 vs 4
21698
+ // This code is required because in IE are simulated using fireEvent instead of dispatchEvent and it seems fireEvent will
21699
+ // will not update a checkbox 'checked' state properly so we're forcing the toggle to solve this situation.
21700
+ // This issue is only relevant in IE + Ext.
21701
+ //
21702
+ // Test case: 507_form_checkbox.t.js
21703
+ simulateMouseClick: function (el, callback, scope) {
21704
+
21705
+ // Force check toggle for input checkboxes
21706
+ if (this.simulateEventsWith === 'fireEvent' && (el.type === 'checkbox' || el.type === 'radio') && !el.disabled && !el.readOnly) {
21707
+ var oldState = el.checked;
21708
+
21709
+ if (callback) {
21710
+ this.SUPER(el, function() {
21711
+ if (el.checked === oldState) {
21712
+ el.checked = !oldState;
21713
+ }
21714
+ callback.call(scope || this);
21715
+ });
21716
+ } else {
21717
+ this.SUPER(el);
21718
+
21719
+ if (el.checked === oldState) {
21720
+ el.checked = !oldState;
21721
+ }
21722
+ }
21723
+ } else {
21724
+ this.SUPERARG(arguments);
21725
+ }
21171
21726
  }
21172
21727
  },
21173
21728
 
@@ -21271,11 +21826,33 @@ Role('Siesta.Test.ExtJSCore', {
21271
21826
 
21272
21827
  return component;
21273
21828
  },
21274
-
21275
-
21276
- compToEl : function (comp) {
21829
+
21830
+ /**
21831
+ * @private
21832
+ * @param {Ext.Component} comp the Ext.Component
21833
+ * @param {Boolean} locateInputEl For form fields, try to find the inner input element by default.
21834
+ * If you want to target the containing Component element, pass false instead.
21835
+ * @return {*}
21836
+ */
21837
+ compToEl : function (comp, locateInputEl) {
21838
+ var Ext = this.Ext();
21839
+
21277
21840
  if (!comp) return null
21278
-
21841
+
21842
+ locateInputEl = locateInputEl !== false;
21843
+
21844
+ // Ext JS
21845
+ if (Ext && Ext.form && Ext.form.Field && locateInputEl) {
21846
+ if (comp instanceof Ext.form.Field && comp.inputEl){
21847
+ return comp.inputEl;
21848
+ }
21849
+ }
21850
+
21851
+ // Sencha Touch: Form fields can have a child input component
21852
+ if (Ext && Ext.field && Ext.field.Field && comp instanceof Ext.field.Field && locateInputEl) {
21853
+ comp = comp.getComponent();
21854
+ }
21855
+
21279
21856
  // Ext JS vs Sencha Touch
21280
21857
  return comp.getEl ? comp.getEl() : (comp.el || comp.element);
21281
21858
  },
@@ -21305,17 +21882,14 @@ Role('Siesta.Test.ExtJSCore', {
21305
21882
  }
21306
21883
  }
21307
21884
 
21308
- if (el instanceof Ext.form.Field && el.inputEl) {
21309
- el = el.inputEl;
21310
- } else
21311
- if (el instanceof Ext.Component) {
21312
- el = this.compToEl(el);
21313
- var center = this.findCenter(el);
21314
-
21315
- el = this.elementFromPoint(center[0], center[1]) || el;
21316
- }
21317
-
21318
- // ExtJS Element
21885
+ if (Ext && Ext.Component && el instanceof Ext.Component) {
21886
+ el = this.compToEl(el);
21887
+ var center = this.findCenter(el);
21888
+
21889
+ el = this.elementFromPoint(center[0], center[1]) || el;
21890
+ }
21891
+
21892
+ // ExtJS Element
21319
21893
  if (el && el.dom) return el.dom
21320
21894
 
21321
21895
  // will also handle the case of conversion of array with coordinates to el
@@ -21416,7 +21990,7 @@ Role('Siesta.Test.ExtJSCore', {
21416
21990
  me.fail("Class: " + className + " was loaded")
21417
21991
  })
21418
21992
 
21419
- callback && callback()
21993
+ callback && me.processCallbackFromTest(callback)
21420
21994
  }
21421
21995
 
21422
21996
  var timeout = Ext.isIE ? 120000 : 30000,
@@ -21530,8 +22104,9 @@ Role('Siesta.Test.ExtJSCore', {
21530
22104
  cmp = cmp[0];
21531
22105
 
21532
22106
  if (!cmp.rendered) throw 'The source component of the composite query: ' + cmp.id + ' is not yet rendered';
21533
-
21534
- return this.compToEl(cmp).query(result[1]);
22107
+
22108
+
22109
+ return this.compToEl(cmp, false).query(result[1]);
21535
22110
  },
21536
22111
 
21537
22112
  /**
@@ -21809,7 +22384,7 @@ Role('Siesta.Test.ExtJS.Observable', {
21809
22384
  annotation : n + " '" + event + "' events were expected, but " + counter + ' were fired'
21810
22385
  });
21811
22386
 
21812
- callback && callback();
22387
+ callback && me.processCallbackFromTest(callback);
21813
22388
 
21814
22389
  }, timeOut);
21815
22390
 
@@ -22345,7 +22920,9 @@ Role('Siesta.Test.ExtJS.Component', {
22345
22920
  * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value.
22346
22921
  */
22347
22922
  waitForComponent: function (component, rendered, callback, scope, timeout) {
22348
- var Ext = this.getExt();
22923
+ var Ext = this.getExt();
22924
+ var xtype
22925
+
22349
22926
  if (Ext.isString(component)) {
22350
22927
  xtype = Ext.ClassManager.get(component).xtype;
22351
22928
  } else {
@@ -22383,6 +22960,75 @@ Role('Siesta.Test.ExtJS.Component', {
22383
22960
  hasPosition: function (component, x, y, description) {
22384
22961
  component = this.normalizeComponent(component);
22385
22962
  this.isDeeply(component.getPosition(), [x, y], description);
22963
+ },
22964
+
22965
+
22966
+ /**
22967
+ * This assertion accepts variable number of Ext.Component instances (can be also provided as component query string).
22968
+ * Then it calls their "destroy" method and verifies that:
22969
+ * - there were no exceptions during destroy
22970
+ * - that each component was actually destoyed (since destroy can be canceled in the "beforedestroy" event listener)
22971
+ *
22972
+ * @param {Ext.Component/Array[Ext.Component]/String} components A single instance of Ext.Component, an array of such or a string with component query
22973
+ * @param {String} description The description of the assertion
22974
+ */
22975
+ destroysOk : function (components, description) {
22976
+ var Ext = this.Ext();
22977
+
22978
+ if (this.typeOf(components) != 'Array') {
22979
+ if (this.typeOf(components) == 'String')
22980
+ components = this.Ext().ComponentQuery.query(components);
22981
+ else
22982
+ components = [ components ]
22983
+ }
22984
+
22985
+ if (!components.length) {
22986
+ this.fail(description, {
22987
+ assertionName : 'destroysOk',
22988
+ annotation : 'No components provided, or component query returned empty result'
22989
+ })
22990
+
22991
+ return
22992
+ }
22993
+
22994
+ var currentComp
22995
+
22996
+ var e = this.getExceptionCatcher()(function () {
22997
+ Joose.A.each(components, function (component) {
22998
+ currentComp = component
22999
+
23000
+ component.destroy()
23001
+ })
23002
+ })
23003
+
23004
+ if (e !== undefined) {
23005
+ this.fail(description, {
23006
+ assertionName : 'destroysOk',
23007
+ got : e,
23008
+ gotDesc : 'Exception',
23009
+ annotation : 'Exception thrown while calling "destroy" method of ' + currentComp.id
23010
+ })
23011
+
23012
+ return
23013
+ }
23014
+
23015
+ var me = this
23016
+
23017
+ var allDestroyed = Joose.A.each(components, function (component) {
23018
+ // ExtJS ST
23019
+ if (!(component.isDestroyed || component.destroy == Ext.emptyFn)) {
23020
+ me.fail(description, {
23021
+ assertionName : 'destroysOk',
23022
+ annotation : 'Component [' + component.id + '] was not destroyed (probably destroy was canceled in the `beforedestroy` listener)'
23023
+ })
23024
+
23025
+ return false
23026
+ }
23027
+ })
23028
+
23029
+ if (allDestroyed === false) return
23030
+
23031
+ this.pass(description)
22386
23032
  }
22387
23033
  }
22388
23034
  });
@@ -22784,13 +23430,14 @@ Role('Siesta.Test.Element', {
22784
23430
 
22785
23431
 
22786
23432
  /**
22787
- * Returns true if the element is visible.
23433
+ * Returns true if the element is visible, checking jQuery :visible selector + style visibilty value.
22788
23434
  * @param {Siesta.Test.ActionTarget} el The element
22789
23435
  * @return {Boolean}
22790
23436
  */
22791
23437
  isElementVisible : function(el) {
22792
23438
  el = this.normalizeElement(el);
22793
- return !!el && this.$(el).is(':visible');
23439
+ // Jquery :visible doesn't take visibility into account
23440
+ return !!el && this.$(el).is(':visible') && el.style.visibility !== 'hidden';
22794
23441
  },
22795
23442
 
22796
23443
  /**
@@ -22966,7 +23613,7 @@ Role('Siesta.Test.Element', {
22966
23613
  assertionChecker()
22967
23614
  }
22968
23615
 
22969
- callback && callback.call(scope || me);
23616
+ callback && me.processCallbackFromTest(callback, null, scope || me)
22970
23617
  });
22971
23618
  },
22972
23619
 
@@ -23386,11 +24033,11 @@ Role('Siesta.Test.Element', {
23386
24033
  this.fail(description, {
23387
24034
  assertionName : 'elementIsAt',
23388
24035
  got : { x: xy[0], y : xy[1] },
23389
- gotDesc : 'Postion',
24036
+ gotDesc : 'Position',
23390
24037
  annotation : 'No element found at the specified position'
23391
24038
  });
23392
24039
  } else if (allowChildren) {
23393
- if (foundEl === el || $(foundEl).closest(el)) {
24040
+ if (foundEl === el || $(foundEl).closest(el).length > 0) {
23394
24041
  this.pass(description);
23395
24042
  } else {
23396
24043
  this.fail(description, {
@@ -23488,21 +24135,14 @@ Role('Siesta.Test.Element', {
23488
24135
 
23489
24136
  var foundEl = this.$(doc.elementFromPoint(xy[0], xy[1]) || doc.body);
23490
24137
 
23491
- if (!foundEl) {
23492
- this.fail(description, {
23493
- assertionName : 'selectorIsAt',
23494
- got : { x: xy[0], y : xy[1] },
23495
- gotDesc : 'Postion',
23496
- annotation : 'No element matching the passed selector found at the specified position'
23497
- });
23498
- }
23499
-
23500
24138
  if (foundEl.has(selector).length > 0 || foundEl.closest(selector).length > 0) {
23501
24139
  this.pass(description);
23502
24140
  } else {
23503
24141
  this.fail(description, {
24142
+ got : foundEl[0].outerHTML ? foundEl[0].outerHTML : foundEl[0].innerHTML,
24143
+ need : 'Element matching ' + selector,
23504
24144
  assertionName : 'selectorIsAt',
23505
- annotation : 'Passed selector does not match DOM content at xy position'
24145
+ annotation : 'Passed selector does not match any selector at [' + xy + ']'
23506
24146
  });
23507
24147
  }
23508
24148
  },
@@ -23629,7 +24269,11 @@ Role('Siesta.Test.Element', {
23629
24269
  })
23630
24270
  })
23631
24271
 
23632
- if (callback) steps.push(callback)
24272
+ var me = this
24273
+
24274
+ if (callback) steps.push(function () {
24275
+ me.processCallbackFromTest(callback)
24276
+ })
23633
24277
 
23634
24278
  this.chain.apply(this, steps)
23635
24279
  },
@@ -23646,7 +24290,7 @@ Role('Siesta.Test.Element', {
23646
24290
  *
23647
24291
  * t.clickSelector('.my-grid .x-grid-row', function () {})
23648
24292
  *
23649
- * The provided callback will receive
24293
+ * The provided callback will receive an array with DOM elements - result of query.
23650
24294
  *
23651
24295
  *
23652
24296
  * @param {String} selector The selector/xpath query
@@ -23942,7 +24586,7 @@ Class('Siesta.Test.Browser', {
23942
24586
  annotation : n + " '" + event + "' events were expected, but " + counter + ' were fired'
23943
24587
  });
23944
24588
 
23945
- callback && callback();
24589
+ callback && me.processCallbackFromTest(callback, null, scope || me)
23946
24590
 
23947
24591
  }, timeOut);
23948
24592
 
@@ -24077,15 +24721,19 @@ Class('Siesta.Test.ExtJS', {
24077
24721
 
24078
24722
  getExtBundlePath : function() {
24079
24723
  var path;
24080
-
24081
- this.harness.mainPreset.eachResource(function (resource) {
24082
- var desc = resource.asDescriptor();
24083
-
24084
- if (desc.url.match(/ext(?:js)?-\d\.\d+\.\d+.*?\/ext-all(?:-debug)?\.js/)) {
24085
- path = desc.url;
24086
- return false;
24724
+ var testDescriptor = this.harness.getScriptDescriptor(this.url)
24725
+
24726
+ while (testDescriptor && !path) {
24727
+ if (testDescriptor.preload) {
24728
+ Joose.A.each(testDescriptor.preload, function (url) {
24729
+ if (url.match && url.match(/ext(?:js)?-\d\.\d+(?:\.\d+)?.*?\/ext-all(?:-debug)?\.js/)) {
24730
+ path = url;
24731
+ return false;
24732
+ }
24733
+ });
24087
24734
  }
24088
- });
24735
+ testDescriptor = testDescriptor.parent;
24736
+ }
24089
24737
 
24090
24738
  return path;
24091
24739
  },
@@ -24093,17 +24741,21 @@ Class('Siesta.Test.ExtJS', {
24093
24741
 
24094
24742
  getExtBundleFolder : function() {
24095
24743
  var folder;
24744
+ var testDescriptor = this.harness.getScriptDescriptor(this.url)
24096
24745
 
24097
- this.harness.mainPreset.eachResource(function (resource) {
24098
- var desc = resource.asDescriptor();
24099
-
24100
- var regex = /(.*ext(?:js)?-\d\.\d+\.\d+.*?)\/ext-all(?:-debug)?\.js/;
24101
- var match = regex.exec(desc.url);
24746
+ while (testDescriptor && !folder) {
24747
+ if (testDescriptor.preload) {
24748
+ Joose.A.each(testDescriptor.preload, function (url) {
24749
+ var regex = /(.*ext(?:js)?-\d\.\d+(?:\.\d+)?.*?)\/ext-all(?:-debug)?\.js/;
24750
+ var match = regex.exec(url);
24102
24751
 
24103
- if (match) {
24104
- folder = match[1];
24752
+ if (match) {
24753
+ folder = match[1];
24754
+ }
24755
+ });
24105
24756
  }
24106
- });
24757
+ testDescriptor = testDescriptor.parent;
24758
+ }
24107
24759
 
24108
24760
  return folder;
24109
24761
  }
@@ -24182,7 +24834,47 @@ Class('Siesta.Test.SenchaTouch', {
24182
24834
  },
24183
24835
 
24184
24836
  methods : {
24185
-
24837
+ getTouchBundlePath : function() {
24838
+ var path;
24839
+ var testDescriptor = this.harness.getScriptDescriptor(this.url)
24840
+
24841
+ while (testDescriptor && !path) {
24842
+ if (testDescriptor.preload) {
24843
+ Joose.A.each(testDescriptor.preload, function (url) {
24844
+ if (url.match && url.match(/(.*sencha-touch-\d\.\d+\.\d+.*?)\/sencha-touch(.*)\.js/)) {
24845
+ path = url;
24846
+ return false;
24847
+ }
24848
+ });
24849
+ }
24850
+ testDescriptor = testDescriptor.parent;
24851
+ }
24852
+
24853
+ return path;
24854
+ },
24855
+
24856
+
24857
+ getTouchBundleFolder : function() {
24858
+ var folder;
24859
+ var testDescriptor = this.harness.getScriptDescriptor(this.url)
24860
+
24861
+ while (testDescriptor && !folder) {
24862
+ if (testDescriptor.preload) {
24863
+ Joose.A.each(testDescriptor.preload, function (url) {
24864
+ var regex = /(.*sencha-touch-\d\.\d+\.\d+.*?)\/sencha-touch(.*)\.js/;
24865
+ var match = regex.exec(url);
24866
+
24867
+ if (match) {
24868
+ folder = match[1];
24869
+ }
24870
+ });
24871
+ }
24872
+ testDescriptor = testDescriptor.parent;
24873
+ }
24874
+
24875
+ return folder;
24876
+ },
24877
+
24186
24878
  /**
24187
24879
  * This method taps the passed target, which can be of several different types, see {@link Siesta.Test.ActionTarget}
24188
24880
  *
@@ -24191,18 +24883,73 @@ Class('Siesta.Test.SenchaTouch', {
24191
24883
  * @param {Object} scope (optional) The scope for the callback
24192
24884
  */
24193
24885
  tap: function (target, callback, scope) {
24194
- this.click(target, callback, scope);
24195
- },
24196
-
24197
- /**
24198
- * This method double taps the passed target, which can be of several different types, see {@link Siesta.Test.ActionTarget}
24199
- *
24200
- * @param {Siesta.Test.ActionTarget} target Target for this action
24201
- * @param {Function} callback (optional) A function to call after action
24886
+ var me = this;
24887
+
24888
+ target = this.normalizeElement(target);
24889
+
24890
+ var queue = new Siesta.Util.Queue({
24891
+ deferer : this.originalSetTimeout,
24892
+ deferClearer : this.originalClearTimeout,
24893
+
24894
+ interval : callback ? 30 : 0,
24895
+
24896
+ observeTest : this,
24897
+
24898
+ processor : function (data) {
24899
+ me.simulateEvent.apply(me, data);
24900
+ }
24901
+ })
24902
+
24903
+ queue.addStep([ target, "mousedown", {}, false ])
24904
+ queue.addStep([ target, "mouseup", {}, true ])
24905
+
24906
+ var async = me.beginAsync();
24907
+
24908
+ queue.run(function () {
24909
+ me.endAsync(async);
24910
+
24911
+ callback && me.processCallbackFromTest(callback, null, scope || me)
24912
+ })
24913
+ },
24914
+
24915
+ /**
24916
+ * This method double taps the passed target, which can be of several different types, see {@link Siesta.Test.ActionTarget}
24917
+ *
24918
+ * @param {Siesta.Test.ActionTarget} target Target for this action
24919
+ * @param {Function} callback (optional) A function to call after action
24202
24920
  * @param {Object} scope (optional) The scope for the callback
24203
24921
  */
24204
24922
  doubleTap: function (target, callback, scope) {
24205
- this.doubleClick(target, callback, scope);
24923
+ var me = this;
24924
+
24925
+ target = this.normalizeElement(target);
24926
+
24927
+ var queue = new Siesta.Util.Queue({
24928
+ deferer : this.originalSetTimeout,
24929
+ deferClearer : this.originalClearTimeout,
24930
+
24931
+ interval : callback ? 30 : 0,
24932
+
24933
+ observeTest : this,
24934
+
24935
+ processor : function (data) {
24936
+ me.simulateEvent.apply(me, data);
24937
+ }
24938
+ })
24939
+
24940
+ queue.addStep([ target, "mousedown", {}, false ])
24941
+ queue.addStep([ target, "mouseup", {}, true ])
24942
+
24943
+ queue.addStep([ target, "mousedown", {}, false ])
24944
+ queue.addStep([ target, "mouseup", {}, true ])
24945
+
24946
+ var async = me.beginAsync();
24947
+
24948
+ queue.run(function () {
24949
+ me.endAsync(async);
24950
+
24951
+ callback && me.processCallbackFromTest(callback, null, scope || me)
24952
+ })
24206
24953
  },
24207
24954
 
24208
24955
  /**
@@ -24214,11 +24961,17 @@ Class('Siesta.Test.SenchaTouch', {
24214
24961
  */
24215
24962
  longpress: function (target, callback, scope) {
24216
24963
  var Ext = this.Ext();
24964
+ var me = this;
24217
24965
 
24218
24966
  this.simulateEvent(target, 'mousedown');
24219
24967
 
24220
24968
  var amount = Ext.event.recognizer.LongPress.prototype.config.minDuration;
24221
- this.waitFor(amount, callback, scope);
24969
+
24970
+ this.waitFor(amount, function() {
24971
+ me.simulateEvent(target, 'mouseup');
24972
+
24973
+ callback.call(scope || me);
24974
+ });
24222
24975
  },
24223
24976
 
24224
24977
  /**
@@ -24237,30 +24990,41 @@ Class('Siesta.Test.SenchaTouch', {
24237
24990
  start,
24238
24991
  end,
24239
24992
  edgeOffsetRatio = 10;
24993
+
24994
+ // Since this method accepts elements as target, we need to assure that we swipe at least about 150px
24995
+ // using Math.max below etc
24240
24996
 
24241
24997
  switch(direction) {
24242
24998
  case 'u':
24243
24999
  case 'up':
24244
- start = [box.x + box.width/2, (box.y + box.height*9/edgeOffsetRatio)];
24245
- end = [box.x + box.width/2, box.y + box.height/edgeOffsetRatio];
25000
+ start = [box.x + box.width/2, (box.y + box.height*9/edgeOffsetRatio)];
25001
+ end = [box.x + box.width/2, box.y + box.height/edgeOffsetRatio];
25002
+
25003
+ end[1] = Math.min(start[1] - 100, end[1]);
24246
25004
  break;
24247
25005
 
24248
25006
  case 'd':
24249
25007
  case 'down':
24250
- start = [box.x + box.width/2, (box.y + box.height/edgeOffsetRatio)];
24251
- end = [box.x + box.width/2, (box.y + box.height*9/edgeOffsetRatio)];
24252
- break;
25008
+ start = [box.x + box.width/2, (box.y + box.height/edgeOffsetRatio)];
25009
+ end = [box.x + box.width/2, (box.y + box.height*9/edgeOffsetRatio)];
24253
25010
 
24254
- case 'l':
24255
- case 'left':
24256
- start = [box.x + (box.width /edgeOffsetRatio), (box.y + box.height/2)];
24257
- end = [box.x + (box.width * 9/edgeOffsetRatio), (box.y + box.height/2)];
25011
+ end[1] = Math.max(start[1] + 100, end[1]);
24258
25012
  break;
24259
25013
 
24260
25014
  case 'r':
24261
25015
  case 'right':
24262
- start = [box.x + (box.width * 9/edgeOffsetRatio), (box.y + box.height/2)];
24263
- end = [box.x, (box.y + box.height/2)];
25016
+ start = [box.x + (box.width /edgeOffsetRatio), (box.y + box.height/2)];
25017
+ end = [box.x + (box.width * 9/edgeOffsetRatio), (box.y + box.height/2)];
25018
+
25019
+ end[0] = Math.max(start[0] + 100, end[0]);
25020
+ break;
25021
+
25022
+ case 'l':
25023
+ case 'left':
25024
+ start = [box.x + (box.width * 9/edgeOffsetRatio), (box.y + box.height/2)];
25025
+ end = [box.x + (box.width /edgeOffsetRatio), (box.y + box.height/2)];
25026
+
25027
+ end[0] = Math.min(start[0] - 100, end[0]);
24264
25028
  break;
24265
25029
 
24266
25030
  default:
@@ -24270,6 +25034,42 @@ Class('Siesta.Test.SenchaTouch', {
24270
25034
  this.dragTo(start, end, callback, scope);
24271
25035
  },
24272
25036
 
25037
+ /**
25038
+ * This method will simulate a finger move to an xy-coordinate or an element (the center of it)
25039
+ *
25040
+ * @param {Siesta.Test.ActionTarget} target Target point to move the mouse to.
25041
+ * @param {Function} callback (optional) To run this method async, provide a callback method to be called after the operation is completed.
25042
+ * @param {Object} scope (optional) the scope for the callback
25043
+ */
25044
+ moveFingerTo : function(target, callback, scope) {
25045
+ if (!target) {
25046
+ throw 'Trying to call moveFingerTo without a target';
25047
+ }
25048
+
25049
+ // Normalize target
25050
+ if (!this.isArray(target)) {
25051
+ target = this.detectCenter(this.normalizeElement(target), 'moveFingerTo');
25052
+ }
25053
+ this.moveMouse(this.currentPosition, target, callback, scope);
25054
+ },
25055
+
25056
+ /**
25057
+ * This method will simulate a finger move from current position relative by the x and y distances provided.
25058
+ *
25059
+ * @param {Siesta.Test.ActionTarget} target Target point to move the mouse to.
25060
+ * @param {Function} callback (optional) To run this method async, provide a callback method to be called after the operation is completed.
25061
+ * @param {Object} scope (optional) the scope for the callback
25062
+ */
25063
+ moveFingerBy : function(delta, callback, scope) {
25064
+ if (!delta) {
25065
+ throw 'Trying to call moveFingerBy without relative distances';
25066
+ }
25067
+
25068
+ var targetXY = [ this.currentPosition[0] + delta[0], this.currentPosition[1] + delta[1] ];
25069
+
25070
+ this.moveMouseTo(targetXY, callback, scope);
25071
+ },
25072
+
24273
25073
  // /**
24274
25074
  // * This method will simulate a swipe operation between either two points or on a single DOM element.
24275
25075
  // *
@@ -24327,7 +25127,7 @@ Class('Siesta.Test.SenchaTouch', {
24327
25127
  var inner = function() {
24328
25128
  if (checkerFn.call(scope || me, target)) {
24329
25129
  // We're done
24330
- callback.call(scope || me);
25130
+ me.processCallbackFromTest(callback, null, scope || me)
24331
25131
  } else {
24332
25132
  me.swipe(target, direction, function() {
24333
25133
  var as = me.beginAsync();
@@ -24732,7 +25532,9 @@ Class('Siesta.Harness.Browser', {
24732
25532
  */
24733
25533
  viewportHeight : 768,
24734
25534
 
24735
- needUI : true
25535
+ needUI : true,
25536
+
25537
+ isAutomated : false
24736
25538
  },
24737
25539
 
24738
25540
 
@@ -24775,6 +25577,10 @@ Class('Siesta.Harness.Browser', {
24775
25577
 
24776
25578
  onTestUpdate : function (test, result) {
24777
25579
  if (this.viewport) this.viewport.onTestUpdate(test, result)
25580
+
25581
+ if ((result instanceof Siesta.Result.Diagnostic) && result.isWarning && this.needUI) {
25582
+ if (typeof console != 'undefined' && console.warn) console.warn(result + '')
25583
+ }
24778
25584
  },
24779
25585
 
24780
25586
 
@@ -24811,11 +25617,6 @@ Class('Siesta.Harness.Browser', {
24811
25617
  },
24812
25618
 
24813
25619
 
24814
- isAutomated : function () {
24815
- return false
24816
- },
24817
-
24818
-
24819
25620
  configure : function() {
24820
25621
  this.SUPERARG(arguments);
24821
25622
 
@@ -24857,7 +25658,7 @@ Class('Siesta.Harness.Browser', {
24857
25658
 
24858
25659
  // if we here, then we were requested to show the UI for automated launch
24859
25660
  // auto-launch the test suite in this case
24860
- if (me.isAutomated()) SUPER.apply(me, args)
25661
+ if (me.isAutomated) SUPER.apply(me, args)
24861
25662
  };
24862
25663
 
24863
25664
  if (Ext.setup) {
@@ -24892,7 +25693,7 @@ Class('Siesta.Harness.Browser', {
24892
25693
  var sup = this.SUPER
24893
25694
 
24894
25695
  // delay the super setup until dom ready
24895
- if (!this.isAutomated()) {
25696
+ if (!this.isAutomated) {
24896
25697
  Ext.onReady(function () {
24897
25698
  Siesta.supports.init();
24898
25699
 
@@ -25032,16 +25833,28 @@ Class('Siesta.Harness.Browser', {
25032
25833
 
25033
25834
 
25034
25835
  showForcedIFrame : function (iframe, test) {
25035
- Ext.fly(iframe).addCls('tr-iframe-forced')
25036
- Ext.fly(iframe).removeCls('tr-iframe-hidden')
25836
+ $.rebindWindowContext(window);
25837
+ $(iframe).addClass('tr-iframe-forced')
25838
+ $(iframe).removeClass('tr-iframe-hidden')
25037
25839
 
25038
- Ext.fly(iframe).center()
25840
+ $(iframe).center()
25039
25841
  },
25040
25842
 
25041
25843
 
25042
25844
  hideForcedIFrame : function (iframe) {
25043
- Ext.fly(iframe).removeCls('tr-iframe-forced')
25044
- Ext.fly(iframe).addCls('tr-iframe-hidden')
25845
+ $.rebindWindowContext(window);
25846
+ $(iframe).removeClass('tr-iframe-forced')
25847
+ $(iframe).addClass('tr-iframe-hidden')
25848
+ },
25849
+
25850
+ getQueryParam : function (paramName) {
25851
+ var regex = new RegExp('(?:\\?|&)' + paramName + '=(.*?)(?:\\?|&|$)', 'i')
25852
+
25853
+ var match = regex.exec(window.location.search)
25854
+
25855
+ if (!match) return null
25856
+
25857
+ return match[ 1 ]
25045
25858
  }
25046
25859
  }
25047
25860
 
@@ -25220,179 +26033,527 @@ Ext.Container.override({
25220
26033
  }
25221
26034
  })
25222
26035
  ;
25223
- var config = {
25224
- idProperty : 'id',
26036
+ (function () {
26037
+ var config = {
26038
+ idProperty : 'id',
25225
26039
 
25226
- fields : [
25227
- 'id',
25228
- 'url',
25229
-
25230
- 'title',
25231
-
25232
- { name : 'passCount', type : 'int', defaultValue : 0 },
25233
- { name : 'failCount', type : 'int', defaultValue : 0 },
25234
- { name : 'todoPassCount', type : 'int', defaultValue : 0 },
25235
- { name : 'todoFailCount', type : 'int', defaultValue : 0 },
25236
-
25237
- { name : 'time', type : 'int', defaultValue : 0 },
25238
-
25239
- {
25240
- name : 'checked',
25241
- defaultValue : false
26040
+ fields : [
26041
+ 'id',
26042
+ 'url',
26043
+
26044
+ 'title',
26045
+
26046
+ { name : 'passCount', type : 'int', defaultValue : 0 },
26047
+ { name : 'failCount', type : 'int', defaultValue : 0 },
26048
+ { name : 'todoPassCount', type : 'int', defaultValue : 0 },
26049
+ { name : 'todoFailCount', type : 'int', defaultValue : 0 },
26050
+
26051
+ { name : 'time', type : 'int', defaultValue : 0 },
26052
+
26053
+ {
26054
+ name : 'checked',
26055
+ defaultValue : false
26056
+ },
26057
+
26058
+ {
26059
+ name : 'folderStatus',
26060
+ defaultValue : 'yellow'
26061
+ },
26062
+
26063
+ // will be set to true for all tests, once the users clicks "run"
26064
+ 'isStarting',
26065
+ // will be set to true, right before the scope preload begin
26066
+ 'isStarted',
26067
+ // will be set to true, after preload ends and tests launch
26068
+ { name : 'isRunning', type : 'boolean', defaultValue : false },
26069
+ { name : 'isMissing', type : 'boolean', defaultValue : false },
26070
+ { name : 'isFailed', type : 'boolean', defaultValue : false },
26071
+
26072
+ // composite objects
26073
+ 'assertionsStore',
26074
+ 'test',
26075
+ 'descriptor'
26076
+ ]
26077
+ };
26078
+
26079
+ Ext.define('Siesta.Harness.Browser.Model.TestFile', Ext.apply({
26080
+
26081
+ extend : 'Ext.data.Model',
26082
+
26083
+ init : function () {
26084
+ this.internalId = this.getId() || this.internalId
25242
26085
  },
25243
-
25244
- {
25245
- name : 'folderStatus',
25246
- defaultValue : 'yellow'
25247
- },
25248
-
25249
- // will be set to true for all tests, once the users clicks "run"
25250
- 'isStarting',
25251
- // will be set to true, right before the scope preload begin
25252
- 'isStarted',
25253
- // will be set to true, after preload ends and tests launch
25254
- { name : 'isRunning', type : 'boolean', defaultValue : false },
25255
- { name : 'isMissing', type : 'boolean', defaultValue : false },
25256
- { name : 'isFailed', type : 'boolean', defaultValue : false },
25257
-
25258
- // composite objects
25259
- 'assertionsStore',
25260
- 'test',
25261
- 'descriptor'
25262
- ]
25263
- };
25264
26086
 
25265
- Ext.define('Siesta.Harness.Browser.Model.TestFile', Ext.apply({
25266
26087
 
25267
- extend : 'Ext.data.Model',
26088
+ computeFolderStatus : function () {
26089
+ if (!this.childNodes.length) return 'yellow'
25268
26090
 
25269
- init : function () {
25270
- this.internalId = this.getId() || this.internalId
25271
- },
26091
+ var isWorking = false
26092
+ var hasFailed = false
26093
+ var allGreen = true
26094
+
26095
+ Joose.A.each(this.childNodes, function (childNode) {
26096
+
26097
+ if (childNode.isLeaf()) {
26098
+ var test = childNode.get('test')
26099
+
26100
+ if (test && test.isFailed()) {
26101
+ allGreen = false
26102
+ hasFailed = true
26103
+
26104
+ // stop iteration
26105
+ return false
26106
+ }
26107
+
26108
+ if (!test && childNode.get('isStarting')) isWorking = true
26109
+ if (test && !test.isFinished()) isWorking = true
26110
+ if (test && !test.isPassed()) allGreen = false
26111
+ if (!test) allGreen = false
26112
+
26113
+ } else {
26114
+ var status = childNode.computeFolderStatus()
26115
+
26116
+ if (status == 'red') {
26117
+ allGreen = false
26118
+ hasFailed = true
26119
+
26120
+ // stop iteration
26121
+ return false
26122
+ }
26123
+
26124
+ if (status == 'working') {
26125
+ isWorking = true
26126
+
26127
+ // stop iteration
26128
+ return false
26129
+ }
26130
+
26131
+ if (status == 'yellow') allGreen = false
26132
+ }
26133
+ })
26134
+
26135
+ if (isWorking) return 'working'
26136
+ if (hasFailed) return 'red'
26137
+ if (allGreen) return 'green'
26138
+
26139
+ return 'yellow'
26140
+ },
26141
+
26142
+
26143
+ updateFolderStatus : function () {
26144
+ this.set('folderStatus', this.computeFolderStatus())
26145
+
26146
+ var parentNode = this.parentNode
26147
+
26148
+ if (parentNode && !parentNode.isRoot()) parentNode.updateFolderStatus()
26149
+ },
25272
26150
 
26151
+ getFailedAssertions : function () {
26152
+ var failed = [];
26153
+ var as = this.get('assertionsStore');
25273
26154
 
25274
- computeFolderStatus : function () {
25275
- if (!this.childNodes.length) return 'yellow'
26155
+ if (as) {
26156
+ as.each(function (assertion) {
26157
+ if (assertion.get('passed') === false) {
26158
+ failed.push(assertion);
26159
+ }
26160
+ });
26161
+ }
26162
+
26163
+ return failed;
26164
+ }
26165
+ }, (Ext.getVersion && Ext.getVersion('touch')) ? { config : config } : config), function () {
26166
+ var isSenchaTouch = Ext.getVersion && Ext.getVersion('touch')
26167
+
26168
+ if (!isSenchaTouch) {
26169
+ Ext.data.NodeInterface.decorate(this);
26170
+
26171
+ this.override({
26172
+ expand : function () {
26173
+ Ext.suspendLayouts();
26174
+ this.callParent(arguments);
26175
+ Ext.resumeLayouts();
26176
+ }
26177
+ });
26178
+ }
26179
+ })
26180
+ })();
26181
+ ;
26182
+ (function () {
26183
+ var config = {
26184
+ idProperty : 'index',
26185
+
26186
+ fields : [
26187
+ 'index',
26188
+ 'summaryFailure',
26189
+ { name : 'passed', type : 'boolean', defaultValue : false },
26190
+ { name : 'isTodo', type : 'boolean', defaultValue : false },
26191
+ { name : 'isWaitFor', type : 'boolean', defaultValue : false },
26192
+ { name : 'completed', type : 'boolean', defaultValue : false },
26193
+ 'description',
26194
+ 'annotation',
26195
+ 'type',
26196
+ 'sourceLine',
26197
+ 'isWarning',
26198
+
26199
+ // For logging simulated events (will also have a type as for diagnostic messages)
26200
+ { name : 'isSimulatedEvent', type : 'boolean', defaultValue : false },
26201
+ 'eventType'
26202
+ ]
26203
+ };
26204
+
26205
+ Ext.define('Siesta.Harness.Browser.Model.Assertion', Ext.apply({
26206
+ extend : 'Ext.data.Model'
26207
+ }, (Ext.getVersion && Ext.getVersion('touch')) ? { config : config } : config));
26208
+ })();
26209
+ ;
26210
+ Ext.define("Sch.data.FilterableNodeStore", {
26211
+ extend : 'Ext.data.NodeStore',
25276
26212
 
25277
- var isWorking = false
25278
- var hasFailed = false
25279
- var allGreen = true
25280
26213
 
25281
- Joose.A.each(this.childNodes, function (childNode) {
26214
+ onNodeExpand : function (parent, records, suppressEvent) {
26215
+ var visibleRecords = [];
25282
26216
 
25283
- if (childNode.isLeaf()) {
25284
- var test = childNode.get('test')
26217
+ for (var i = 0; i < records.length; i++) {
26218
+ var record = records[ i ];
25285
26219
 
25286
- if (test && test.isFailed()) {
25287
- allGreen = false
25288
- hasFailed = true
25289
-
25290
- // stop iteration
25291
- return false
25292
- }
26220
+ if (!(record.isHidden && record.isHidden() || record.hidden || record.data.hidden)) visibleRecords[ visibleRecords.length ] = record;
26221
+ }
26222
+
26223
+ return this.callParent([ parent, visibleRecords, suppressEvent ]);
26224
+ }
26225
+ });;
26226
+ Ext.define("Sch.data.mixin.FilterableTreeStore", {
26227
+
26228
+ requires : [
26229
+ 'Sch.data.FilterableNodeStore'
26230
+ ],
26231
+
26232
+
26233
+ nodeStoreClassName : 'Sch.data.FilterableNodeStore',
26234
+
26235
+ nodeStore : null,
26236
+
26237
+ isFilteredFlag : false,
26238
+
26239
+
26240
+ /**
26241
+ * Should be called in the constructor of the consuming class, to activate the filteirng functionality.
26242
+ */
26243
+ initTreeFiltering : function () {
26244
+ if (!this.nodeStore) this.nodeStore = this.createNodeStore(this);
26245
+
26246
+ this.addEvents(
26247
+ 'filter-set',
26248
+ 'filter-clear',
26249
+ 'nodestore-datachange-start',
26250
+ 'nodestore-datachange-end'
26251
+ );
26252
+ },
26253
+
26254
+
26255
+ createNodeStore : function (treeStore) {
26256
+ return Ext.create(this.nodeStoreClassName, {
26257
+ treeStore : treeStore,
26258
+ recursive : true,
26259
+ rootVisible : this.rootVisible
26260
+ });
26261
+ },
26262
+
26263
+
26264
+ /**
26265
+ * Clears current filter (if any).
26266
+ *
26267
+ * See also {@link Sch.data.mixin.FilterableTreeStore} for additional information.
26268
+ */
26269
+ clearTreeFilter : function () {
26270
+ if (!this.isTreeFiltered()) return;
26271
+
26272
+ this.refreshNodeStoreContent();
26273
+
26274
+ this.isFilteredFlag = false;
26275
+
26276
+ this.fireEvent('filter-clear', this);
26277
+ },
26278
+
26279
+
26280
+ refreshNodeStoreContent : function (skipUIRefresh) {
26281
+ var root = this.getRootNode(),
26282
+ linearNodes = [];
25293
26283
 
25294
- if (!test && childNode.get('isStarting')) isWorking = true
25295
- if (test && !test.isFinished()) isWorking = true
25296
- if (test && !test.isPassed()) allGreen = false
25297
- if (!test) allGreen = false
26284
+ var rootVisible = this.rootVisible;
26285
+
26286
+ var collectNodes = function (node) {
26287
+ if (node.isHidden && node.isHidden() || node.hidden || node.data.hidden) return;
25298
26288
 
25299
- } else {
25300
- var status = childNode.computeFolderStatus()
26289
+ if (rootVisible || node != root) linearNodes[ linearNodes.length ] = node;
25301
26290
 
25302
- if (status == 'red') {
25303
- allGreen = false
25304
- hasFailed = true
26291
+ if (!node.data.leaf && node.isExpanded()) {
26292
+ var childNodes = node.childNodes,
26293
+ length = childNodes.length;
25305
26294
 
25306
- // stop iteration
25307
- return false
25308
- }
26295
+ for (var k = 0; k < length; k++) collectNodes(childNodes[ k ]);
26296
+ }
26297
+ };
26298
+
26299
+ collectNodes(root);
26300
+
26301
+ this.fireEvent('nodestore-datachange-start', this);
26302
+
26303
+ var nodeStore = this.nodeStore;
26304
+
26305
+ // "loadDataInNodeStore" is a special hook for buffered case
26306
+ // in buffered case, instead of "loadRecords" we need to use "cachePage"
26307
+ if (!this.loadDataInNodeStore || !this.loadDataInNodeStore(linearNodes)) nodeStore.loadRecords(linearNodes);
26308
+
26309
+ // HACK - forcing view to refresh, the usual "refresh" event is blocked by the tree view (see `blockRefresh` property)
26310
+ if (!skipUIRefresh) nodeStore.fireEvent('clear', nodeStore);
26311
+
26312
+ this.fireEvent('nodestore-datachange-end', this);
26313
+ },
26314
+
26315
+
26316
+ /**
26317
+ * Returns the boolean, indicating whether this store is currently filtered
26318
+ *
26319
+ * @return {Boolean}
26320
+ */
26321
+ isTreeFiltered : function () {
26322
+ return this.isFilteredFlag;
26323
+ },
26324
+
26325
+
26326
+ /**
26327
+ * This method filters the tree store. It accept an object with following properties:
26328
+ *
26329
+ * - `filter` - a function to check if the node should be included in the results. It will be called for each **leaf** node in tree and will receive the current node as the first argument.
26330
+ * It should return `true` if node should remain visible, `false` otherwise. The results will also contain all parents nodes of all matching leafs. Results will not include
26331
+ * parent nodes, which do not have at least one matching child.
26332
+ * To call this method for parent nodes too, pass an additional parameter - `checkParents` (see below).
26333
+ * - `scope` - a scope to call the filter with (optional)
26334
+ * - `checkParents` - when set to `true` will also call the `filter` function for each parent node. If function returns `false` for some parent node,
26335
+ * it still may be included in filter results, if some of its children matches the `filter` (see also "shallow" option below). If function returns `true` for some parent node, it will be
26336
+ * included in the filtering results even if it does not have any matching child nodes.
26337
+ * - `shallow` - implies `checkParents`. When set to `true` will stop checking child nodes if the `filter` function return `false` for some parent node. Whole sub-tree, starting
26338
+ * from non-matching parent, will be excluded from filtering results in such case.
26339
+ * - `onlyParents` - alternative for `checkParents`. When set to `true` will only call the provided `filter` function for parent tasks. If
26340
+ * filter returns `true`, parent, and all its direct children leaf will be included in the results. If `filter` returns `false`, parent node still can
26341
+ * be included in the results (w/o direct children leafs), if some of its child nodes matches the filter.
26342
+ * - `fullMathchingParents` - implies `onlyParents`. In this mode, if parent node matches the filter, then not only its direct children
26343
+ * will be included in the results, but a whole sub-tree, starting form matching node.
26344
+ *
26345
+ * Repeated calls to this method will clear previous filters.
26346
+ *
26347
+ * This function can be also called with 2 arguments, which should be the `filter` function and `scope` in such case.
26348
+ *
26349
+ * See also {@link Sch.data.mixin.FilterableTreeStore} for additional information.
26350
+ *
26351
+ * @param {Object} params
26352
+ */
26353
+ filterTreeBy : function (params, scope) {
26354
+ var filter;
26355
+
26356
+ if (arguments.length == 1 && Ext.isObject(arguments[ 0 ])) {
26357
+ scope = params.scope;
26358
+ filter = params.filter;
26359
+ } else {
26360
+ filter = params;
26361
+ params = {};
26362
+ }
26363
+
26364
+ this.fireEvent('nodestore-datachange-start', this);
26365
+
26366
+ params = params || {};
26367
+
26368
+ var shallowScan = params.shallow;
26369
+ var checkParents = params.checkParents || shallowScan;
26370
+ var fullMathchingParents = params.fullMathchingParents;
26371
+ var onlyParents = params.onlyParents || fullMathchingParents;
26372
+ var rootVisible = this.rootVisible;
26373
+
26374
+ if (onlyParents && checkParents) throw new Error("Can't combine `onlyParents` and `checkParents` options");
26375
+
26376
+ var keepTheseParents = {};
26377
+
26378
+ var root = this.getRootNode(),
26379
+ linearNodes = [];
26380
+
26381
+ var includeParentNodesInResults = function (node) {
26382
+ var parent = node.parentNode;
26383
+
26384
+ while (parent && !keepTheseParents[ parent.internalId ]) {
26385
+ keepTheseParents[ parent.internalId ] = true;
25309
26386
 
25310
- if (status == 'working') {
25311
- isWorking = true
26387
+ parent = parent.parentNode;
26388
+ }
26389
+ };
26390
+
26391
+ var collectNodes = function (node) {
26392
+ if (node.isHidden && node.isHidden() || node.hidden || node.data.hidden) return;
26393
+
26394
+ var nodeMatches, childNodes, length, k;
26395
+
26396
+ // `collectNodes` should not be called for leafs at all
26397
+ if (node.data.leaf) {
26398
+ if (filter.call(scope, node, keepTheseParents)) {
26399
+ linearNodes[ linearNodes.length ] = node;
26400
+
26401
+ includeParentNodesInResults(node);
26402
+ }
26403
+ } else {
26404
+ // include _all_ parent nodes in intermediate result set originally, except the root one
26405
+ // intermediate result set will be filtered
26406
+ if (rootVisible || node != root) linearNodes[ linearNodes.length ] = node;
25312
26407
 
25313
- // stop iteration
25314
- return false
26408
+ if (onlyParents) {
26409
+ nodeMatches = filter.call(scope, node);
26410
+
26411
+ childNodes = node.childNodes;
26412
+ length = childNodes.length;
26413
+
26414
+ if (nodeMatches) {
26415
+ keepTheseParents[ node.internalId ] = true;
26416
+
26417
+ includeParentNodesInResults(node);
26418
+
26419
+ if (fullMathchingParents) {
26420
+ node.cascadeBy(function (node) {
26421
+ linearNodes[ linearNodes.length ] = node;
26422
+
26423
+ if (!node.data.leaf) keepTheseParents[ node.internalId ] = true;
26424
+ });
26425
+
26426
+ return;
26427
+ }
26428
+ }
26429
+
26430
+ // at this point nodeMatches and fullMathchingParents can't be both true
26431
+ for (k = 0; k < length; k++)
26432
+ if (nodeMatches && childNodes[ k ].data.leaf)
26433
+ linearNodes[ linearNodes.length ] = childNodes[ k ];
26434
+ else if (!childNodes[ k ].data.leaf)
26435
+ collectNodes(childNodes[ k ]);
26436
+
26437
+ } else {
26438
+ // mark matching nodes to be kept in results
26439
+ if (checkParents) {
26440
+ nodeMatches = filter.call(scope, node, keepTheseParents);
26441
+
26442
+ if (nodeMatches) {
26443
+ keepTheseParents[ node.internalId ] = true;
26444
+
26445
+ includeParentNodesInResults(node);
26446
+ }
26447
+ }
26448
+
26449
+ // recurse if
26450
+ // - we don't check parents
26451
+ // - shallow scan is not enabled
26452
+ // - shallow scan is enabled and parent node matches the filter or it does not, but its and invisible root, so we don't care
26453
+ if (!checkParents || !shallowScan || shallowScan && (nodeMatches || node == root && !rootVisible)) {
26454
+ childNodes = node.childNodes;
26455
+ length = childNodes.length;
26456
+
26457
+ for (k = 0; k < length; k++) collectNodes(childNodes[ k ]);
26458
+ }
25315
26459
  }
25316
-
25317
- if (status == 'yellow') allGreen = false
25318
26460
  }
25319
- })
26461
+ };
26462
+
26463
+ collectNodes(root);
26464
+
26465
+ // additional filtering of the result set
26466
+ // removes the parent nodes which do not match filter themselves and have no macthing children
26467
+ var nodesToKeep = [];
26468
+
26469
+ for (var i = 0, len = linearNodes.length; i < len; i++) {
26470
+ var node = linearNodes[ i ];
26471
+
26472
+ if (node.data.leaf || keepTheseParents[ node.internalId ]) nodesToKeep[ nodesToKeep.length ] = node;
26473
+ }
26474
+
26475
+ var nodeStore = this.nodeStore;
26476
+
26477
+ nodeStore.loadRecords(nodesToKeep, false);
26478
+
26479
+ // HACK - forcing view to refresh, the usual "refresh" event is blocked by the tree view (see `blockRefresh` property)
26480
+ nodeStore.fireEvent('clear', nodeStore);
26481
+
26482
+ this.isFilteredFlag = true;
26483
+
26484
+ this.fireEvent('nodestore-datachange-end', this);
26485
+
26486
+ this.fireEvent('filter-set', this);
26487
+ },
25320
26488
 
25321
- if (isWorking) return 'working'
25322
- if (hasFailed) return 'red'
25323
- if (allGreen) return 'green'
25324
26489
 
25325
- return 'yellow'
26490
+ /**
26491
+ * Hide nodes from the tree store rendering presenation (they still remains in the store).
26492
+ *
26493
+ * See also {@link Sch.data.mixin.FilterableTreeStore} for additional information.
26494
+ *
26495
+ * @param {Function} filter - A filtering function. Will be called for each node in the tree store and receive a current node as 1st argument. Should return `true` to **hide** the node
26496
+ * and `false`, to **keep it visible**.
26497
+ * @param {Object} scope (optional).
26498
+ */
26499
+ hideNodesBy : function (filter, scope) {
26500
+ if (this.isFiltered()) throw new Error("Can't hide nodes of the filtered tree store");
26501
+
26502
+ var me = this;
26503
+ scope = scope || this;
26504
+
26505
+ this.getRootNode().cascadeBy(function (node) {
26506
+ node.hidden = filter.call(scope, node, me);
26507
+ });
26508
+
26509
+ this.refreshNodeStoreContent();
25326
26510
  },
25327
-
25328
-
25329
- updateFolderStatus : function () {
25330
- this.set('folderStatus', this.computeFolderStatus())
25331
26511
 
25332
- var parentNode = this.parentNode
25333
26512
 
25334
- if (parentNode && !parentNode.isRoot()) parentNode.updateFolderStatus()
25335
- },
25336
-
25337
- getFailedAssertions : function() {
25338
- var failed = [];
25339
- var as = this.get('assertionsStore');
25340
-
25341
- if (as) {
25342
- as.each(function(assertion) {
25343
- if (assertion.get('passed') === false) {
25344
- failed.push(assertion);
25345
- }
25346
- });
25347
- }
25348
-
25349
- return failed;
26513
+ /**
26514
+ * Shows all nodes, previously hidden with {@link #hideNodesBy}
26515
+ *
26516
+ * See also {@link Sch.data.mixin.FilterableTreeStore} for additional information.
26517
+ */
26518
+ showAllNodes : function () {
26519
+ this.getRootNode().cascadeBy(function (node) {
26520
+ node.hidden = node.data.hidden = false;
26521
+ });
26522
+
26523
+ this.refreshNodeStoreContent();
26524
+ }
26525
+ });;
26526
+ Ext.define('Siesta.Harness.Browser.Model.TestTreeStore', {
26527
+ extend : 'Ext.data.TreeStore',
26528
+
26529
+ mixins : [
26530
+ 'Sch.data.mixin.FilterableTreeStore'
26531
+ ],
26532
+
26533
+
26534
+ constructor : function () {
26535
+ this.callParent(arguments)
26536
+
26537
+ this.initTreeFiltering()
25350
26538
  }
25351
- }, (Ext.getVersion && Ext.getVersion('touch')) ? { config : config } : config ))
25352
- ;
25353
- var config = {
25354
- idProperty : 'index',
25355
-
25356
- fields : [
25357
- 'index',
25358
- 'summaryFailure',
25359
- { name : 'passed', type : 'boolean', defaultValue : false },
25360
- { name : 'isTodo', type : 'boolean', defaultValue : false },
25361
- { name : 'isWaitFor', type : 'boolean', defaultValue : false },
25362
- { name : 'completed', type : 'boolean', defaultValue : false },
25363
- 'description',
25364
- 'annotation',
25365
- 'type',
25366
- 'sourceLine',
25367
-
25368
- // For logging simulated events (will also have a type as for diagnostic messages)
25369
- { name : 'isSimulatedEvent', type : 'boolean', defaultValue : false },
25370
- 'eventType'
25371
- ]
25372
- }
25373
26539
 
25374
- Ext.define('Siesta.Harness.Browser.Model.Assertion', Ext.apply({
25375
- extend : 'Ext.data.Model'
25376
- }, (Ext.getVersion && Ext.getVersion('touch')) ? { config : config } : config ))
25377
- ;
26540
+ });
25378
26541
  Ext.define('Siesta.Harness.Browser.UI.VersionField', {
25379
26542
 
25380
26543
  extend : 'Ext.form.field.Spinner',
25381
26544
  alias : 'widget.versionfield',
25382
-
25383
-
26545
+
25384
26546
  width : 90,
25385
26547
 
25386
26548
  // versions : read from harness?
25387
26549
  versions : [
25388
26550
  '4.0.2a',
25389
- '4.0.4',
25390
- '4.0.5',
25391
- '4.0.6',
25392
26551
  '4.0.7',
25393
- '4.1.0-beta-2',
25394
- '4.1.0-beta-3',
25395
- '4.1.0'
26552
+ '4.1.0',
26553
+ '4.1.1',
26554
+ '4.1.2',
26555
+ '4.1.3',
26556
+ '4.1.4'
25396
26557
  ],
25397
26558
 
25398
26559
 
@@ -25721,6 +26882,164 @@ Ext.define('Siesta.Harness.Browser.UI.ExtHeader', {
25721
26882
  })
25722
26883
  //eof Siesta.Harness.Browser.UI.ExtHeader
25723
26884
  ;
26885
+ // !XXX when adding new methods to this mixing need to also update the
26886
+ // `setupLockableTree` method in the Sch.mixin.Lockable
26887
+ Ext.define("Sch.mixin.FilterableTreeView", {
26888
+
26889
+ initTreeFiltering : function () {
26890
+ var doInit = function () {
26891
+ var treeStore = this.up('tablepanel').store;
26892
+
26893
+ this.mon(treeStore, 'nodestore-datachange-start', this.onFilterChangeStart, this);
26894
+ this.mon(treeStore, 'nodestore-datachange-end', this.onFilterChangeEnd, this);
26895
+
26896
+ this.mon(treeStore, 'filter-clear', this.onFilterCleared, this);
26897
+ this.mon(treeStore, 'filter-set', this.onFilterSet, this);
26898
+ };
26899
+
26900
+ if (this.rendered)
26901
+ doInit.call(this);
26902
+ else
26903
+ this.on('beforerender', doInit, this, { single : true });
26904
+ },
26905
+
26906
+
26907
+ onFilterChangeStart : function () {
26908
+ Ext.suspendLayouts();
26909
+ },
26910
+
26911
+
26912
+ onFilterChangeEnd : function () {
26913
+ Ext.resumeLayouts();
26914
+ },
26915
+
26916
+
26917
+ onFilterCleared : function () {
26918
+ delete this.toggle;
26919
+
26920
+ var el = this.getEl();
26921
+
26922
+ if (el) el.removeCls('sch-tree-filtered');
26923
+ },
26924
+
26925
+
26926
+ onFilterSet : function () {
26927
+ this.toggle = function () {};
26928
+
26929
+ var el = this.getEl();
26930
+
26931
+ if (el) el.addCls('sch-tree-filtered');
26932
+ }
26933
+ });;
26934
+ Ext.define('Siesta.Harness.Browser.UI.FilterableTreeView', {
26935
+ extend : 'Ext.tree.View',
26936
+ alias : 'widget.filterabletreeview',
26937
+
26938
+ mixins : [
26939
+ 'Sch.mixin.FilterableTreeView'
26940
+ ],
26941
+
26942
+
26943
+ constructor : function () {
26944
+
26945
+ if (!Ext.tree.View.prototype.patched)
26946
+ // PATCH
26947
+ Ext.tree.View.addMembers({
26948
+ patched : true,
26949
+
26950
+ initComponent: function() {
26951
+ var me = this,
26952
+ treeStore = me.panel.getStore();
26953
+
26954
+ if (me.initialConfig.animate === undefined) {
26955
+ me.animate = Ext.enableFx;
26956
+ }
26957
+
26958
+ // BEGIN OF MODIFICATIONS
26959
+ me.store = me.store || new Ext.data.NodeStore({
26960
+ treeStore: treeStore,
26961
+ recursive: true,
26962
+ rootVisible: me.rootVisible
26963
+ });
26964
+
26965
+ me.store.on({
26966
+ beforeexpand: me.onBeforeExpand,
26967
+ expand: me.onExpand,
26968
+ beforecollapse: me.onBeforeCollapse,
26969
+ collapse: me.onCollapse,
26970
+ write: me.onStoreWrite,
26971
+ datachanged: me.onStoreDataChanged,
26972
+ collapsestart: me.beginBulkUpdate,
26973
+ collapsecomplete: me.endBulkUpdate,
26974
+ scope: me
26975
+ });
26976
+
26977
+ if (Ext.versions.extjs.isGreaterThanOrEqual('4.1.2')) {
26978
+ me.mon(treeStore, {
26979
+ scope: me,
26980
+ beforefill: me.onBeforeFill,
26981
+ fillcomplete: me.onFillComplete,
26982
+ beforebulkremove: me.beginBulkUpdate,
26983
+ bulkremovecomplete: me.endBulkUpdate
26984
+ });
26985
+
26986
+ if (!treeStore.remoteSort) {
26987
+ me.mon(treeStore, {
26988
+ scope: me,
26989
+ beforesort: me.onBeforeSort,
26990
+ sort: me.onSort
26991
+ });
26992
+ }
26993
+ }
26994
+ if (me.node && !me.store.node) {
26995
+ me.setRootNode(me.node);
26996
+ }
26997
+ // EOF MODIFICATIONS
26998
+
26999
+ me.animQueue = {};
27000
+ me.animWraps = {};
27001
+ me.addEvents(
27002
+ /**
27003
+ * @event afteritemexpand
27004
+ * Fires after an item has been visually expanded and is visible in the tree.
27005
+ * @param {Ext.data.NodeInterface} node The node that was expanded
27006
+ * @param {Number} index The index of the node
27007
+ * @param {HTMLElement} item The HTML element for the node that was expanded
27008
+ */
27009
+ 'afteritemexpand',
27010
+ /**
27011
+ * @event afteritemcollapse
27012
+ * Fires after an item has been visually collapsed and is no longer visible in the tree.
27013
+ * @param {Ext.data.NodeInterface} node The node that was collapsed
27014
+ * @param {Number} index The index of the node
27015
+ * @param {HTMLElement} item The HTML element for the node that was collapsed
27016
+ */
27017
+ 'afteritemcollapse'
27018
+ );
27019
+ me.callParent(arguments);
27020
+ me.on({
27021
+ element: 'el',
27022
+ scope: me,
27023
+ delegate: me.expanderSelector,
27024
+ mouseover: me.onExpanderMouseOver,
27025
+ mouseout: me.onExpanderMouseOut
27026
+ });
27027
+ me.on({
27028
+ element: 'el',
27029
+ scope: me,
27030
+ delegate: me.checkboxSelector,
27031
+ click: me.onCheckboxChange
27032
+ });
27033
+ }
27034
+ });
27035
+ // EOF PATCH
27036
+
27037
+ this.callParent(arguments)
27038
+
27039
+ this.initTreeFiltering()
27040
+ }
27041
+ })
27042
+ ;
25724
27043
  Ext.define('Siesta.Harness.Browser.UI.Viewport', {
25725
27044
 
25726
27045
  extend : 'Ext.container.Viewport',
@@ -25736,6 +27055,8 @@ Ext.define('Siesta.Harness.Browser.UI.Viewport', {
25736
27055
  // stateful
25737
27056
  selection : null,
25738
27057
  selectedURL : null,
27058
+ filter : null,
27059
+ filterGroups : false,
25739
27060
  // eof stateful
25740
27061
 
25741
27062
  testsStore : null,
@@ -25763,10 +27084,10 @@ Ext.define('Siesta.Harness.Browser.UI.Viewport', {
25763
27084
 
25764
27085
  this.selection = {}
25765
27086
 
25766
- this.applyState(Ext.state.Manager.get(this.getStateId()))
27087
+ this.applyState(this.loadState())
25767
27088
 
25768
27089
 
25769
- var testsStore = this.testsStore = new Ext.data.TreeStore({
27090
+ var testsStore = this.testsStore = new Siesta.Harness.Browser.Model.TestTreeStore({
25770
27091
  model : 'Siesta.Harness.Browser.Model.TestFile',
25771
27092
 
25772
27093
  sortOnLoad : false,
@@ -25815,7 +27136,7 @@ Ext.define('Siesta.Harness.Browser.UI.Viewport', {
25815
27136
  Ext.apply(this, {
25816
27137
  plugins : Ext.isIE ? undefined : new Siesta.Harness.Browser.UI.MouseVisualizer(this.harness),
25817
27138
  slots : true,
25818
-
27139
+
25819
27140
  contextMenu : this.buildContextMenu(),
25820
27141
 
25821
27142
  layout : 'border',
@@ -25832,6 +27153,9 @@ Ext.define('Siesta.Harness.Browser.UI.Viewport', {
25832
27153
 
25833
27154
  animate : !Ext.isIE,
25834
27155
  split : true,
27156
+
27157
+ filter : this.filter,
27158
+ filterGroups : this.filterGroups,
25835
27159
 
25836
27160
  listeners : {
25837
27161
  selectionchange : this.onSelectionChange,
@@ -25839,8 +27163,10 @@ Ext.define('Siesta.Harness.Browser.UI.Viewport', {
25839
27163
 
25840
27164
  itemcontextmenu : this.onFilesContextMenu,
25841
27165
  itemdblclick : this.onTestFileDoubleClick,
27166
+
27167
+ 'filter-group-change' : this.saveState,
25842
27168
 
25843
- scope : this
27169
+ scope : this
25844
27170
  },
25845
27171
 
25846
27172
  store : testsStore
@@ -25879,6 +27205,13 @@ Ext.define('Siesta.Harness.Browser.UI.Viewport', {
25879
27205
 
25880
27206
  // delay is required to avoid recursive loop
25881
27207
  this.on('afterlayout', this.onAfterLayout, this, { single : true, delay : 1 })
27208
+
27209
+ this.slots.filesTree.store.on({
27210
+ 'filter-set' : this.saveState,
27211
+ 'filter-clear' : this.saveState,
27212
+
27213
+ scope : this
27214
+ })
25882
27215
  },
25883
27216
 
25884
27217
 
@@ -25902,9 +27235,9 @@ Ext.define('Siesta.Harness.Browser.UI.Viewport', {
25902
27235
  })
25903
27236
 
25904
27237
  Ext.apply(data, {
25905
- expanded : collapsedNodes[ prevId ] != null ? false : true,
27238
+ expanded : (collapsedNodes[ prevId ] != null || descriptor.expanded === false) ? false : true,
25906
27239
  // || false is required for TreeView - it checks that "checked" field contains Boolean
25907
- checked : me.selection[ prevId ] || false,
27240
+ checked : me.selection.hasOwnProperty(prevId) || false,
25908
27241
 
25909
27242
  folderStatus : 'yellow',
25910
27243
 
@@ -25918,7 +27251,7 @@ Ext.define('Siesta.Harness.Browser.UI.Viewport', {
25918
27251
 
25919
27252
  leaf : true,
25920
27253
  // || false is required for TreeView - it checks that "checked" field contains Boolean
25921
- checked : me.selection[ prevId ] || false,
27254
+ checked : me.selection.hasOwnProperty(prevId) || false,
25922
27255
 
25923
27256
  passCount : 0,
25924
27257
  failCount : 0,
@@ -25991,7 +27324,7 @@ Ext.define('Siesta.Harness.Browser.UI.Viewport', {
25991
27324
  var id = testFile.getId()
25992
27325
 
25993
27326
  if (checked)
25994
- this.selection[ id ] = true
27327
+ this.selection[ id ] = 1
25995
27328
  else
25996
27329
  delete this.selection[ id ]
25997
27330
 
@@ -26042,8 +27375,12 @@ Ext.define('Siesta.Harness.Browser.UI.Viewport', {
26042
27375
  }
26043
27376
  })
26044
27377
 
27378
+ Ext.suspendLayouts();
27379
+
26045
27380
  filesTree.setIconCls('tr-status-running-small')
26046
27381
  filesTree.setTitle('Running...')
27382
+
27383
+ Ext.resumeLayouts();
26047
27384
  },
26048
27385
 
26049
27386
 
@@ -26056,7 +27393,7 @@ Ext.define('Siesta.Harness.Browser.UI.Viewport', {
26056
27393
  testRecord.get('assertionsStore').removeAll(true)
26057
27394
  testRecord.reject();
26058
27395
  // || false is required for TreeView - it checks that "checked" field contains Boolean
26059
- testRecord.set('checked', this.selection[ descriptor.id ] || false)
27396
+ testRecord.set('checked', this.selection.hasOwnProperty(descriptor.id) || false)
26060
27397
  }, this);
26061
27398
  },
26062
27399
 
@@ -26077,7 +27414,7 @@ Ext.define('Siesta.Harness.Browser.UI.Viewport', {
26077
27414
  forEachTestFile : function (func, scope) {
26078
27415
  var nodeStore = this.getNodeStore()
26079
27416
 
26080
- if (nodeStore.isFiltered())
27417
+ if (this.testsStore.isTreeFiltered())
26081
27418
  nodeStore.each(func, scope)
26082
27419
  else
26083
27420
  Ext.Array.each(this.testsStore.tree.flatten(), func, scope)
@@ -26249,6 +27586,8 @@ Ext.define('Siesta.Harness.Browser.UI.Viewport', {
26249
27586
 
26250
27587
  type : result.meta.name,
26251
27588
  sourceLine : result.sourceLine,
27589
+
27590
+ isWarning : result.isWarning,
26252
27591
 
26253
27592
  // For logging simulated events
26254
27593
  isSimulatedEvent : result.isSimulatedEvent,
@@ -26332,15 +27671,17 @@ Ext.define('Siesta.Harness.Browser.UI.Viewport', {
26332
27671
 
26333
27672
 
26334
27673
  setOption : function (name, value) {
26335
-
26336
27674
  switch (name) {
26337
- case 'selection' : return this.selection = value
27675
+ case 'selection' : return this.selection = value
26338
27676
 
26339
- case 'selectedURL' : return this.selectedURL = value
27677
+ case 'selectedURL' : return this.selectedURL = value
26340
27678
 
26341
27679
  case 'collapsedNodes': return this.collapsedNodes = value
26342
27680
 
26343
- default : return this.harness[ name ] = value
27681
+ case 'filter' : return this.filter = value
27682
+ case 'filterGroups' : return this.filterGroups = value
27683
+
27684
+ default : return this.harness[ name ] = value
26344
27685
  }
26345
27686
  },
26346
27687
 
@@ -26357,19 +27698,33 @@ Ext.define('Siesta.Harness.Browser.UI.Viewport', {
26357
27698
  breakOnFail : this.getOption('breakOnFail'),
26358
27699
 
26359
27700
  // UI configs
26360
- selection : this.selection,
26361
27701
  selectedURL : this.selectedURL,
26362
27702
 
26363
- collapsedNodes : this.getCollapsedFolders()
27703
+ selection : this.getCheckedNodes(),
27704
+ collapsedNodes : this.getCollapsedFolders(),
27705
+
27706
+ filter : this.slots ? this.slots.filesTree.getFilterValue() : this.filter,
27707
+ filterGroups : this.slots ? this.slots.filesTree.getFilterGroups() : this.filterGroups
26364
27708
  }
26365
27709
  },
26366
27710
 
26367
27711
 
27712
+ getCheckedNodes : function () {
27713
+ var checked = {}
27714
+
27715
+ Joose.A.each(this.testsStore.tree.flatten(), function (treeNode) {
27716
+ if (treeNode.get('checked')) checked[ treeNode.getId() ] = 1
27717
+ })
27718
+
27719
+ return checked
27720
+ },
27721
+
27722
+
26368
27723
  getCollapsedFolders : function () {
26369
27724
  var collapsed = {}
26370
27725
 
26371
27726
  Joose.A.each(this.testsStore.tree.flatten(), function (treeNode) {
26372
- if (!treeNode.isLeaf() && !treeNode.isExpanded()) collapsed[ treeNode.getId() ] = ''
27727
+ if (!treeNode.isLeaf() && !treeNode.isExpanded()) collapsed[ treeNode.getId() ] = 1
26373
27728
  })
26374
27729
 
26375
27730
  return collapsed
@@ -26401,10 +27756,32 @@ Ext.define('Siesta.Harness.Browser.UI.Viewport', {
26401
27756
 
26402
27757
  this.saveState()
26403
27758
  },
27759
+
27760
+
27761
+ loadState : function () {
27762
+ var stateId = this.getStateId()
27763
+ var state = Ext.state.Manager.get(stateId)
27764
+
27765
+ if (!state) return
27766
+
27767
+ if (!state.collapsedNodes) state.collapsedNodes = Ext.state.Manager.get(stateId + '-collapsed')
27768
+ if (!state.selection) state.selection = Ext.state.Manager.get(stateId + '-selection')
27769
+
27770
+ return state
27771
+ },
26404
27772
 
26405
27773
 
26406
27774
  saveState : function () {
26407
- Ext.state.Manager.set(this.getStateId(), this.getState())
27775
+ var stateId = this.getStateId()
27776
+ var state = this.getState()
27777
+
27778
+ Ext.state.Manager.set(stateId + '-collapsed', state.collapsedNodes)
27779
+ Ext.state.Manager.set(stateId + '-selection', state.selection)
27780
+
27781
+ delete state.collapsedNodes
27782
+ delete state.selection
27783
+
27784
+ Ext.state.Manager.set(stateId, state)
26408
27785
  },
26409
27786
 
26410
27787
 
@@ -26494,7 +27871,7 @@ Ext.define('Siesta.Harness.Browser.UI.Viewport', {
26494
27871
 
26495
27872
  onTestFileDoubleClick : function (view, testFile) {
26496
27873
  // don't launch groups when filtered - will be confusing for user
26497
- if (this.getNodeStore().isFiltered() && !testFile.isLeaf()) return
27874
+ if (this.testsStore.isTreeFiltered() && !testFile.isLeaf()) return
26498
27875
 
26499
27876
  this.launchTest(testFile);
26500
27877
  },
@@ -26539,6 +27916,8 @@ Ext.define('Siesta.Harness.Browser.UI.Viewport', {
26539
27916
 
26540
27917
  // statusIndicatorEl.removeCls([ 'tr-status-running', 'tr-status-allgreen', 'tr-status-bugs' ])
26541
27918
 
27919
+ Ext.suspendLayouts();
27920
+
26542
27921
  var filesTree = this.slots.filesTree
26543
27922
 
26544
27923
  filesTree.setTitle('Totals: ' + totalPassed + ' / ' + totalFailed)
@@ -26551,7 +27930,8 @@ Ext.define('Siesta.Harness.Browser.UI.Viewport', {
26551
27930
  if (isNeutral) filesTree.setIconCls('tr-status-neutral-small')
26552
27931
  if (allGreen) filesTree.setIconCls('tr-status-allgreen-small')
26553
27932
  if (hasFailures) filesTree.setIconCls('tr-status-bugs-small')
26554
-
27933
+
27934
+ Ext.resumeLayouts();
26555
27935
  },
26556
27936
 
26557
27937
  onSettingsMenuBeforeShow : function(hdr, menu) {
@@ -26829,23 +28209,31 @@ Ext.define('Siesta.Harness.Browser.UI.MouseVisualizer', {
26829
28209
  });
26830
28210
  ;
26831
28211
  Ext.define('Siesta.Harness.Browser.UI.TestGrid', {
26832
-
28212
+ extend : 'Ext.tree.Panel',
26833
28213
  alias : 'widget.testgrid',
26834
28214
 
26835
- extend : 'Ext.tree.Panel',
26836
28215
  stateful : true,
26837
28216
  forceFit : true,
26838
28217
  rootVisible : false,
28218
+
26839
28219
  cls : 'tr-testgrid',
26840
28220
  width : 300,
28221
+
28222
+ filter : null,
28223
+
26841
28224
  title : 'Double click a test to run it',
28225
+
28226
+ filterGroups : false,
28227
+
26842
28228
 
26843
28229
  initComponent : function () {
26844
28230
  var me = this;
26845
28231
 
26846
28232
  Ext.apply(this, {
26847
28233
 
28234
+ viewType : 'filterabletreeview',
26848
28235
  viewConfig : {
28236
+ store : this.store.nodeStore,
26849
28237
  enableTextSelection : true,
26850
28238
  toggleOnDblClick : false
26851
28239
  },
@@ -26891,25 +28279,43 @@ Ext.define('Siesta.Harness.Browser.UI.TestGrid', {
26891
28279
 
26892
28280
  itemId : 'trigger',
26893
28281
 
26894
- triggerCls : 'x-form-clear-trigger',
28282
+ trigger1Cls : 'x-form-clear-trigger',
28283
+ trigger2Cls : 'tr-filter-trigger-leaf',
26895
28284
 
26896
- onTriggerClick : function() {
26897
- var treeView = me.getView();
26898
-
26899
- this.setValue('')
26900
- treeView.store.clearFilter();
28285
+ onTrigger1Click : function() {
28286
+ this.reset()
28287
+ },
28288
+
28289
+ onTrigger2Click : function() {
28290
+ me.filterGroups = !me.filterGroups
28291
+
28292
+ this.triggerEl.item(1).toggleCls('tr-filter-trigger-group')
28293
+
28294
+ me.onFilterChange(this, this.getValue())
28295
+
28296
+ me.fireEvent('filter-group-change', me)
26901
28297
  },
26902
28298
 
28299
+
26903
28300
  listeners : {
26904
- change : this.onFilterChange,
26905
- specialkey : this.onFilterSpecialKey,
26906
- scope : this
28301
+ change : this.onFilterChange,
28302
+ specialkey : this.onFilterSpecialKey,
28303
+ scope : this
26907
28304
  }
26908
28305
  }
26909
28306
  ]
26910
28307
  })
26911
28308
 
26912
28309
  this.callParent(arguments);
28310
+
28311
+ var me = this
28312
+
28313
+ this.getView().on('refresh', function () {
28314
+ var trigger = me.down('trigger')
28315
+
28316
+ if (me.filterGroups) trigger.triggerEl.item(1).addCls('tr-filter-trigger-group')
28317
+ if (me.filter) trigger.setValue(me.filter)
28318
+ }, null, { single : true })
26913
28319
  },
26914
28320
 
26915
28321
  onFilterSpecialKey : function(field, e, t) {
@@ -26918,46 +28324,45 @@ Ext.define('Siesta.Harness.Browser.UI.TestGrid', {
26918
28324
  }
26919
28325
  },
26920
28326
 
26921
- onFilterChange : function (field, newValue, oldValue) {
26922
- var treeView = this.getView()
26923
- var nodeStore = treeView.store
26924
- treeView.blockRefresh = false
26925
-
28327
+
28328
+ getFilterValue : function () {
28329
+ return this.down('trigger').getValue()
28330
+ },
28331
+
28332
+
28333
+ getFilterGroups : function () {
28334
+ return this.filterGroups
28335
+ },
28336
+
28337
+
28338
+ onFilterChange : function (field, newValue) {
28339
+ var header = this.header
28340
+
26926
28341
  if (newValue) {
26927
28342
  var regexps = Ext.Array.map(newValue.split(/\s+/), function (token) { return new RegExp(Ext.String.escapeRegex(token), 'i') })
26928
28343
  var length = regexps.length
26929
28344
 
26930
- var filteredById = {}
26931
-
26932
- Ext.Array.each(this.store.tree.flatten(), function (testFile) {
26933
- var title = testFile.get('title')
26934
-
26935
- // blazing fast "for" loop! :)
26936
- for (var i = 0; i < length; i++)
26937
- if (!regexps[ i ].test(title)) return
26938
-
26939
- filteredById[ testFile.getId() ] = true
26940
-
26941
- // also include parent nodes for leafs for better user experience
26942
- if (testFile.isLeaf()) {
26943
- var parent = testFile.parentNode
28345
+ this.store.filterTreeBy({
28346
+ filter : function (testFile) {
28347
+ var title = testFile.get('title')
26944
28348
 
26945
- while (parent) {
26946
- filteredById[ parent.getId() ] = true
28349
+ // blazing fast "for" loop! :)
28350
+ for (var i = 0; i < length; i++)
28351
+ if (!regexps[ i ].test(title)) return false
26947
28352
 
26948
- parent = parent.parentNode
26949
- }
26950
- }
26951
- })
26952
-
26953
- nodeStore.filterBy(function (testFile) {
26954
- return filteredById[ testFile.getId() ]
28353
+ return true
28354
+ },
28355
+ onlyParents : this.filterGroups
26955
28356
  })
26956
28357
 
28358
+ header.down('[type="down"]').disable()
28359
+ header.down('[type="up"]').disable()
26957
28360
  } else {
26958
- nodeStore.clearFilter()
28361
+ this.store.clearTreeFilter()
28362
+
28363
+ header.down('[type="down"]').enable()
28364
+ header.down('[type="up"]').enable()
26959
28365
  }
26960
- treeView.blockRefresh = true;
26961
28366
  },
26962
28367
 
26963
28368
 
@@ -27311,7 +28716,6 @@ Ext.define('Siesta.Harness.Browser.UI.ResultPanel', {
27311
28716
 
27312
28717
 
27313
28718
  showTest : function (testFile) {
27314
-
27315
28719
  if (this.testRecord !== testFile) {
27316
28720
  this.hideIFrame();
27317
28721
 
@@ -27327,16 +28731,11 @@ Ext.define('Siesta.Harness.Browser.UI.ResultPanel', {
27327
28731
 
27328
28732
  grid.reconfigure(testFile.get('assertionsStore'));
27329
28733
 
27330
- if (Ext.suspendLayouts) {
27331
- Ext.suspendLayouts();
27332
- }
27333
-
28734
+ Ext.suspendLayouts();
27334
28735
  // This triggers an unnecessary layout recalc
27335
- this.setTitle(testFile.get('title') || url);
28736
+ this.setTitle(url);
28737
+ Ext.resumeLayouts();
27336
28738
 
27337
- if (Ext.resumeLayouts) {
27338
- Ext.resumeLayouts();
27339
- }
27340
28739
  this.alignIFrame();
27341
28740
  },
27342
28741
 
@@ -27387,8 +28786,11 @@ Ext.define('Siesta.Harness.Browser.UI.AssertionGrid', {
27387
28786
 
27388
28787
  Ext.apply(this, {
27389
28788
  resultTpl : new Ext.XTemplate(
27390
- '<span class="assertion-index">{index}</span><div class="assertion-status"></div><span class="assertion-text">{description}</span>{[this.getAnnotation(values)]}',
28789
+ '<span class="assertion-index">{index}</span><div class="assertion-status"></div><span class="assertion-text">{[this.getDescription(values)]}</span>{[this.getAnnotation(values)]}',
27391
28790
  {
28791
+ getDescription : function (data) {
28792
+ return data.isWarning ? 'WARN: ' + data.description : data.description
28793
+ },
27392
28794
  getAnnotation : function(data) {
27393
28795
  if (data.annotation) {
27394
28796
  return '<pre class="tr-assert-row-annontation">' + Ext.String.htmlEncode(data.annotation) + '</pre>';
@@ -27416,6 +28818,9 @@ Ext.define('Siesta.Harness.Browser.UI.AssertionGrid', {
27416
28818
  disableSelection : true,
27417
28819
  markDirty : false,
27418
28820
 
28821
+ // this should be kept `false` - otherwise assertion grid goes crazy, see #477
28822
+ deferInitialRefresh : false,
28823
+
27419
28824
  getRowClass : function(record, rowIndex, rowParams, store){
27420
28825
  switch (record.data.type) {
27421
28826
  case 'Siesta.Result.Diagnostic':
@@ -27448,6 +28853,10 @@ Ext.define('Siesta.Harness.Browser.UI.AssertionGrid', {
27448
28853
  }
27449
28854
  } else if (record.data.isTodo) {
27450
28855
  metaData.tdCls = value ? 'tr-assert-row-ok-todo-cell' : 'tr-assert-row-bug-todo-cell';
28856
+ } else if (record.data.isWarning) {
28857
+
28858
+ metaData.tdCls = 'tr-warning'
28859
+
27451
28860
  } else {
27452
28861
  metaData.tdCls = value ? 'tr-assert-row-ok-cell' : 'tr-assert-row-bug-cell';
27453
28862
  }
@@ -27657,16 +29066,258 @@ Class('Siesta.Harness.Browser.ExtJS', {
27657
29066
  })
27658
29067
 
27659
29068
 
29069
+ ;
29070
+ /**
29071
+ @class Siesta.Harness.Browser.SenchaTouch
29072
+ @extends Siesta.Harness.Browser
29073
+
29074
+ A Class representing the browser harness. This class provides a web-based UI and defines some additional configuration options.
29075
+
29076
+ The default value of the `testClass` configuration option in this class is {@link Siesta.Test.SenchaTouch}, which inherits from
29077
+ {@link Siesta.Test.Browser} and contains various Sencha Touch-specific assertions. Use this harness class when testing Sencha Touch applications.
29078
+
29079
+ * **Note** Make sure, you've checked the {@link #performSetup} configuration option.
29080
+
29081
+ This file is for reference only, for a getting start guide and manual, please refer to <a href="#!/guide/siesta_getting_started">Getting Started Guide</a>.
29082
+
29083
+ Synopsys
29084
+ ========
29085
+
29086
+ var Harness = Siesta.Harness.Browser.SenchaTouch;
29087
+
29088
+ Harness.configure({
29089
+ title : 'Awesome Sencha Touch Application Test Suite',
29090
+
29091
+ transparentEx : true,
29092
+
29093
+ preload : [
29094
+ "http://cdn.sencha.io/ext-4.0.2a/ext-all-debug.js",
29095
+ "../awesome-project-all.js"
29096
+ ]
29097
+ })
29098
+
29099
+
29100
+ Harness.start(
29101
+ // simple string - url relative to harness file
29102
+ 'sanity.t.js',
29103
+
29104
+ // test file descriptor with own configuration options
29105
+ {
29106
+ url : 'basic.t.js',
29107
+
29108
+ // replace `preload` option of harness
29109
+ preload : [
29110
+ "http://cdn.sencha.io/ext-4.0.6/ext-all-debug.js",
29111
+ "../awesome-project-all.js"
29112
+ ]
29113
+ },
29114
+
29115
+ // groups ("folders") of test files (possibly with own options)
29116
+ {
29117
+ group : 'Sanity',
29118
+
29119
+ autoCheckGlobals : false,
29120
+
29121
+ items : [
29122
+ 'data/crud.t.js',
29123
+ ...
29124
+ ]
29125
+ },
29126
+ ...
29127
+ )
29128
+
29129
+
29130
+ */
29131
+
29132
+ Class('Siesta.Harness.Browser.SenchaTouch', {
29133
+
29134
+ isa: Siesta.Harness.Browser,
29135
+
29136
+ // pure static class, no need to instantiate it
29137
+ my: {
29138
+
29139
+ has: {
29140
+ /**
29141
+ * @cfg {Class} testClass The test class which will be used for creating test instances, defaults to {@link Siesta.Test.SenchaTouch}.
29142
+ * You can subclass {@link Siesta.Test.SenchaTouch} and provide a new class.
29143
+ *
29144
+ * This option can be also specified in the test file descriptor.
29145
+ */
29146
+ testClass : Siesta.Test.SenchaTouch,
29147
+
29148
+ /**
29149
+ * @cfg {Boolean} transparentEx
29150
+ */
29151
+ transparentEx : true,
29152
+ keepResults : false,
29153
+ keepNLastResults : 0,
29154
+
29155
+ /**
29156
+ * @cfg {Boolean} performSetup When set to `true`, Siesta will perform a `Ext.setup()` call, so you can safely assume there's a viewport for example.
29157
+ * If, however your test code, performs `Ext.setup()` itself, you need to disable this option.
29158
+ *
29159
+ * This option can be also specified in the test file descriptor.
29160
+ */
29161
+ performSetup : true,
29162
+
29163
+ /**
29164
+ * @cfg {String} runCore
29165
+ */
29166
+ runCore : 'sequential',
29167
+
29168
+ /**
29169
+ * @cfg {Object} loaderPath
29170
+ *
29171
+ * The path used to configure the Ext.Loader with, for dynamic loading of Ext JS classes.
29172
+ *
29173
+ * This option can be also specified in the test file descriptor.
29174
+ */
29175
+ loaderPath : null,
29176
+
29177
+ isRunningOnMobile : true,
29178
+ useExtJSUI : true
29179
+ },
29180
+
29181
+
29182
+ methods: {
29183
+
29184
+ setup : function () {
29185
+ // TODO fix proper mobile detection, since Ext may be absent in "no-ui" harness
29186
+ this.isRunningOnMobile = typeof Ext !== 'undefined' && Ext.getVersion && Ext.getVersion('touch')
29187
+
29188
+ if (!this.isRunningOnMobile) this.keepNLastResults = 2
29189
+
29190
+ this.SUPERARG(arguments)
29191
+ },
29192
+
29193
+
29194
+ getNewTestConfiguration: function (desc, scopeProvider, contentManager, options, runFunc) {
29195
+ var config = this.SUPERARG(arguments)
29196
+
29197
+ config.performSetup = this.getDescriptorConfig(desc, 'performSetup')
29198
+ config.loaderPath = this.getDescriptorConfig(desc, 'loaderPath')
29199
+
29200
+ return config
29201
+ },
29202
+
29203
+
29204
+ createViewport: function (config) {
29205
+ if (!this.isRunningOnMobile && this.useExtJSUI) return Ext.create("Siesta.Harness.Browser.UI.ExtViewport", config);
29206
+
29207
+ var mainPanel = Ext.create('Siesta.Harness.Browser.UI_Mobile.MainPanel', config);
29208
+
29209
+ Ext.Viewport.add(mainPanel);
29210
+
29211
+ return mainPanel;
29212
+ },
29213
+
29214
+
29215
+ showForcedIFrame : function (iframe, test) {
29216
+ $.rebindWindowContext(window);
29217
+
29218
+ $(iframe).setStyle({
29219
+ 'z-index' : 100000
29220
+ });
29221
+ },
29222
+
29223
+
29224
+ onBeforeScopePreload : function (scopeProvider, url) {
29225
+ var setupEventTranslation = function() {
29226
+ Ext.event.publisher.TouchGesture.override({
29227
+ moveEventName: 'mousemove',
29228
+
29229
+ map: {
29230
+ mouseToTouch: {
29231
+ mousedown: 'touchstart',
29232
+ mousemove: 'touchmove',
29233
+ mouseup: 'touchend'
29234
+ },
29235
+
29236
+ touchToMouse: {
29237
+ touchstart: 'mousedown',
29238
+ touchmove: 'mousemove',
29239
+ touchend: 'mouseup'
29240
+ }
29241
+ },
29242
+
29243
+ attachListener: function(eventName) {
29244
+ eventName = this.map.touchToMouse[eventName];
29245
+
29246
+ if (!eventName) {
29247
+ return;
29248
+ }
29249
+
29250
+ return this.callOverridden([eventName]);
29251
+ },
29252
+
29253
+ lastEventType: null,
29254
+
29255
+ onEvent: function(e) {
29256
+ if ('button' in e && e.button !== 0) {
29257
+ return;
29258
+ }
29259
+
29260
+ var type = e.type,
29261
+ touchList = [e];
29262
+
29263
+ // Temporary fix for a recent Chrome bugs where events don't seem to bubble up to document
29264
+ // when the element is being animated
29265
+ // with webkit-transition (2 mousedowns without any mouseup)
29266
+ if (type === 'mousedown' && this.lastEventType && this.lastEventType !== 'mouseup') {
29267
+ var fixedEvent = document.createEvent("MouseEvent");
29268
+ fixedEvent.initMouseEvent('mouseup', e.bubbles, e.cancelable,
29269
+ document.defaultView, e.detail, e.screenX, e.screenY, e.clientX,
29270
+ e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.metaKey,
29271
+ e.button, e.relatedTarget);
29272
+
29273
+ this.onEvent(fixedEvent);
29274
+ }
29275
+
29276
+ if (type !== 'mousemove') {
29277
+ this.lastEventType = type;
29278
+ }
29279
+
29280
+ e.identifier = 1;
29281
+ e.touches = (type !== 'mouseup') ? touchList : [];
29282
+ e.targetTouches = (type !== 'mouseup') ? touchList : [];
29283
+ e.changedTouches = touchList;
29284
+
29285
+ return this.callOverridden([e]);
29286
+ },
29287
+
29288
+ processEvent: function(e) {
29289
+ this.eventProcessors[this.map.mouseToTouch[e.type]].call(this, e);
29290
+ }
29291
+ });
29292
+ };
29293
+
29294
+ if ("ontouchstart" in window) {
29295
+
29296
+ // Need to tell ST to convert mouse events to their touch counterpart
29297
+ scopeProvider.addPreload({
29298
+ type : 'js',
29299
+ content : '(' + setupEventTranslation.toString() + ')();'
29300
+ })
29301
+ }
29302
+
29303
+ this.SUPERARG(arguments)
29304
+ }
29305
+ }
29306
+ }
29307
+ })
29308
+
29309
+
27660
29310
  ;
27661
29311
  ;
27662
29312
  Class('Siesta', {
27663
- /*PKGVERSION*/VERSION : '1.1.0',
29313
+ /*PKGVERSION*/VERSION : '1.1.5',
27664
29314
 
27665
29315
  // "my" should been named "static"
27666
29316
  my : {
27667
29317
 
27668
29318
  has : {
27669
- config : null
29319
+ config : null,
29320
+ activeHarness : null
27670
29321
  },
27671
29322
 
27672
29323
  methods : {
@@ -27696,4 +29347,3 @@ Class('Siesta', {
27696
29347
 
27697
29348
  // fake StartTest function to extract test configs
27698
29349
  if (typeof StartTest == 'undefined') StartTest = Siesta.StartTest;
27699
- ;