siesta 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- ;