diff options
author | xue <> | 2006-09-30 18:40:40 +0000 |
---|---|---|
committer | xue <> | 2006-09-30 18:40:40 +0000 |
commit | 1c32172efb18e8d08ea483e2460813670ebfe1a5 (patch) | |
tree | 8420f9e53eaba35d7b4822fac823197254f0d131 /tests/test_tools/selenium | |
parent | 6b1d87352911e43672b46b7a65a3c90dd8e5b8b1 (diff) |
merge from 3.0 branch till 1451.
Diffstat (limited to 'tests/test_tools/selenium')
36 files changed, 16485 insertions, 2077 deletions
diff --git a/tests/test_tools/selenium/core/lib/cssQuery/cssQuery-p.js b/tests/test_tools/selenium/core/lib/cssQuery/cssQuery-p.js new file mode 100644 index 00000000..4a7eb88a --- /dev/null +++ b/tests/test_tools/selenium/core/lib/cssQuery/cssQuery-p.js @@ -0,0 +1,6 @@ +/*
+ cssQuery, version 2.0.2 (2005-08-19)
+ Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+ License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('7 x=6(){7 1D="2.0.2";7 C=/\\s*,\\s*/;7 x=6(s,A){33{7 m=[];7 u=1z.32.2c&&!A;7 b=(A)?(A.31==22)?A:[A]:[1g];7 1E=18(s).1l(C),i;9(i=0;i<1E.y;i++){s=1y(1E[i]);8(U&&s.Z(0,3).2b("")==" *#"){s=s.Z(2);A=24([],b,s[1])}1A A=b;7 j=0,t,f,a,c="";H(j<s.y){t=s[j++];f=s[j++];c+=t+f;a="";8(s[j]=="("){H(s[j++]!=")")a+=s[j];a=a.Z(0,-1);c+="("+a+")"}A=(u&&V[c])?V[c]:21(A,t,f,a);8(u)V[c]=A}m=m.30(A)}2a x.2d;5 m}2Z(e){x.2d=e;5[]}};x.1Z=6(){5"6 x() {\\n [1D "+1D+"]\\n}"};7 V={};x.2c=L;x.2Y=6(s){8(s){s=1y(s).2b("");2a V[s]}1A V={}};7 29={};7 19=L;x.15=6(n,s){8(19)1i("s="+1U(s));29[n]=12 s()};x.2X=6(c){5 c?1i(c):o};7 D={};7 h={};7 q={P:/\\[([\\w-]+(\\|[\\w-]+)?)\\s*(\\W?=)?\\s*([^\\]]*)\\]/};7 T=[];D[" "]=6(r,f,t,n){7 e,i,j;9(i=0;i<f.y;i++){7 s=X(f[i],t,n);9(j=0;(e=s[j]);j++){8(M(e)&&14(e,n))r.z(e)}}};D["#"]=6(r,f,i){7 e,j;9(j=0;(e=f[j]);j++)8(e.B==i)r.z(e)};D["."]=6(r,f,c){c=12 1t("(^|\\\\s)"+c+"(\\\\s|$)");7 e,i;9(i=0;(e=f[i]);i++)8(c.l(e.1V))r.z(e)};D[":"]=6(r,f,p,a){7 t=h[p],e,i;8(t)9(i=0;(e=f[i]);i++)8(t(e,a))r.z(e)};h["2W"]=6(e){7 d=Q(e);8(d.1C)9(7 i=0;i<d.1C.y;i++){8(d.1C[i]==e)5 K}};h["2V"]=6(e){};7 M=6(e){5(e&&e.1c==1&&e.1f!="!")?e:23};7 16=6(e){H(e&&(e=e.2U)&&!M(e))28;5 e};7 G=6(e){H(e&&(e=e.2T)&&!M(e))28;5 e};7 1r=6(e){5 M(e.27)||G(e.27)};7 1P=6(e){5 M(e.26)||16(e.26)};7 1o=6(e){7 c=[];e=1r(e);H(e){c.z(e);e=G(e)}5 c};7 U=K;7 1h=6(e){7 d=Q(e);5(2S d.25=="2R")?/\\.1J$/i.l(d.2Q):2P(d.25=="2O 2N")};7 Q=6(e){5 e.2M||e.1g};7 X=6(e,t){5(t=="*"&&e.1B)?e.1B:e.X(t)};7 17=6(e,t,n){8(t=="*")5 M(e);8(!14(e,n))5 L;8(!1h(e))t=t.2L();5 e.1f==t};7 14=6(e,n){5!n||(n=="*")||(e.2K==n)};7 1e=6(e){5 e.1G};6 24(r,f,B){7 m,i,j;9(i=0;i<f.y;i++){8(m=f[i].1B.2J(B)){8(m.B==B)r.z(m);1A 8(m.y!=23){9(j=0;j<m.y;j++){8(m[j].B==B)r.z(m[j])}}}}5 r};8(![].z)22.2I.z=6(){9(7 i=0;i<1z.y;i++){o[o.y]=1z[i]}5 o.y};7 N=/\\|/;6 21(A,t,f,a){8(N.l(f)){f=f.1l(N);a=f[0];f=f[1]}7 r=[];8(D[t]){D[t](r,A,f,a)}5 r};7 S=/^[^\\s>+~]/;7 20=/[\\s#.:>+~()@]|[^\\s#.:>+~()@]+/g;6 1y(s){8(S.l(s))s=" "+s;5 s.P(20)||[]};7 W=/\\s*([\\s>+~(),]|^|$)\\s*/g;7 I=/([\\s>+~,]|[^(]\\+|^)([#.:@])/g;7 18=6(s){5 s.O(W,"$1").O(I,"$1*$2")};7 1u={1Z:6(){5"\'"},P:/^(\'[^\']*\')|("[^"]*")$/,l:6(s){5 o.P.l(s)},1S:6(s){5 o.l(s)?s:o+s+o},1Y:6(s){5 o.l(s)?s.Z(1,-1):s}};7 1s=6(t){5 1u.1Y(t)};7 E=/([\\/()[\\]?{}|*+-])/g;6 R(s){5 s.O(E,"\\\\$1")};x.15("1j-2H",6(){D[">"]=6(r,f,t,n){7 e,i,j;9(i=0;i<f.y;i++){7 s=1o(f[i]);9(j=0;(e=s[j]);j++)8(17(e,t,n))r.z(e)}};D["+"]=6(r,f,t,n){9(7 i=0;i<f.y;i++){7 e=G(f[i]);8(e&&17(e,t,n))r.z(e)}};D["@"]=6(r,f,a){7 t=T[a].l;7 e,i;9(i=0;(e=f[i]);i++)8(t(e))r.z(e)};h["2G-10"]=6(e){5!16(e)};h["1x"]=6(e,c){c=12 1t("^"+c,"i");H(e&&!e.13("1x"))e=e.1n;5 e&&c.l(e.13("1x"))};q.1X=/\\\\:/g;q.1w="@";q.J={};q.O=6(m,a,n,c,v){7 k=o.1w+m;8(!T[k]){a=o.1W(a,c||"",v||"");T[k]=a;T.z(a)}5 T[k].B};q.1Q=6(s){s=s.O(o.1X,"|");7 m;H(m=s.P(o.P)){7 r=o.O(m[0],m[1],m[2],m[3],m[4]);s=s.O(o.P,r)}5 s};q.1W=6(p,t,v){7 a={};a.B=o.1w+T.y;a.2F=p;t=o.J[t];t=t?t(o.13(p),1s(v)):L;a.l=12 2E("e","5 "+t);5 a};q.13=6(n){1d(n.2D()){F"B":5"e.B";F"2C":5"e.1V";F"9":5"e.2B";F"1T":8(U){5"1U((e.2A.P(/1T=\\\\1v?([^\\\\s\\\\1v]*)\\\\1v?/)||[])[1]||\'\')"}}5"e.13(\'"+n.O(N,":")+"\')"};q.J[""]=6(a){5 a};q.J["="]=6(a,v){5 a+"=="+1u.1S(v)};q.J["~="]=6(a,v){5"/(^| )"+R(v)+"( |$)/.l("+a+")"};q.J["|="]=6(a,v){5"/^"+R(v)+"(-|$)/.l("+a+")"};7 1R=18;18=6(s){5 1R(q.1Q(s))}});x.15("1j-2z",6(){D["~"]=6(r,f,t,n){7 e,i;9(i=0;(e=f[i]);i++){H(e=G(e)){8(17(e,t,n))r.z(e)}}};h["2y"]=6(e,t){t=12 1t(R(1s(t)));5 t.l(1e(e))};h["2x"]=6(e){5 e==Q(e).1H};h["2w"]=6(e){7 n,i;9(i=0;(n=e.1F[i]);i++){8(M(n)||n.1c==3)5 L}5 K};h["1N-10"]=6(e){5!G(e)};h["2v-10"]=6(e){e=e.1n;5 1r(e)==1P(e)};h["2u"]=6(e,s){7 n=x(s,Q(e));9(7 i=0;i<n.y;i++){8(n[i]==e)5 L}5 K};h["1O-10"]=6(e,a){5 1p(e,a,16)};h["1O-1N-10"]=6(e,a){5 1p(e,a,G)};h["2t"]=6(e){5 e.B==2s.2r.Z(1)};h["1M"]=6(e){5 e.1M};h["2q"]=6(e){5 e.1q===L};h["1q"]=6(e){5 e.1q};h["1L"]=6(e){5 e.1L};q.J["^="]=6(a,v){5"/^"+R(v)+"/.l("+a+")"};q.J["$="]=6(a,v){5"/"+R(v)+"$/.l("+a+")"};q.J["*="]=6(a,v){5"/"+R(v)+"/.l("+a+")"};6 1p(e,a,t){1d(a){F"n":5 K;F"2p":a="2n";1a;F"2o":a="2n+1"}7 1m=1o(e.1n);6 1k(i){7 i=(t==G)?1m.y-i:i-1;5 1m[i]==e};8(!Y(a))5 1k(a);a=a.1l("n");7 m=1K(a[0]);7 s=1K(a[1]);8((Y(m)||m==1)&&s==0)5 K;8(m==0&&!Y(s))5 1k(s);8(Y(s))s=0;7 c=1;H(e=t(e))c++;8(Y(m)||m==1)5(t==G)?(c<=s):(s>=c);5(c%m)==s}});x.15("1j-2m",6(){U=1i("L;/*@2l@8(@\\2k)U=K@2j@*/");8(!U){X=6(e,t,n){5 n?e.2i("*",t):e.X(t)};14=6(e,n){5!n||(n=="*")||(e.2h==n)};1h=1g.1I?6(e){5/1J/i.l(Q(e).1I)}:6(e){5 Q(e).1H.1f!="2g"};1e=6(e){5 e.2f||e.1G||1b(e)};6 1b(e){7 t="",n,i;9(i=0;(n=e.1F[i]);i++){1d(n.1c){F 11:F 1:t+=1b(n);1a;F 3:t+=n.2e;1a}}5 t}}});19=K;5 x}();',62,190,'|||||return|function|var|if|for||||||||pseudoClasses||||test|||this||AttributeSelector|||||||cssQuery|length|push|fr|id||selectors||case|nextElementSibling|while||tests|true|false|thisElement||replace|match|getDocument|regEscape||attributeSelectors|isMSIE|cache||getElementsByTagName|isNaN|slice|child||new|getAttribute|compareNamespace|addModule|previousElementSibling|compareTagName|parseSelector|loaded|break|_0|nodeType|switch|getTextContent|tagName|document|isXML|eval|css|_1|split|ch|parentNode|childElements|nthChild|disabled|firstElementChild|getText|RegExp|Quote|x22|PREFIX|lang|_2|arguments|else|all|links|version|se|childNodes|innerText|documentElement|contentType|xml|parseInt|indeterminate|checked|last|nth|lastElementChild|parse|_3|add|href|String|className|create|NS_IE|remove|toString|ST|select|Array|null|_4|mimeType|lastChild|firstChild|continue|modules|delete|join|caching|error|nodeValue|textContent|HTML|prefix|getElementsByTagNameNS|end|x5fwin32|cc_on|standard||odd|even|enabled|hash|location|target|not|only|empty|root|contains|level3|outerHTML|htmlFor|class|toLowerCase|Function|name|first|level2|prototype|item|scopeName|toUpperCase|ownerDocument|Document|XML|Boolean|URL|unknown|typeof|nextSibling|previousSibling|visited|link|valueOf|clearCache|catch|concat|constructor|callee|try'.split('|'),0,{}))
diff --git a/tests/test_tools/selenium/core/lib/cssQuery/src/cssQuery-level2.js b/tests/test_tools/selenium/core/lib/cssQuery/src/cssQuery-level2.js new file mode 100644 index 00000000..02dd0e5f --- /dev/null +++ b/tests/test_tools/selenium/core/lib/cssQuery/src/cssQuery-level2.js @@ -0,0 +1,142 @@ +/*
+ cssQuery, version 2.0.2 (2005-08-19)
+ Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+ License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+
+cssQuery.addModule("css-level2", function() {
+
+// -----------------------------------------------------------------------
+// selectors
+// -----------------------------------------------------------------------
+
+// child selector
+selectors[">"] = function($results, $from, $tagName, $namespace) {
+ var $element, i, j;
+ for (i = 0; i < $from.length; i++) {
+ var $subset = childElements($from[i]);
+ for (j = 0; ($element = $subset[j]); j++)
+ if (compareTagName($element, $tagName, $namespace))
+ $results.push($element);
+ }
+};
+
+// sibling selector
+selectors["+"] = function($results, $from, $tagName, $namespace) {
+ for (var i = 0; i < $from.length; i++) {
+ var $element = nextElementSibling($from[i]);
+ if ($element && compareTagName($element, $tagName, $namespace))
+ $results.push($element);
+ }
+};
+
+// attribute selector
+selectors["@"] = function($results, $from, $attributeSelectorID) {
+ var $test = attributeSelectors[$attributeSelectorID].test;
+ var $element, i;
+ for (i = 0; ($element = $from[i]); i++)
+ if ($test($element)) $results.push($element);
+};
+
+// -----------------------------------------------------------------------
+// pseudo-classes
+// -----------------------------------------------------------------------
+
+pseudoClasses["first-child"] = function($element) {
+ return !previousElementSibling($element);
+};
+
+pseudoClasses["lang"] = function($element, $code) {
+ $code = new RegExp("^" + $code, "i");
+ while ($element && !$element.getAttribute("lang")) $element = $element.parentNode;
+ return $element && $code.test($element.getAttribute("lang"));
+};
+
+// -----------------------------------------------------------------------
+// attribute selectors
+// -----------------------------------------------------------------------
+
+// constants
+AttributeSelector.NS_IE = /\\:/g;
+AttributeSelector.PREFIX = "@";
+// properties
+AttributeSelector.tests = {};
+// methods
+AttributeSelector.replace = function($match, $attribute, $namespace, $compare, $value) {
+ var $key = this.PREFIX + $match;
+ if (!attributeSelectors[$key]) {
+ $attribute = this.create($attribute, $compare || "", $value || "");
+ // store the selector
+ attributeSelectors[$key] = $attribute;
+ attributeSelectors.push($attribute);
+ }
+ return attributeSelectors[$key].id;
+};
+AttributeSelector.parse = function($selector) {
+ $selector = $selector.replace(this.NS_IE, "|");
+ var $match;
+ while ($match = $selector.match(this.match)) {
+ var $replace = this.replace($match[0], $match[1], $match[2], $match[3], $match[4]);
+ $selector = $selector.replace(this.match, $replace);
+ }
+ return $selector;
+};
+AttributeSelector.create = function($propertyName, $test, $value) {
+ var $attributeSelector = {};
+ $attributeSelector.id = this.PREFIX + attributeSelectors.length;
+ $attributeSelector.name = $propertyName;
+ $test = this.tests[$test];
+ $test = $test ? $test(this.getAttribute($propertyName), getText($value)) : false;
+ $attributeSelector.test = new Function("e", "return " + $test);
+ return $attributeSelector;
+};
+AttributeSelector.getAttribute = function($name) {
+ switch ($name.toLowerCase()) {
+ case "id":
+ return "e.id";
+ case "class":
+ return "e.className";
+ case "for":
+ return "e.htmlFor";
+ case "href":
+ if (isMSIE) {
+ // IE always returns the full path not the fragment in the href attribute
+ // so we RegExp it out of outerHTML. Opera does the same thing but there
+ // is no way to get the original attribute.
+ return "String((e.outerHTML.match(/href=\\x22?([^\\s\\x22]*)\\x22?/)||[])[1]||'')";
+ }
+ }
+ return "e.getAttribute('" + $name.replace($NAMESPACE, ":") + "')";
+};
+
+// -----------------------------------------------------------------------
+// attribute selector tests
+// -----------------------------------------------------------------------
+
+AttributeSelector.tests[""] = function($attribute) {
+ return $attribute;
+};
+
+AttributeSelector.tests["="] = function($attribute, $value) {
+ return $attribute + "==" + Quote.add($value);
+};
+
+AttributeSelector.tests["~="] = function($attribute, $value) {
+ return "/(^| )" + regEscape($value) + "( |$)/.test(" + $attribute + ")";
+};
+
+AttributeSelector.tests["|="] = function($attribute, $value) {
+ return "/^" + regEscape($value) + "(-|$)/.test(" + $attribute + ")";
+};
+
+// -----------------------------------------------------------------------
+// parsing
+// -----------------------------------------------------------------------
+
+// override parseSelector to parse out attribute selectors
+var _parseSelector = parseSelector;
+parseSelector = function($selector) {
+ return _parseSelector(AttributeSelector.parse($selector));
+};
+
+}); // addModule
diff --git a/tests/test_tools/selenium/core/lib/cssQuery/src/cssQuery-level3.js b/tests/test_tools/selenium/core/lib/cssQuery/src/cssQuery-level3.js new file mode 100644 index 00000000..11d19664 --- /dev/null +++ b/tests/test_tools/selenium/core/lib/cssQuery/src/cssQuery-level3.js @@ -0,0 +1,150 @@ +/*
+ cssQuery, version 2.0.2 (2005-08-19)
+ Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+ License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+
+/* Thanks to Bill Edney */
+
+cssQuery.addModule("css-level3", function() {
+
+// -----------------------------------------------------------------------
+// selectors
+// -----------------------------------------------------------------------
+
+// indirect sibling selector
+selectors["~"] = function($results, $from, $tagName, $namespace) {
+ var $element, i;
+ for (i = 0; ($element = $from[i]); i++) {
+ while ($element = nextElementSibling($element)) {
+ if (compareTagName($element, $tagName, $namespace))
+ $results.push($element);
+ }
+ }
+};
+
+// -----------------------------------------------------------------------
+// pseudo-classes
+// -----------------------------------------------------------------------
+
+// I'm hoping these pseudo-classes are pretty readable. Let me know if
+// any need explanation.
+
+pseudoClasses["contains"] = function($element, $text) {
+ $text = new RegExp(regEscape(getText($text)));
+ return $text.test(getTextContent($element));
+};
+
+pseudoClasses["root"] = function($element) {
+ return $element == getDocument($element).documentElement;
+};
+
+pseudoClasses["empty"] = function($element) {
+ var $node, i;
+ for (i = 0; ($node = $element.childNodes[i]); i++) {
+ if (thisElement($node) || $node.nodeType == 3) return false;
+ }
+ return true;
+};
+
+pseudoClasses["last-child"] = function($element) {
+ return !nextElementSibling($element);
+};
+
+pseudoClasses["only-child"] = function($element) {
+ $element = $element.parentNode;
+ return firstElementChild($element) == lastElementChild($element);
+};
+
+pseudoClasses["not"] = function($element, $selector) {
+ var $negated = cssQuery($selector, getDocument($element));
+ for (var i = 0; i < $negated.length; i++) {
+ if ($negated[i] == $element) return false;
+ }
+ return true;
+};
+
+pseudoClasses["nth-child"] = function($element, $arguments) {
+ return nthChild($element, $arguments, previousElementSibling);
+};
+
+pseudoClasses["nth-last-child"] = function($element, $arguments) {
+ return nthChild($element, $arguments, nextElementSibling);
+};
+
+pseudoClasses["target"] = function($element) {
+ return $element.id == location.hash.slice(1);
+};
+
+// UI element states
+
+pseudoClasses["checked"] = function($element) {
+ return $element.checked;
+};
+
+pseudoClasses["enabled"] = function($element) {
+ return $element.disabled === false;
+};
+
+pseudoClasses["disabled"] = function($element) {
+ return $element.disabled;
+};
+
+pseudoClasses["indeterminate"] = function($element) {
+ return $element.indeterminate;
+};
+
+// -----------------------------------------------------------------------
+// attribute selector tests
+// -----------------------------------------------------------------------
+
+AttributeSelector.tests["^="] = function($attribute, $value) {
+ return "/^" + regEscape($value) + "/.test(" + $attribute + ")";
+};
+
+AttributeSelector.tests["$="] = function($attribute, $value) {
+ return "/" + regEscape($value) + "$/.test(" + $attribute + ")";
+};
+
+AttributeSelector.tests["*="] = function($attribute, $value) {
+ return "/" + regEscape($value) + "/.test(" + $attribute + ")";
+};
+
+// -----------------------------------------------------------------------
+// nth child support (Bill Edney)
+// -----------------------------------------------------------------------
+
+function nthChild($element, $arguments, $traverse) {
+ switch ($arguments) {
+ case "n": return true;
+ case "even": $arguments = "2n"; break;
+ case "odd": $arguments = "2n+1";
+ }
+
+ var $$children = childElements($element.parentNode);
+ function _checkIndex($index) {
+ var $index = ($traverse == nextElementSibling) ? $$children.length - $index : $index - 1;
+ return $$children[$index] == $element;
+ };
+
+ // it was just a number (no "n")
+ if (!isNaN($arguments)) return _checkIndex($arguments);
+
+ $arguments = $arguments.split("n");
+ var $multiplier = parseInt($arguments[0]);
+ var $step = parseInt($arguments[1]);
+
+ if ((isNaN($multiplier) || $multiplier == 1) && $step == 0) return true;
+ if ($multiplier == 0 && !isNaN($step)) return _checkIndex($step);
+ if (isNaN($step)) $step = 0;
+
+ var $count = 1;
+ while ($element = $traverse($element)) $count++;
+
+ if (isNaN($multiplier) || $multiplier == 1)
+ return ($traverse == nextElementSibling) ? ($count <= $step) : ($step >= $count);
+
+ return ($count % $multiplier) == $step;
+};
+
+}); // addModule
diff --git a/tests/test_tools/selenium/core/lib/cssQuery/src/cssQuery-standard.js b/tests/test_tools/selenium/core/lib/cssQuery/src/cssQuery-standard.js new file mode 100644 index 00000000..77314b86 --- /dev/null +++ b/tests/test_tools/selenium/core/lib/cssQuery/src/cssQuery-standard.js @@ -0,0 +1,53 @@ +/*
+ cssQuery, version 2.0.2 (2005-08-19)
+ Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+ License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+
+cssQuery.addModule("css-standard", function() { // override IE optimisation
+
+// cssQuery was originally written as the CSS engine for IE7. It is
+// optimised (in terms of size not speed) for IE so this module is
+// provided separately to provide cross-browser support.
+
+// -----------------------------------------------------------------------
+// browser compatibility
+// -----------------------------------------------------------------------
+
+// sniff for Win32 Explorer
+isMSIE = eval("false;/*@cc_on@if(@\x5fwin32)isMSIE=true@end@*/");
+
+if (!isMSIE) {
+ getElementsByTagName = function($element, $tagName, $namespace) {
+ return $namespace ? $element.getElementsByTagNameNS("*", $tagName) :
+ $element.getElementsByTagName($tagName);
+ };
+
+ compareNamespace = function($element, $namespace) {
+ return !$namespace || ($namespace == "*") || ($element.prefix == $namespace);
+ };
+
+ isXML = document.contentType ? function($element) {
+ return /xml/i.test(getDocument($element).contentType);
+ } : function($element) {
+ return getDocument($element).documentElement.tagName != "HTML";
+ };
+
+ getTextContent = function($element) {
+ // mozilla || opera || other
+ return $element.textContent || $element.innerText || _getTextContent($element);
+ };
+
+ function _getTextContent($element) {
+ var $textContent = "", $node, i;
+ for (i = 0; ($node = $element.childNodes[i]); i++) {
+ switch ($node.nodeType) {
+ case 11: // document fragment
+ case 1: $textContent += _getTextContent($node); break;
+ case 3: $textContent += $node.nodeValue; break;
+ }
+ }
+ return $textContent;
+ };
+}
+}); // addModule
diff --git a/tests/test_tools/selenium/core/lib/cssQuery/src/cssQuery.js b/tests/test_tools/selenium/core/lib/cssQuery/src/cssQuery.js new file mode 100644 index 00000000..1fcab4a1 --- /dev/null +++ b/tests/test_tools/selenium/core/lib/cssQuery/src/cssQuery.js @@ -0,0 +1,356 @@ +/*
+ cssQuery, version 2.0.2 (2005-08-19)
+ Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+ License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+
+// the following functions allow querying of the DOM using CSS selectors
+var cssQuery = function() {
+var version = "2.0.2";
+
+// -----------------------------------------------------------------------
+// main query function
+// -----------------------------------------------------------------------
+
+var $COMMA = /\s*,\s*/;
+var cssQuery = function($selector, $$from) {
+try {
+ var $match = [];
+ var $useCache = arguments.callee.caching && !$$from;
+ var $base = ($$from) ? ($$from.constructor == Array) ? $$from : [$$from] : [document];
+ // process comma separated selectors
+ var $$selectors = parseSelector($selector).split($COMMA), i;
+ for (i = 0; i < $$selectors.length; i++) {
+ // convert the selector to a stream
+ $selector = _toStream($$selectors[i]);
+ // faster chop if it starts with id (MSIE only)
+ if (isMSIE && $selector.slice(0, 3).join("") == " *#") {
+ $selector = $selector.slice(2);
+ $$from = _msie_selectById([], $base, $selector[1]);
+ } else $$from = $base;
+ // process the stream
+ var j = 0, $token, $filter, $arguments, $cacheSelector = "";
+ while (j < $selector.length) {
+ $token = $selector[j++];
+ $filter = $selector[j++];
+ $cacheSelector += $token + $filter;
+ // some pseudo-classes allow arguments to be passed
+ // e.g. nth-child(even)
+ $arguments = "";
+ if ($selector[j] == "(") {
+ while ($selector[j++] != ")" && j < $selector.length) {
+ $arguments += $selector[j];
+ }
+ $arguments = $arguments.slice(0, -1);
+ $cacheSelector += "(" + $arguments + ")";
+ }
+ // process a token/filter pair use cached results if possible
+ $$from = ($useCache && cache[$cacheSelector]) ?
+ cache[$cacheSelector] : select($$from, $token, $filter, $arguments);
+ if ($useCache) cache[$cacheSelector] = $$from;
+ }
+ $match = $match.concat($$from);
+ }
+ delete cssQuery.error;
+ return $match;
+} catch ($error) {
+ cssQuery.error = $error;
+ return [];
+}};
+
+// -----------------------------------------------------------------------
+// public interface
+// -----------------------------------------------------------------------
+
+cssQuery.toString = function() {
+ return "function cssQuery() {\n [version " + version + "]\n}";
+};
+
+// caching
+var cache = {};
+cssQuery.caching = false;
+cssQuery.clearCache = function($selector) {
+ if ($selector) {
+ $selector = _toStream($selector).join("");
+ delete cache[$selector];
+ } else cache = {};
+};
+
+// allow extensions
+var modules = {};
+var loaded = false;
+cssQuery.addModule = function($name, $script) {
+ if (loaded) eval("$script=" + String($script));
+ modules[$name] = new $script();;
+};
+
+// hackery
+cssQuery.valueOf = function($code) {
+ return $code ? eval($code) : this;
+};
+
+// -----------------------------------------------------------------------
+// declarations
+// -----------------------------------------------------------------------
+
+var selectors = {};
+var pseudoClasses = {};
+// a safari bug means that these have to be declared here
+var AttributeSelector = {match: /\[([\w-]+(\|[\w-]+)?)\s*(\W?=)?\s*([^\]]*)\]/};
+var attributeSelectors = [];
+
+// -----------------------------------------------------------------------
+// selectors
+// -----------------------------------------------------------------------
+
+// descendant selector
+selectors[" "] = function($results, $from, $tagName, $namespace) {
+ // loop through current selection
+ var $element, i, j;
+ for (i = 0; i < $from.length; i++) {
+ // get descendants
+ var $subset = getElementsByTagName($from[i], $tagName, $namespace);
+ // loop through descendants and add to results selection
+ for (j = 0; ($element = $subset[j]); j++) {
+ if (thisElement($element) && compareNamespace($element, $namespace))
+ $results.push($element);
+ }
+ }
+};
+
+// ID selector
+selectors["#"] = function($results, $from, $id) {
+ // loop through current selection and check ID
+ var $element, j;
+ for (j = 0; ($element = $from[j]); j++) if ($element.id == $id) $results.push($element);
+};
+
+// class selector
+selectors["."] = function($results, $from, $className) {
+ // create a RegExp version of the class
+ $className = new RegExp("(^|\\s)" + $className + "(\\s|$)");
+ // loop through current selection and check class
+ var $element, i;
+ for (i = 0; ($element = $from[i]); i++)
+ if ($className.test($element.className)) $results.push($element);
+};
+
+// pseudo-class selector
+selectors[":"] = function($results, $from, $pseudoClass, $arguments) {
+ // retrieve the cssQuery pseudo-class function
+ var $test = pseudoClasses[$pseudoClass], $element, i;
+ // loop through current selection and apply pseudo-class filter
+ if ($test) for (i = 0; ($element = $from[i]); i++)
+ // if the cssQuery pseudo-class function returns "true" add the element
+ if ($test($element, $arguments)) $results.push($element);
+};
+
+// -----------------------------------------------------------------------
+// pseudo-classes
+// -----------------------------------------------------------------------
+
+pseudoClasses["link"] = function($element) {
+ var $document = getDocument($element);
+ if ($document.links) for (var i = 0; i < $document.links.length; i++) {
+ if ($document.links[i] == $element) return true;
+ }
+};
+
+pseudoClasses["visited"] = function($element) {
+ // can't do this without jiggery-pokery
+};
+
+// -----------------------------------------------------------------------
+// DOM traversal
+// -----------------------------------------------------------------------
+
+// IE5/6 includes comments (LOL) in it's elements collections.
+// so we have to check for this. the test is tagName != "!". LOL (again).
+var thisElement = function($element) {
+ return ($element && $element.nodeType == 1 && $element.tagName != "!") ? $element : null;
+};
+
+// return the previous element to the supplied element
+// previousSibling is not good enough as it might return a text or comment node
+var previousElementSibling = function($element) {
+ while ($element && ($element = $element.previousSibling) && !thisElement($element)) continue;
+ return $element;
+};
+
+// return the next element to the supplied element
+var nextElementSibling = function($element) {
+ while ($element && ($element = $element.nextSibling) && !thisElement($element)) continue;
+ return $element;
+};
+
+// return the first child ELEMENT of an element
+// NOT the first child node (though they may be the same thing)
+var firstElementChild = function($element) {
+ return thisElement($element.firstChild) || nextElementSibling($element.firstChild);
+};
+
+var lastElementChild = function($element) {
+ return thisElement($element.lastChild) || previousElementSibling($element.lastChild);
+};
+
+// return child elements of an element (not child nodes)
+var childElements = function($element) {
+ var $childElements = [];
+ $element = firstElementChild($element);
+ while ($element) {
+ $childElements.push($element);
+ $element = nextElementSibling($element);
+ }
+ return $childElements;
+};
+
+// -----------------------------------------------------------------------
+// browser compatibility
+// -----------------------------------------------------------------------
+
+// all of the functions in this section can be overwritten. the default
+// configuration is for IE. The functions below reflect this. standard
+// methods are included in a separate module. It would probably be better
+// the other way round of course but this makes it easier to keep IE7 trim.
+
+var isMSIE = true;
+
+var isXML = function($element) {
+ var $document = getDocument($element);
+ return (typeof $document.mimeType == "unknown") ?
+ /\.xml$/i.test($document.URL) :
+ Boolean($document.mimeType == "XML Document");
+};
+
+// return the element's containing document
+var getDocument = function($element) {
+ return $element.ownerDocument || $element.document;
+};
+
+var getElementsByTagName = function($element, $tagName) {
+ return ($tagName == "*" && $element.all) ? $element.all : $element.getElementsByTagName($tagName);
+};
+
+var compareTagName = function($element, $tagName, $namespace) {
+ if ($tagName == "*") return thisElement($element);
+ if (!compareNamespace($element, $namespace)) return false;
+ if (!isXML($element)) $tagName = $tagName.toUpperCase();
+ return $element.tagName == $tagName;
+};
+
+var compareNamespace = function($element, $namespace) {
+ return !$namespace || ($namespace == "*") || ($element.scopeName == $namespace);
+};
+
+var getTextContent = function($element) {
+ return $element.innerText;
+};
+
+function _msie_selectById($results, $from, id) {
+ var $match, i, j;
+ for (i = 0; i < $from.length; i++) {
+ if ($match = $from[i].all.item(id)) {
+ if ($match.id == id) $results.push($match);
+ else if ($match.length != null) {
+ for (j = 0; j < $match.length; j++) {
+ if ($match[j].id == id) $results.push($match[j]);
+ }
+ }
+ }
+ }
+ return $results;
+};
+
+// for IE5.0
+if (![].push) Array.prototype.push = function() {
+ for (var i = 0; i < arguments.length; i++) {
+ this[this.length] = arguments[i];
+ }
+ return this.length;
+};
+
+// -----------------------------------------------------------------------
+// query support
+// -----------------------------------------------------------------------
+
+// select a set of matching elements.
+// "from" is an array of elements.
+// "token" is a character representing the type of filter
+// e.g. ">" means child selector
+// "filter" represents the tag name, id or class name that is being selected
+// the function returns an array of matching elements
+var $NAMESPACE = /\|/;
+function select($$from, $token, $filter, $arguments) {
+ if ($NAMESPACE.test($filter)) {
+ $filter = $filter.split($NAMESPACE);
+ $arguments = $filter[0];
+ $filter = $filter[1];
+ }
+ var $results = [];
+ if (selectors[$token]) {
+ selectors[$token]($results, $$from, $filter, $arguments);
+ }
+ return $results;
+};
+
+// -----------------------------------------------------------------------
+// parsing
+// -----------------------------------------------------------------------
+
+// convert css selectors to a stream of tokens and filters
+// it's not a real stream. it's just an array of strings.
+var $STANDARD_SELECT = /^[^\s>+~]/;
+var $$STREAM = /[\s#.:>+~()@]|[^\s#.:>+~()@]+/g;
+function _toStream($selector) {
+ if ($STANDARD_SELECT.test($selector)) $selector = " " + $selector;
+ return $selector.match($$STREAM) || [];
+};
+
+var $WHITESPACE = /\s*([\s>+~(),]|^|$)\s*/g;
+var $IMPLIED_ALL = /([\s>+~,]|[^(]\+|^)([#.:@])/g;
+var parseSelector = function($selector) {
+ return $selector
+ // trim whitespace
+ .replace($WHITESPACE, "$1")
+ // e.g. ".class1" --> "*.class1"
+ .replace($IMPLIED_ALL, "$1*$2");
+};
+
+var Quote = {
+ toString: function() {return "'"},
+ match: /^('[^']*')|("[^"]*")$/,
+ test: function($string) {
+ return this.match.test($string);
+ },
+ add: function($string) {
+ return this.test($string) ? $string : this + $string + this;
+ },
+ remove: function($string) {
+ return this.test($string) ? $string.slice(1, -1) : $string;
+ }
+};
+
+var getText = function($text) {
+ return Quote.remove($text);
+};
+
+var $ESCAPE = /([\/()[\]?{}|*+-])/g;
+function regEscape($string) {
+ return $string.replace($ESCAPE, "\\$1");
+};
+
+// -----------------------------------------------------------------------
+// modules
+// -----------------------------------------------------------------------
+
+// -------- >> insert modules here for packaging << -------- \\
+
+loaded = true;
+
+// -----------------------------------------------------------------------
+// return the query function
+// -----------------------------------------------------------------------
+
+return cssQuery;
+
+}(); // cssQuery
diff --git a/tests/test_tools/selenium/core/lib/prototype.js b/tests/test_tools/selenium/core/lib/prototype.js new file mode 100644 index 00000000..0caf9cd7 --- /dev/null +++ b/tests/test_tools/selenium/core/lib/prototype.js @@ -0,0 +1,2006 @@ +/* Prototype JavaScript framework, version 1.5.0_rc0 + * (c) 2005 Sam Stephenson <sam@conio.net> + * + * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://prototype.conio.net/ + * +/*--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.5.0_rc0', + ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)', + + emptyFunction: function() {}, + K: function(x) {return x} +} + +var Class = { + create: function() { + return function() { + this.initialize.apply(this, arguments); + } + } +} + +var Abstract = new Object(); + +Object.extend = function(destination, source) { + for (var property in source) { + destination[property] = source[property]; + } + return destination; +} + +Object.inspect = function(object) { + try { + if (object == undefined) return 'undefined'; + if (object == null) return 'null'; + return object.inspect ? object.inspect() : object.toString(); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } +} + +Function.prototype.bind = function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } +} + +Function.prototype.bindAsEventListener = function(object) { + var __method = this; + return function(event) { + return __method.call(object, event || window.event); + } +} + +Object.extend(Number.prototype, { + toColorPart: function() { + var digits = this.toString(16); + if (this < 16) return '0' + digits; + return digits; + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator) { + $R(0, this, true).each(iterator); + return this; + } +}); + +var Try = { + these: function() { + var returnValue; + + for (var i = 0; i < arguments.length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) {} + } + + return returnValue; + } +} + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create(); +PeriodicalExecuter.prototype = { + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.callback(); + } finally { + this.currentlyExecuting = false; + } + } + } +} +Object.extend(String.prototype, { + gsub: function(pattern, replacement) { + var result = '', source = this, match; + replacement = arguments.callee.prepareReplacement(replacement); + + while (source.length > 0) { + if (match = source.match(pattern)) { + result += source.slice(0, match.index); + result += (replacement(match) || '').toString(); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + }, + + sub: function(pattern, replacement, count) { + replacement = this.gsub.prepareReplacement(replacement); + count = count === undefined ? 1 : count; + + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + }, + + scan: function(pattern, iterator) { + this.gsub(pattern, iterator); + return this; + }, + + truncate: function(length, truncation) { + length = length || 30; + truncation = truncation === undefined ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : this; + }, + + strip: function() { + return this.replace(/^\s+/, '').replace(/\s+$/, ''); + }, + + stripTags: function() { + return this.replace(/<\/?[^>]+>/gi, ''); + }, + + stripScripts: function() { + return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); + }, + + extractScripts: function() { + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); + var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + return (this.match(matchAll) || []).map(function(scriptTag) { + return (scriptTag.match(matchOne) || ['', ''])[1]; + }); + }, + + evalScripts: function() { + return this.extractScripts().map(function(script) { return eval(script) }); + }, + + escapeHTML: function() { + var div = document.createElement('div'); + var text = document.createTextNode(this); + div.appendChild(text); + return div.innerHTML; + }, + + unescapeHTML: function() { + var div = document.createElement('div'); + div.innerHTML = this.stripTags(); + return div.childNodes[0] ? div.childNodes[0].nodeValue : ''; + }, + + toQueryParams: function() { + var pairs = this.match(/^\??(.*)$/)[1].split('&'); + return pairs.inject({}, function(params, pairString) { + var pair = pairString.split('='); + params[pair[0]] = pair[1]; + return params; + }); + }, + + toArray: function() { + return this.split(''); + }, + + camelize: function() { + var oStringList = this.split('-'); + if (oStringList.length == 1) return oStringList[0]; + + var camelizedString = this.indexOf('-') == 0 + ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1) + : oStringList[0]; + + for (var i = 1, len = oStringList.length; i < len; i++) { + var s = oStringList[i]; + camelizedString += s.charAt(0).toUpperCase() + s.substring(1); + } + + return camelizedString; + }, + + inspect: function() { + return "'" + this.replace(/\\/g, '\\\\').replace(/'/g, '\\\'') + "'"; + } +}); + +String.prototype.gsub.prepareReplacement = function(replacement) { + if (typeof replacement == 'function') return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; +} + +String.prototype.parseQuery = String.prototype.toQueryParams; + +var Template = Class.create(); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; +Template.prototype = { + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + return this.template.gsub(this.pattern, function(match) { + var before = match[1]; + if (before == '\\') return match[2]; + return before + (object[match[3]] || '').toString(); + }); + } +} + +var $break = new Object(); +var $continue = new Object(); + +var Enumerable = { + each: function(iterator) { + var index = 0; + try { + this._each(function(value) { + try { + iterator(value, index++); + } catch (e) { + if (e != $continue) throw e; + } + }); + } catch (e) { + if (e != $break) throw e; + } + }, + + all: function(iterator) { + var result = true; + this.each(function(value, index) { + result = result && !!(iterator || Prototype.K)(value, index); + if (!result) throw $break; + }); + return result; + }, + + any: function(iterator) { + var result = true; + this.each(function(value, index) { + if (result = !!(iterator || Prototype.K)(value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator) { + var results = []; + this.each(function(value, index) { + results.push(iterator(value, index)); + }); + return results; + }, + + detect: function (iterator) { + var result; + this.each(function(value, index) { + if (iterator(value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator) { + var results = []; + this.each(function(value, index) { + if (iterator(value, index)) + results.push(value); + }); + return results; + }, + + grep: function(pattern, iterator) { + var results = []; + this.each(function(value, index) { + var stringValue = value.toString(); + if (stringValue.match(pattern)) + results.push((iterator || Prototype.K)(value, index)); + }) + return results; + }, + + include: function(object) { + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inject: function(memo, iterator) { + this.each(function(value, index) { + memo = iterator(memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.collect(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (result == undefined || value >= result) + result = value; + }); + return result; + }, + + min: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (result == undefined || value < result) + result = value; + }); + return result; + }, + + partition: function(iterator) { + var trues = [], falses = []; + this.each(function(value, index) { + ((iterator || Prototype.K)(value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value, index) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator) { + var results = []; + this.each(function(value, index) { + if (!iterator(value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator) { + return this.collect(function(value, index) { + return {value: value, criteria: iterator(value, index)}; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + toArray: function() { + return this.collect(Prototype.K); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (typeof args.last() == 'function') + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + return iterator(collections.pluck(index)); + }); + }, + + inspect: function() { + return '#<Enumerable:' + this.toArray().inspect() + '>'; + } +} + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray +}); +var $A = Array.from = function(iterable) { + if (!iterable) return []; + if (iterable.toArray) { + return iterable.toArray(); + } else { + var results = []; + for (var i = 0; i < iterable.length; i++) + results.push(iterable[i]); + return results; + } +} + +Object.extend(Array.prototype, Enumerable); + +if (!Array.prototype._reverse) + Array.prototype._reverse = Array.prototype.reverse; + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0; i < this.length; i++) + iterator(this[i]); + }, + + clear: function() { + this.length = 0; + return this; + }, + + first: function() { + return this[0]; + }, + + last: function() { + return this[this.length - 1]; + }, + + compact: function() { + return this.select(function(value) { + return value != undefined || value != null; + }); + }, + + flatten: function() { + return this.inject([], function(array, value) { + return array.concat(value && value.constructor == Array ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + indexOf: function(object) { + for (var i = 0; i < this.length; i++) + if (this[i] == object) return i; + return -1; + }, + + reverse: function(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + } +}); +var Hash = { + _each: function(iterator) { + for (var key in this) { + var value = this[key]; + if (typeof value == 'function') continue; + + var pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + merge: function(hash) { + return $H(hash).inject($H(this), function(mergedHash, pair) { + mergedHash[pair.key] = pair.value; + return mergedHash; + }); + }, + + toQueryString: function() { + return this.map(function(pair) { + return pair.map(encodeURIComponent).join('='); + }).join('&'); + }, + + inspect: function() { + return '#<Hash:{' + this.map(function(pair) { + return pair.map(Object.inspect).join(': '); + }).join(', ') + '}>'; + } +} + +function $H(object) { + var hash = Object.extend({}, object || {}); + Object.extend(hash, Enumerable); + Object.extend(hash, Hash); + return hash; +} +ObjectRange = Class.create(); +Object.extend(ObjectRange.prototype, Enumerable); +Object.extend(ObjectRange.prototype, { + initialize: function(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + }, + + _each: function(iterator) { + var value = this.start; + do { + iterator(value); + value = value.succ(); + } while (this.include(value)); + }, + + include: function(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } +}); + +var $R = function(start, end, exclusive) { + return new ObjectRange(start, end, exclusive); +} + +var Ajax = { + getTransport: function() { + return Try.these( + function() {return new XMLHttpRequest()}, + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')} + ) || false; + }, + + activeRequestCount: 0 +} + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responderToAdd) { + if (!this.include(responderToAdd)) + this.responders.push(responderToAdd); + }, + + unregister: function(responderToRemove) { + this.responders = this.responders.without(responderToRemove); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (responder[callback] && typeof responder[callback] == 'function') { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) {} + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { + Ajax.activeRequestCount++; + }, + + onComplete: function() { + Ajax.activeRequestCount--; + } +}); + +Ajax.Base = function() {}; +Ajax.Base.prototype = { + setOptions: function(options) { + this.options = { + method: 'post', + asynchronous: true, + contentType: 'application/x-www-form-urlencoded', + parameters: '' + } + Object.extend(this.options, options || {}); + }, + + responseIsSuccess: function() { + return this.transport.status == undefined + || this.transport.status == 0 + || (this.transport.status >= 200 && this.transport.status < 300); + }, + + responseIsFailure: function() { + return !this.responseIsSuccess(); + } +} + +Ajax.Request = Class.create(); +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Request.prototype = Object.extend(new Ajax.Base(), { + initialize: function(url, options) { + this.transport = Ajax.getTransport(); + this.setOptions(options); + this.request(url); + }, + + request: function(url) { + var parameters = this.options.parameters || ''; + if (parameters.length > 0) parameters += '&_='; + + try { + this.url = url; + if (this.options.method == 'get' && parameters.length > 0) + this.url += (this.url.match(/\?/) ? '&' : '?') + parameters; + + Ajax.Responders.dispatch('onCreate', this, this.transport); + + this.transport.open(this.options.method, this.url, + this.options.asynchronous); + + if (this.options.asynchronous) { + this.transport.onreadystatechange = this.onStateChange.bind(this); + setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10); + } + + this.setRequestHeaders(); + + var body = this.options.postBody ? this.options.postBody : parameters; + this.transport.send(this.options.method == 'post' ? body : null); + + } catch (e) { + this.dispatchException(e); + } + }, + + setRequestHeaders: function() { + var requestHeaders = + ['X-Requested-With', 'XMLHttpRequest', + 'X-Prototype-Version', Prototype.Version, + 'Accept', 'text/javascript, text/html, application/xml, text/xml, */*']; + + if (this.options.method == 'post') { + requestHeaders.push('Content-type', this.options.contentType); + + /* Force "Connection: close" for Mozilla browsers to work around + * a bug where XMLHttpReqeuest sends an incorrect Content-length + * header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType) + requestHeaders.push('Connection', 'close'); + } + + if (this.options.requestHeaders) + requestHeaders.push.apply(requestHeaders, this.options.requestHeaders); + + for (var i = 0; i < requestHeaders.length; i += 2) + this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]); + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState != 1) + this.respondToReadyState(this.transport.readyState); + }, + + header: function(name) { + try { + return this.transport.getResponseHeader(name); + } catch (e) {} + }, + + evalJSON: function() { + try { + return eval('(' + this.header('X-JSON') + ')'); + } catch (e) {} + }, + + evalResponse: function() { + try { + return eval(this.transport.responseText); + } catch (e) { + this.dispatchException(e); + } + }, + + respondToReadyState: function(readyState) { + var event = Ajax.Request.Events[readyState]; + var transport = this.transport, json = this.evalJSON(); + + if (event == 'Complete') { + try { + (this.options['on' + this.transport.status] + || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(transport, json); + } catch (e) { + this.dispatchException(e); + } + + if ((this.header('Content-type') || '').match(/^text\/javascript/i)) + this.evalResponse(); + } + + try { + (this.options['on' + event] || Prototype.emptyFunction)(transport, json); + Ajax.Responders.dispatch('on' + event, this, transport, json); + } catch (e) { + this.dispatchException(e); + } + + /* Avoid memory leak in MSIE: clean up the oncomplete event handler */ + if (event == 'Complete') + this.transport.onreadystatechange = Prototype.emptyFunction; + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Updater = Class.create(); + +Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { + initialize: function(container, url, options) { + this.containers = { + success: container.success ? $(container.success) : $(container), + failure: container.failure ? $(container.failure) : + (container.success ? null : $(container)) + } + + this.transport = Ajax.getTransport(); + this.setOptions(options); + + var onComplete = this.options.onComplete || Prototype.emptyFunction; + this.options.onComplete = (function(transport, object) { + this.updateContent(); + onComplete(transport, object); + }).bind(this); + + this.request(url); + }, + + updateContent: function() { + var receiver = this.responseIsSuccess() ? + this.containers.success : this.containers.failure; + var response = this.transport.responseText; + + if (!this.options.evalScripts) + response = response.stripScripts(); + + if (receiver) { + if (this.options.insertion) { + new this.options.insertion(receiver, response); + } else { + Element.update(receiver, response); + } + } + + if (this.responseIsSuccess()) { + if (this.onComplete) + setTimeout(this.onComplete.bind(this), 10); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(); +Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { + initialize: function(container, url, options) { + this.setOptions(options); + this.onComplete = this.options.onComplete; + + this.frequency = (this.options.frequency || 2); + this.decay = (this.options.decay || 1); + + this.updater = {}; + this.container = container; + this.url = url; + + this.start(); + }, + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); + }, + + stop: function() { + this.updater.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(request) { + if (this.options.decay) { + this.decay = (request.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = request.responseText; + } + this.timer = setTimeout(this.onTimerEvent.bind(this), + this.decay * this.frequency * 1000); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); +function $() { + var results = [], element; + for (var i = 0; i < arguments.length; i++) { + element = arguments[i]; + if (typeof element == 'string') + element = document.getElementById(element); + results.push(Element.extend(element)); + } + return results.length < 2 ? results[0] : results; +} + +document.getElementsByClassName = function(className, parentElement) { + var children = ($(parentElement) || document.body).getElementsByTagName('*'); + return $A(children).inject([], function(elements, child) { + if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)"))) + elements.push(Element.extend(child)); + return elements; + }); +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Element) + var Element = new Object(); + +Element.extend = function(element) { + if (!element) return; + if (_nativeExtensions) return element; + + if (!element._extended && element.tagName && element != window) { + var methods = Element.Methods, cache = Element.extend.cache; + for (property in methods) { + var value = methods[property]; + if (typeof value == 'function') + element[property] = cache.findOrStore(value); + } + } + + element._extended = true; + return element; +} + +Element.extend.cache = { + findOrStore: function(value) { + return this[value] = this[value] || function() { + return value.apply(null, [this].concat($A(arguments))); + } + } +} + +Element.Methods = { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + Element[Element.visible(element) ? 'hide' : 'show'](element); + } + }, + + hide: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = 'none'; + } + }, + + show: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = ''; + } + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + }, + + update: function(element, html) { + $(element).innerHTML = html.stripScripts(); + setTimeout(function() {html.evalScripts()}, 10); + }, + + replace: function(element, html) { + element = $(element); + if (element.outerHTML) { + element.outerHTML = html.stripScripts(); + } else { + var range = element.ownerDocument.createRange(); + range.selectNodeContents(element); + element.parentNode.replaceChild( + range.createContextualFragment(html.stripScripts()), element); + } + setTimeout(function() {html.evalScripts()}, 10); + }, + + getHeight: function(element) { + element = $(element); + return element.offsetHeight; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).include(className); + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).add(className); + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).remove(className); + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + element = $(element); + for (var i = 0; i < element.childNodes.length; i++) { + var node = element.childNodes[i]; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + Element.remove(node); + } + }, + + empty: function(element) { + return $(element).innerHTML.match(/^\s*$/); + }, + + childOf: function(element, ancestor) { + element = $(element), ancestor = $(ancestor); + while (element = element.parentNode) + if (element == ancestor) return true; + return false; + }, + + scrollTo: function(element) { + element = $(element); + var x = element.x ? element.x : element.offsetLeft, + y = element.y ? element.y : element.offsetTop; + window.scrollTo(x, y); + }, + + getStyle: function(element, style) { + element = $(element); + var value = element.style[style.camelize()]; + if (!value) { + if (document.defaultView && document.defaultView.getComputedStyle) { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css.getPropertyValue(style) : null; + } else if (element.currentStyle) { + value = element.currentStyle[style.camelize()]; + } + } + + if (window.opera && ['left', 'top', 'right', 'bottom'].include(style)) + if (Element.getStyle(element, 'position') == 'static') value = 'auto'; + + return value == 'auto' ? null : value; + }, + + setStyle: function(element, style) { + element = $(element); + for (var name in style) + element.style[name.camelize()] = style[name]; + }, + + getDimensions: function(element) { + element = $(element); + if (Element.getStyle(element, 'display') != 'none') + return {width: element.offsetWidth, height: element.offsetHeight}; + + // All *Width and *Height properties give 0 on elements with display none, + // so enable the element temporarily + var els = element.style; + var originalVisibility = els.visibility; + var originalPosition = els.position; + els.visibility = 'hidden'; + els.position = 'absolute'; + els.display = ''; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = 'none'; + els.position = originalPosition; + els.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + }, + + makePositioned: function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element._madePositioned = true; + element.style.position = 'relative'; + // Opera returns the offset relative to the positioning context, when an + // element is position relative but top and left have not been defined + if (window.opera) { + element.style.top = 0; + element.style.left = 0; + } + } + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return; + element._overflow = element.style.overflow; + if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden') + element.style.overflow = 'hidden'; + }, + + undoClipping: function(element) { + element = $(element); + if (element._overflow) return; + element.style.overflow = element._overflow; + element._overflow = undefined; + } +} + +Object.extend(Element, Element.Methods); + +var _nativeExtensions = false; + +if(!HTMLElement && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) { + var HTMLElement = {} + HTMLElement.prototype = document.createElement('div').__proto__; +} + +Element.addMethods = function(methods) { + Object.extend(Element.Methods, methods || {}); + + if(typeof HTMLElement != 'undefined') { + var methods = Element.Methods, cache = Element.extend.cache; + for (property in methods) { + var value = methods[property]; + if (typeof value == 'function') + HTMLElement.prototype[property] = cache.findOrStore(value); + } + _nativeExtensions = true; + } +} + +Element.addMethods(); + +var Toggle = new Object(); +Toggle.display = Element.toggle; + +/*--------------------------------------------------------------------------*/ + +Abstract.Insertion = function(adjacency) { + this.adjacency = adjacency; +} + +Abstract.Insertion.prototype = { + initialize: function(element, content) { + this.element = $(element); + this.content = content.stripScripts(); + + if (this.adjacency && this.element.insertAdjacentHTML) { + try { + this.element.insertAdjacentHTML(this.adjacency, this.content); + } catch (e) { + var tagName = this.element.tagName.toLowerCase(); + if (tagName == 'tbody' || tagName == 'tr') { + this.insertContent(this.contentFromAnonymousTable()); + } else { + throw e; + } + } + } else { + this.range = this.element.ownerDocument.createRange(); + if (this.initializeRange) this.initializeRange(); + this.insertContent([this.range.createContextualFragment(this.content)]); + } + + setTimeout(function() {content.evalScripts()}, 10); + }, + + contentFromAnonymousTable: function() { + var div = document.createElement('div'); + div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>'; + return $A(div.childNodes[0].childNodes[0].childNodes); + } +} + +var Insertion = new Object(); + +Insertion.Before = Class.create(); +Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), { + initializeRange: function() { + this.range.setStartBefore(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, this.element); + }).bind(this)); + } +}); + +Insertion.Top = Class.create(); +Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(true); + }, + + insertContent: function(fragments) { + fragments.reverse(false).each((function(fragment) { + this.element.insertBefore(fragment, this.element.firstChild); + }).bind(this)); + } +}); + +Insertion.Bottom = Class.create(); +Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.appendChild(fragment); + }).bind(this)); + } +}); + +Insertion.After = Class.create(); +Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), { + initializeRange: function() { + this.range.setStartAfter(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, + this.element.nextSibling); + }).bind(this)); + } +}); + +/*--------------------------------------------------------------------------*/ + +Element.ClassNames = Class.create(); +Element.ClassNames.prototype = { + initialize: function(element) { + this.element = $(element); + }, + + _each: function(iterator) { + this.element.className.split(/\s+/).select(function(name) { + return name.length > 0; + })._each(iterator); + }, + + set: function(className) { + this.element.className = className; + }, + + add: function(classNameToAdd) { + if (this.include(classNameToAdd)) return; + this.set(this.toArray().concat(classNameToAdd).join(' ')); + }, + + remove: function(classNameToRemove) { + if (!this.include(classNameToRemove)) return; + this.set(this.select(function(className) { + return className != classNameToRemove; + }).join(' ')); + }, + + toString: function() { + return this.toArray().join(' '); + } +} + +Object.extend(Element.ClassNames.prototype, Enumerable); +var Selector = Class.create(); +Selector.prototype = { + initialize: function(expression) { + this.params = {classNames: []}; + this.expression = expression.toString().strip(); + this.parseExpression(); + this.compileMatcher(); + }, + + parseExpression: function() { + function abort(message) { throw 'Parse error in selector: ' + message; } + + if (this.expression == '') abort('empty expression'); + + var params = this.params, expr = this.expression, match, modifier, clause, rest; + while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) { + params.attributes = params.attributes || []; + params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''}); + expr = match[1]; + } + + if (expr == '*') return this.params.wildcard = true; + + while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) { + modifier = match[1], clause = match[2], rest = match[3]; + switch (modifier) { + case '#': params.id = clause; break; + case '.': params.classNames.push(clause); break; + case '': + case undefined: params.tagName = clause.toUpperCase(); break; + default: abort(expr.inspect()); + } + expr = rest; + } + + if (expr.length > 0) abort(expr.inspect()); + }, + + buildMatchExpression: function() { + var params = this.params, conditions = [], clause; + + if (params.wildcard) + conditions.push('true'); + if (clause = params.id) + conditions.push('element.id == ' + clause.inspect()); + if (clause = params.tagName) + conditions.push('element.tagName.toUpperCase() == ' + clause.inspect()); + if ((clause = params.classNames).length > 0) + for (var i = 0; i < clause.length; i++) + conditions.push('Element.hasClassName(element, ' + clause[i].inspect() + ')'); + if (clause = params.attributes) { + clause.each(function(attribute) { + var value = 'element.getAttribute(' + attribute.name.inspect() + ')'; + var splitValueBy = function(delimiter) { + return value + ' && ' + value + '.split(' + delimiter.inspect() + ')'; + } + + switch (attribute.operator) { + case '=': conditions.push(value + ' == ' + attribute.value.inspect()); break; + case '~=': conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break; + case '|=': conditions.push( + splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect() + ); break; + case '!=': conditions.push(value + ' != ' + attribute.value.inspect()); break; + case '': + case undefined: conditions.push(value + ' != null'); break; + default: throw 'Unknown operator ' + attribute.operator + ' in selector'; + } + }); + } + + return conditions.join(' && '); + }, + + compileMatcher: function() { + this.match = new Function('element', 'if (!element.tagName) return false; \ + return ' + this.buildMatchExpression()); + }, + + findElements: function(scope) { + var element; + + if (element = $(this.params.id)) + if (this.match(element)) + if (!scope || Element.childOf(element, scope)) + return [element]; + + scope = (scope || document).getElementsByTagName(this.params.tagName || '*'); + + var results = []; + for (var i = 0; i < scope.length; i++) + if (this.match(element = scope[i])) + results.push(Element.extend(element)); + + return results; + }, + + toString: function() { + return this.expression; + } +} + +function $$() { + return $A(arguments).map(function(expression) { + return expression.strip().split(/\s+/).inject([null], function(results, expr) { + var selector = new Selector(expr); + return results.map(selector.findElements.bind(selector)).flatten(); + }); + }).flatten(); +} +var Field = { + clear: function() { + for (var i = 0; i < arguments.length; i++) + $(arguments[i]).value = ''; + }, + + focus: function(element) { + $(element).focus(); + }, + + present: function() { + for (var i = 0; i < arguments.length; i++) + if ($(arguments[i]).value == '') return false; + return true; + }, + + select: function(element) { + $(element).select(); + }, + + activate: function(element) { + element = $(element); + element.focus(); + if (element.select) + element.select(); + } +} + +/*--------------------------------------------------------------------------*/ + +var Form = { + serialize: function(form) { + var elements = Form.getElements($(form)); + var queryComponents = new Array(); + + for (var i = 0; i < elements.length; i++) { + var queryComponent = Form.Element.serialize(elements[i]); + if (queryComponent) + queryComponents.push(queryComponent); + } + + return queryComponents.join('&'); + }, + + getElements: function(form) { + form = $(form); + var elements = new Array(); + + for (var tagName in Form.Element.Serializers) { + var tagElements = form.getElementsByTagName(tagName); + for (var j = 0; j < tagElements.length; j++) + elements.push(tagElements[j]); + } + return elements; + }, + + getInputs: function(form, typeName, name) { + form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) + return inputs; + + var matchingInputs = new Array(); + for (var i = 0; i < inputs.length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || + (name && input.name != name)) + continue; + matchingInputs.push(input); + } + + return matchingInputs; + }, + + disable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.blur(); + element.disabled = 'true'; + } + }, + + enable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.disabled = ''; + } + }, + + findFirstElement: function(form) { + return Form.getElements(form).find(function(element) { + return element.type != 'hidden' && !element.disabled && + ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + }); + }, + + focusFirstElement: function(form) { + Field.activate(Form.findFirstElement(form)); + }, + + reset: function(form) { + $(form).reset(); + } +} + +Form.Element = { + serialize: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) { + var key = encodeURIComponent(parameter[0]); + if (key.length == 0) return; + + if (parameter[1].constructor != Array) + parameter[1] = [parameter[1]]; + + return parameter[1].map(function(value) { + return key + '=' + encodeURIComponent(value); + }).join('&'); + } + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) + return parameter[1]; + } +} + +Form.Element.Serializers = { + input: function(element) { + switch (element.type.toLowerCase()) { + case 'submit': + case 'hidden': + case 'password': + case 'text': + return Form.Element.Serializers.textarea(element); + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element); + } + return false; + }, + + inputSelector: function(element) { + if (element.checked) + return [element.name, element.value]; + }, + + textarea: function(element) { + return [element.name, element.value]; + }, + + select: function(element) { + return Form.Element.Serializers[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + }, + + selectOne: function(element) { + var value = '', opt, index = element.selectedIndex; + if (index >= 0) { + opt = element.options[index]; + value = opt.value || opt.text; + } + return [element.name, value]; + }, + + selectMany: function(element) { + var value = []; + for (var i = 0; i < element.length; i++) { + var opt = element.options[i]; + if (opt.selected) + value.push(opt.value || opt.text); + } + return [element.name, value]; + } +} + +/*--------------------------------------------------------------------------*/ + +var $F = Form.Element.getValue; + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = function() {} +Abstract.TimedObserver.prototype = { + initialize: function(element, frequency, callback) { + this.frequency = frequency; + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + } +} + +Form.Element.Observer = Class.create(); +Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(); +Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = function() {} +Abstract.EventObserver.prototype = { + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + var elements = Form.getElements(this.element); + for (var i = 0; i < elements.length; i++) + this.registerCallback(elements[i]); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + case 'password': + case 'text': + case 'textarea': + case 'select-one': + case 'select-multiple': + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +} + +Form.Element.EventObserver = Class.create(); +Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(); +Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) { + var Event = new Object(); +} + +Object.extend(Event, { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + + element: function(event) { + return event.target || event.srcElement; + }, + + isLeftClick: function(event) { + return (((event.which) && (event.which == 1)) || + ((event.button) && (event.button == 1))); + }, + + pointerX: function(event) { + return event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)); + }, + + pointerY: function(event) { + return event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)); + }, + + stop: function(event) { + if (event.preventDefault) { + event.preventDefault(); + event.stopPropagation(); + } else { + event.returnValue = false; + event.cancelBubble = true; + } + }, + + // find the first node with the given tagName, starting from the + // node the event was triggered on; traverses the DOM upwards + findElement: function(event, tagName) { + var element = Event.element(event); + while (element.parentNode && (!element.tagName || + (element.tagName.toUpperCase() != tagName.toUpperCase()))) + element = element.parentNode; + return element; + }, + + observers: false, + + _observeAndCache: function(element, name, observer, useCapture) { + if (!this.observers) this.observers = []; + if (element.addEventListener) { + this.observers.push([element, name, observer, useCapture]); + element.addEventListener(name, observer, useCapture); + } else if (element.attachEvent) { + this.observers.push([element, name, observer, useCapture]); + element.attachEvent('on' + name, observer); + } + }, + + unloadCache: function() { + if (!Event.observers) return; + for (var i = 0; i < Event.observers.length; i++) { + Event.stopObserving.apply(this, Event.observers[i]); + Event.observers[i][0] = null; + } + Event.observers = false; + }, + + observe: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.attachEvent)) + name = 'keydown'; + + this._observeAndCache(element, name, observer, useCapture); + }, + + stopObserving: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.detachEvent)) + name = 'keydown'; + + if (element.removeEventListener) { + element.removeEventListener(name, observer, useCapture); + } else if (element.detachEvent) { + element.detachEvent('on' + name, observer); + } + } +}); + +/* prevent memory leaks in IE */ +if (navigator.appVersion.match(/\bMSIE\b/)) + Event.observe(window, 'unload', Event.unloadCache, false); +var Position = { + // set to true if needed, warning: firefox performance problems + // NOT neeeded for page scrolling, only if draggable contained in + // scrollable elements + includeScrollOffsets: false, + + // must be called before calling withinIncludingScrolloffset, every time the + // page is scrolled + prepare: function() { + this.deltaX = window.pageXOffset + || document.documentElement.scrollLeft + || document.body.scrollLeft + || 0; + this.deltaY = window.pageYOffset + || document.documentElement.scrollTop + || document.body.scrollTop + || 0; + }, + + realOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return [valueL, valueT]; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return [valueL, valueT]; + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + p = Element.getStyle(element, 'position'); + if (p == 'relative' || p == 'absolute') break; + } + } while (element); + return [valueL, valueT]; + }, + + offsetParent: function(element) { + if (element.offsetParent) return element.offsetParent; + if (element == document.body) return element; + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return element; + + return document.body; + }, + + // caches x/y coordinate pair to use with overlap + within: function(element, x, y) { + if (this.includeScrollOffsets) + return this.withinIncludingScrolloffsets(element, x, y); + this.xcomp = x; + this.ycomp = y; + this.offset = this.cumulativeOffset(element); + + return (y >= this.offset[1] && + y < this.offset[1] + element.offsetHeight && + x >= this.offset[0] && + x < this.offset[0] + element.offsetWidth); + }, + + withinIncludingScrolloffsets: function(element, x, y) { + var offsetcache = this.realOffset(element); + + this.xcomp = x + offsetcache[0] - this.deltaX; + this.ycomp = y + offsetcache[1] - this.deltaY; + this.offset = this.cumulativeOffset(element); + + return (this.ycomp >= this.offset[1] && + this.ycomp < this.offset[1] + element.offsetHeight && + this.xcomp >= this.offset[0] && + this.xcomp < this.offset[0] + element.offsetWidth); + }, + + // within must be called directly before + overlap: function(mode, element) { + if (!mode) return 0; + if (mode == 'vertical') + return ((this.offset[1] + element.offsetHeight) - this.ycomp) / + element.offsetHeight; + if (mode == 'horizontal') + return ((this.offset[0] + element.offsetWidth) - this.xcomp) / + element.offsetWidth; + }, + + clone: function(source, target) { + source = $(source); + target = $(target); + target.style.position = 'absolute'; + var offsets = this.cumulativeOffset(source); + target.style.top = offsets[1] + 'px'; + target.style.left = offsets[0] + 'px'; + target.style.width = source.offsetWidth + 'px'; + target.style.height = source.offsetHeight + 'px'; + }, + + page: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent==document.body) + if (Element.getStyle(element,'position')=='absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } while (element = element.parentNode); + + return [valueL, valueT]; + }, + + clone: function(source, target) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || {}) + + // find page position of source + source = $(source); + var p = Position.page(source); + + // find coordinate system to use + target = $(target); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(target,'position') == 'absolute') { + parent = Position.offsetParent(target); + delta = Position.page(parent); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if(options.setLeft) target.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if(options.setWidth) target.style.width = source.offsetWidth + 'px'; + if(options.setHeight) target.style.height = source.offsetHeight + 'px'; + }, + + absolutize: function(element) { + element = $(element); + if (element.style.position == 'absolute') return; + Position.prepare(); + + var offsets = Position.positionedOffset(element); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px';; + element.style.left = left + 'px';; + element.style.width = width + 'px';; + element.style.height = height + 'px';; + }, + + relativize: function(element) { + element = $(element); + if (element.style.position == 'relative') return; + Position.prepare(); + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + } +} + +// Safari returns margins on body which is incorrect if the child is absolutely +// positioned. For performance reasons, redefine Position.cumulativeOffset for +// KHTML/WebKit only. +if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) { + Position.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return [valueL, valueT]; + } +}
\ No newline at end of file diff --git a/tests/test_tools/selenium/core/lib/scriptaculous/builder.js b/tests/test_tools/selenium/core/lib/scriptaculous/builder.js new file mode 100644 index 00000000..5b15ba93 --- /dev/null +++ b/tests/test_tools/selenium/core/lib/scriptaculous/builder.js @@ -0,0 +1,101 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// +// See scriptaculous.js for full license. + +var Builder = { + NODEMAP: { + AREA: 'map', + CAPTION: 'table', + COL: 'table', + COLGROUP: 'table', + LEGEND: 'fieldset', + OPTGROUP: 'select', + OPTION: 'select', + PARAM: 'object', + TBODY: 'table', + TD: 'table', + TFOOT: 'table', + TH: 'table', + THEAD: 'table', + TR: 'table' + }, + // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken, + // due to a Firefox bug + node: function(elementName) { + elementName = elementName.toUpperCase(); + + // try innerHTML approach + var parentTag = this.NODEMAP[elementName] || 'div'; + var parentElement = document.createElement(parentTag); + try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707 + parentElement.innerHTML = "<" + elementName + "></" + elementName + ">"; + } catch(e) {} + var element = parentElement.firstChild || null; + + // see if browser added wrapping tags + if(element && (element.tagName != elementName)) + element = element.getElementsByTagName(elementName)[0]; + + // fallback to createElement approach + if(!element) element = document.createElement(elementName); + + // abort if nothing could be created + if(!element) return; + + // attributes (or text) + if(arguments[1]) + if(this._isStringOrNumber(arguments[1]) || + (arguments[1] instanceof Array)) { + this._children(element, arguments[1]); + } else { + var attrs = this._attributes(arguments[1]); + if(attrs.length) { + try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707 + parentElement.innerHTML = "<" +elementName + " " + + attrs + "></" + elementName + ">"; + } catch(e) {} + element = parentElement.firstChild || null; + // workaround firefox 1.0.X bug + if(!element) { + element = document.createElement(elementName); + for(attr in arguments[1]) + element[attr == 'class' ? 'className' : attr] = arguments[1][attr]; + } + if(element.tagName != elementName) + element = parentElement.getElementsByTagName(elementName)[0]; + } + } + + // text, or array of children + if(arguments[2]) + this._children(element, arguments[2]); + + return element; + }, + _text: function(text) { + return document.createTextNode(text); + }, + _attributes: function(attributes) { + var attrs = []; + for(attribute in attributes) + attrs.push((attribute=='className' ? 'class' : attribute) + + '="' + attributes[attribute].toString().escapeHTML() + '"'); + return attrs.join(" "); + }, + _children: function(element, children) { + if(typeof children=='object') { // array can hold nodes and text + children.flatten().each( function(e) { + if(typeof e=='object') + element.appendChild(e) + else + if(Builder._isStringOrNumber(e)) + element.appendChild(Builder._text(e)); + }); + } else + if(Builder._isStringOrNumber(children)) + element.appendChild(Builder._text(children)); + }, + _isStringOrNumber: function(param) { + return(typeof param=='string' || typeof param=='number'); + } +}
\ No newline at end of file diff --git a/tests/test_tools/selenium/core/lib/scriptaculous/controls.js b/tests/test_tools/selenium/core/lib/scriptaculous/controls.js new file mode 100644 index 00000000..de0261ed --- /dev/null +++ b/tests/test_tools/selenium/core/lib/scriptaculous/controls.js @@ -0,0 +1,815 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005 Jon Tirsen (http://www.tirsen.com) +// Contributors: +// Richard Livsey +// Rahul Bhargava +// Rob Wills +// +// See scriptaculous.js for full license. + +// Autocompleter.Base handles all the autocompletion functionality +// that's independent of the data source for autocompletion. This +// includes drawing the autocompletion menu, observing keyboard +// and mouse events, and similar. +// +// Specific autocompleters need to provide, at the very least, +// a getUpdatedChoices function that will be invoked every time +// the text inside the monitored textbox changes. This method +// should get the text for which to provide autocompletion by +// invoking this.getToken(), NOT by directly accessing +// this.element.value. This is to allow incremental tokenized +// autocompletion. Specific auto-completion logic (AJAX, etc) +// belongs in getUpdatedChoices. +// +// Tokenized incremental autocompletion is enabled automatically +// when an autocompleter is instantiated with the 'tokens' option +// in the options parameter, e.g.: +// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); +// will incrementally autocomplete with a comma as the token. +// Additionally, ',' in the above example can be replaced with +// a token array, e.g. { tokens: [',', '\n'] } which +// enables autocompletion on multiple tokens. This is most +// useful when one of the tokens is \n (a newline), as it +// allows smart autocompletion after linebreaks. + +var Autocompleter = {} +Autocompleter.Base = function() {}; +Autocompleter.Base.prototype = { + baseInitialize: function(element, update, options) { + this.element = $(element); + this.update = $(update); + this.hasFocus = false; + this.changed = false; + this.active = false; + this.index = 0; + this.entryCount = 0; + + if (this.setOptions) + this.setOptions(options); + else + this.options = options || {}; + + this.options.paramName = this.options.paramName || this.element.name; + this.options.tokens = this.options.tokens || []; + this.options.frequency = this.options.frequency || 0.4; + this.options.minChars = this.options.minChars || 1; + this.options.onShow = this.options.onShow || + function(element, update){ + if(!update.style.position || update.style.position=='absolute') { + update.style.position = 'absolute'; + Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight}); + } + Effect.Appear(update,{duration:0.15}); + }; + this.options.onHide = this.options.onHide || + function(element, update){ new Effect.Fade(update,{duration:0.15}) }; + + if (typeof(this.options.tokens) == 'string') + this.options.tokens = new Array(this.options.tokens); + + this.observer = null; + + this.element.setAttribute('autocomplete','off'); + + Element.hide(this.update); + + Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this)); + Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this)); + }, + + show: function() { + if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); + if(!this.iefix && + (navigator.appVersion.indexOf('MSIE')>0) && + (navigator.userAgent.indexOf('Opera')<0) && + (Element.getStyle(this.update, 'position')=='absolute')) { + new Insertion.After(this.update, + '<iframe id="' + this.update.id + '_iefix" '+ + 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' + + 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>'); + this.iefix = $(this.update.id+'_iefix'); + } + if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); + }, + + fixIEOverlapping: function() { + Position.clone(this.update, this.iefix); + this.iefix.style.zIndex = 1; + this.update.style.zIndex = 2; + Element.show(this.iefix); + }, + + hide: function() { + this.stopIndicator(); + if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); + if(this.iefix) Element.hide(this.iefix); + }, + + startIndicator: function() { + if(this.options.indicator) Element.show(this.options.indicator); + }, + + stopIndicator: function() { + if(this.options.indicator) Element.hide(this.options.indicator); + }, + + onKeyPress: function(event) { + if(this.active) + switch(event.keyCode) { + case Event.KEY_TAB: + case Event.KEY_RETURN: + this.selectEntry(); + Event.stop(event); + case Event.KEY_ESC: + this.hide(); + this.active = false; + Event.stop(event); + return; + case Event.KEY_LEFT: + case Event.KEY_RIGHT: + return; + case Event.KEY_UP: + this.markPrevious(); + this.render(); + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + return; + case Event.KEY_DOWN: + this.markNext(); + this.render(); + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + return; + } + else + if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || + (navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return; + + this.changed = true; + this.hasFocus = true; + + if(this.observer) clearTimeout(this.observer); + this.observer = + setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); + }, + + activate: function() { + this.changed = false; + this.hasFocus = true; + this.getUpdatedChoices(); + }, + + onHover: function(event) { + var element = Event.findElement(event, 'LI'); + if(this.index != element.autocompleteIndex) + { + this.index = element.autocompleteIndex; + this.render(); + } + Event.stop(event); + }, + + onClick: function(event) { + var element = Event.findElement(event, 'LI'); + this.index = element.autocompleteIndex; + this.selectEntry(); + this.hide(); + }, + + onBlur: function(event) { + // needed to make click events working + setTimeout(this.hide.bind(this), 250); + this.hasFocus = false; + this.active = false; + }, + + render: function() { + if(this.entryCount > 0) { + for (var i = 0; i < this.entryCount; i++) + this.index==i ? + Element.addClassName(this.getEntry(i),"selected") : + Element.removeClassName(this.getEntry(i),"selected"); + + if(this.hasFocus) { + this.show(); + this.active = true; + } + } else { + this.active = false; + this.hide(); + } + }, + + markPrevious: function() { + if(this.index > 0) this.index-- + else this.index = this.entryCount-1; + }, + + markNext: function() { + if(this.index < this.entryCount-1) this.index++ + else this.index = 0; + }, + + getEntry: function(index) { + return this.update.firstChild.childNodes[index]; + }, + + getCurrentEntry: function() { + return this.getEntry(this.index); + }, + + selectEntry: function() { + this.active = false; + this.updateElement(this.getCurrentEntry()); + }, + + updateElement: function(selectedElement) { + if (this.options.updateElement) { + this.options.updateElement(selectedElement); + return; + } + var value = ''; + if (this.options.select) { + var nodes = document.getElementsByClassName(this.options.select, selectedElement) || []; + if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); + } else + value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); + + var lastTokenPos = this.findLastToken(); + if (lastTokenPos != -1) { + var newValue = this.element.value.substr(0, lastTokenPos + 1); + var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/); + if (whitespace) + newValue += whitespace[0]; + this.element.value = newValue + value; + } else { + this.element.value = value; + } + this.element.focus(); + + if (this.options.afterUpdateElement) + this.options.afterUpdateElement(this.element, selectedElement); + }, + + updateChoices: function(choices) { + if(!this.changed && this.hasFocus) { + this.update.innerHTML = choices; + Element.cleanWhitespace(this.update); + Element.cleanWhitespace(this.update.firstChild); + + if(this.update.firstChild && this.update.firstChild.childNodes) { + this.entryCount = + this.update.firstChild.childNodes.length; + for (var i = 0; i < this.entryCount; i++) { + var entry = this.getEntry(i); + entry.autocompleteIndex = i; + this.addObservers(entry); + } + } else { + this.entryCount = 0; + } + + this.stopIndicator(); + + this.index = 0; + this.render(); + } + }, + + addObservers: function(element) { + Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); + Event.observe(element, "click", this.onClick.bindAsEventListener(this)); + }, + + onObserverEvent: function() { + this.changed = false; + if(this.getToken().length>=this.options.minChars) { + this.startIndicator(); + this.getUpdatedChoices(); + } else { + this.active = false; + this.hide(); + } + }, + + getToken: function() { + var tokenPos = this.findLastToken(); + if (tokenPos != -1) + var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,''); + else + var ret = this.element.value; + + return /\n/.test(ret) ? '' : ret; + }, + + findLastToken: function() { + var lastTokenPos = -1; + + for (var i=0; i<this.options.tokens.length; i++) { + var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]); + if (thisTokenPos > lastTokenPos) + lastTokenPos = thisTokenPos; + } + return lastTokenPos; + } +} + +Ajax.Autocompleter = Class.create(); +Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), { + initialize: function(element, update, url, options) { + this.baseInitialize(element, update, options); + this.options.asynchronous = true; + this.options.onComplete = this.onComplete.bind(this); + this.options.defaultParams = this.options.parameters || null; + this.url = url; + }, + + getUpdatedChoices: function() { + entry = encodeURIComponent(this.options.paramName) + '=' + + encodeURIComponent(this.getToken()); + + this.options.parameters = this.options.callback ? + this.options.callback(this.element, entry) : entry; + + if(this.options.defaultParams) + this.options.parameters += '&' + this.options.defaultParams; + + new Ajax.Request(this.url, this.options); + }, + + onComplete: function(request) { + this.updateChoices(request.responseText); + } + +}); + +// The local array autocompleter. Used when you'd prefer to +// inject an array of autocompletion options into the page, rather +// than sending out Ajax queries, which can be quite slow sometimes. +// +// The constructor takes four parameters. The first two are, as usual, +// the id of the monitored textbox, and id of the autocompletion menu. +// The third is the array you want to autocomplete from, and the fourth +// is the options block. +// +// Extra local autocompletion options: +// - choices - How many autocompletion choices to offer +// +// - partialSearch - If false, the autocompleter will match entered +// text only at the beginning of strings in the +// autocomplete array. Defaults to true, which will +// match text at the beginning of any *word* in the +// strings in the autocomplete array. If you want to +// search anywhere in the string, additionally set +// the option fullSearch to true (default: off). +// +// - fullSsearch - Search anywhere in autocomplete array strings. +// +// - partialChars - How many characters to enter before triggering +// a partial match (unlike minChars, which defines +// how many characters are required to do any match +// at all). Defaults to 2. +// +// - ignoreCase - Whether to ignore case when autocompleting. +// Defaults to true. +// +// It's possible to pass in a custom function as the 'selector' +// option, if you prefer to write your own autocompletion logic. +// In that case, the other options above will not apply unless +// you support them. + +Autocompleter.Local = Class.create(); +Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { + initialize: function(element, update, array, options) { + this.baseInitialize(element, update, options); + this.options.array = array; + }, + + getUpdatedChoices: function() { + this.updateChoices(this.options.selector(this)); + }, + + setOptions: function(options) { + this.options = Object.extend({ + choices: 10, + partialSearch: true, + partialChars: 2, + ignoreCase: true, + fullSearch: false, + selector: function(instance) { + var ret = []; // Beginning matches + var partial = []; // Inside matches + var entry = instance.getToken(); + var count = 0; + + for (var i = 0; i < instance.options.array.length && + ret.length < instance.options.choices ; i++) { + + var elem = instance.options.array[i]; + var foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase()) : + elem.indexOf(entry); + + while (foundPos != -1) { + if (foundPos == 0 && elem.length != entry.length) { + ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + + elem.substr(entry.length) + "</li>"); + break; + } else if (entry.length >= instance.options.partialChars && + instance.options.partialSearch && foundPos != -1) { + if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { + partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" + + elem.substr(foundPos, entry.length) + "</strong>" + elem.substr( + foundPos + entry.length) + "</li>"); + break; + } + } + + foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : + elem.indexOf(entry, foundPos + 1); + + } + } + if (partial.length) + ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)) + return "<ul>" + ret.join('') + "</ul>"; + } + }, options || {}); + } +}); + +// AJAX in-place editor +// +// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor + +// Use this if you notice weird scrolling problems on some browsers, +// the DOM might be a bit confused when this gets called so do this +// waits 1 ms (with setTimeout) until it does the activation +Field.scrollFreeActivate = function(field) { + setTimeout(function() { + Field.activate(field); + }, 1); +} + +Ajax.InPlaceEditor = Class.create(); +Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99"; +Ajax.InPlaceEditor.prototype = { + initialize: function(element, url, options) { + this.url = url; + this.element = $(element); + + this.options = Object.extend({ + okButton: true, + okText: "ok", + cancelLink: true, + cancelText: "cancel", + savingText: "Saving...", + clickToEditText: "Click to edit", + okText: "ok", + rows: 1, + onComplete: function(transport, element) { + new Effect.Highlight(element, {startcolor: this.options.highlightcolor}); + }, + onFailure: function(transport) { + alert("Error communicating with the server: " + transport.responseText.stripTags()); + }, + callback: function(form) { + return Form.serialize(form); + }, + handleLineBreaks: true, + loadingText: 'Loading...', + savingClassName: 'inplaceeditor-saving', + loadingClassName: 'inplaceeditor-loading', + formClassName: 'inplaceeditor-form', + highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor, + highlightendcolor: "#FFFFFF", + externalControl: null, + submitOnBlur: false, + ajaxOptions: {}, + evalScripts: false + }, options || {}); + + if(!this.options.formId && this.element.id) { + this.options.formId = this.element.id + "-inplaceeditor"; + if ($(this.options.formId)) { + // there's already a form with that name, don't specify an id + this.options.formId = null; + } + } + + if (this.options.externalControl) { + this.options.externalControl = $(this.options.externalControl); + } + + this.originalBackground = Element.getStyle(this.element, 'background-color'); + if (!this.originalBackground) { + this.originalBackground = "transparent"; + } + + this.element.title = this.options.clickToEditText; + + this.onclickListener = this.enterEditMode.bindAsEventListener(this); + this.mouseoverListener = this.enterHover.bindAsEventListener(this); + this.mouseoutListener = this.leaveHover.bindAsEventListener(this); + Event.observe(this.element, 'click', this.onclickListener); + Event.observe(this.element, 'mouseover', this.mouseoverListener); + Event.observe(this.element, 'mouseout', this.mouseoutListener); + if (this.options.externalControl) { + Event.observe(this.options.externalControl, 'click', this.onclickListener); + Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener); + Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener); + } + }, + enterEditMode: function(evt) { + if (this.saving) return; + if (this.editing) return; + this.editing = true; + this.onEnterEditMode(); + if (this.options.externalControl) { + Element.hide(this.options.externalControl); + } + Element.hide(this.element); + this.createForm(); + this.element.parentNode.insertBefore(this.form, this.element); + Field.scrollFreeActivate(this.editField); + // stop the event to avoid a page refresh in Safari + if (evt) { + Event.stop(evt); + } + return false; + }, + createForm: function() { + this.form = document.createElement("form"); + this.form.id = this.options.formId; + Element.addClassName(this.form, this.options.formClassName) + this.form.onsubmit = this.onSubmit.bind(this); + + this.createEditField(); + + if (this.options.textarea) { + var br = document.createElement("br"); + this.form.appendChild(br); + } + + if (this.options.okButton) { + okButton = document.createElement("input"); + okButton.type = "submit"; + okButton.value = this.options.okText; + okButton.className = 'editor_ok_button'; + this.form.appendChild(okButton); + } + + if (this.options.cancelLink) { + cancelLink = document.createElement("a"); + cancelLink.href = "#"; + cancelLink.appendChild(document.createTextNode(this.options.cancelText)); + cancelLink.onclick = this.onclickCancel.bind(this); + cancelLink.className = 'editor_cancel'; + this.form.appendChild(cancelLink); + } + }, + hasHTMLLineBreaks: function(string) { + if (!this.options.handleLineBreaks) return false; + return string.match(/<br/i) || string.match(/<p>/i); + }, + convertHTMLLineBreaks: function(string) { + return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, ""); + }, + createEditField: function() { + var text; + if(this.options.loadTextURL) { + text = this.options.loadingText; + } else { + text = this.getText(); + } + + var obj = this; + + if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) { + this.options.textarea = false; + var textField = document.createElement("input"); + textField.obj = this; + textField.type = "text"; + textField.name = "value"; + textField.value = text; + textField.style.backgroundColor = this.options.highlightcolor; + textField.className = 'editor_field'; + var size = this.options.size || this.options.cols || 0; + if (size != 0) textField.size = size; + if (this.options.submitOnBlur) + textField.onblur = this.onSubmit.bind(this); + this.editField = textField; + } else { + this.options.textarea = true; + var textArea = document.createElement("textarea"); + textArea.obj = this; + textArea.name = "value"; + textArea.value = this.convertHTMLLineBreaks(text); + textArea.rows = this.options.rows; + textArea.cols = this.options.cols || 40; + textArea.className = 'editor_field'; + if (this.options.submitOnBlur) + textArea.onblur = this.onSubmit.bind(this); + this.editField = textArea; + } + + if(this.options.loadTextURL) { + this.loadExternalText(); + } + this.form.appendChild(this.editField); + }, + getText: function() { + return this.element.innerHTML; + }, + loadExternalText: function() { + Element.addClassName(this.form, this.options.loadingClassName); + this.editField.disabled = true; + new Ajax.Request( + this.options.loadTextURL, + Object.extend({ + asynchronous: true, + onComplete: this.onLoadedExternalText.bind(this) + }, this.options.ajaxOptions) + ); + }, + onLoadedExternalText: function(transport) { + Element.removeClassName(this.form, this.options.loadingClassName); + this.editField.disabled = false; + this.editField.value = transport.responseText.stripTags(); + }, + onclickCancel: function() { + this.onComplete(); + this.leaveEditMode(); + return false; + }, + onFailure: function(transport) { + this.options.onFailure(transport); + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + this.oldInnerHTML = null; + } + return false; + }, + onSubmit: function() { + // onLoading resets these so we need to save them away for the Ajax call + var form = this.form; + var value = this.editField.value; + + // do this first, sometimes the ajax call returns before we get a chance to switch on Saving... + // which means this will actually switch on Saving... *after* we've left edit mode causing Saving... + // to be displayed indefinitely + this.onLoading(); + + if (this.options.evalScripts) { + new Ajax.Request( + this.url, Object.extend({ + parameters: this.options.callback(form, value), + onComplete: this.onComplete.bind(this), + onFailure: this.onFailure.bind(this), + asynchronous:true, + evalScripts:true + }, this.options.ajaxOptions)); + } else { + new Ajax.Updater( + { success: this.element, + // don't update on failure (this could be an option) + failure: null }, + this.url, Object.extend({ + parameters: this.options.callback(form, value), + onComplete: this.onComplete.bind(this), + onFailure: this.onFailure.bind(this) + }, this.options.ajaxOptions)); + } + // stop the event to avoid a page refresh in Safari + if (arguments.length > 1) { + Event.stop(arguments[0]); + } + return false; + }, + onLoading: function() { + this.saving = true; + this.removeForm(); + this.leaveHover(); + this.showSaving(); + }, + showSaving: function() { + this.oldInnerHTML = this.element.innerHTML; + this.element.innerHTML = this.options.savingText; + Element.addClassName(this.element, this.options.savingClassName); + this.element.style.backgroundColor = this.originalBackground; + Element.show(this.element); + }, + removeForm: function() { + if(this.form) { + if (this.form.parentNode) Element.remove(this.form); + this.form = null; + } + }, + enterHover: function() { + if (this.saving) return; + this.element.style.backgroundColor = this.options.highlightcolor; + if (this.effect) { + this.effect.cancel(); + } + Element.addClassName(this.element, this.options.hoverClassName) + }, + leaveHover: function() { + if (this.options.backgroundColor) { + this.element.style.backgroundColor = this.oldBackground; + } + Element.removeClassName(this.element, this.options.hoverClassName) + if (this.saving) return; + this.effect = new Effect.Highlight(this.element, { + startcolor: this.options.highlightcolor, + endcolor: this.options.highlightendcolor, + restorecolor: this.originalBackground + }); + }, + leaveEditMode: function() { + Element.removeClassName(this.element, this.options.savingClassName); + this.removeForm(); + this.leaveHover(); + this.element.style.backgroundColor = this.originalBackground; + Element.show(this.element); + if (this.options.externalControl) { + Element.show(this.options.externalControl); + } + this.editing = false; + this.saving = false; + this.oldInnerHTML = null; + this.onLeaveEditMode(); + }, + onComplete: function(transport) { + this.leaveEditMode(); + this.options.onComplete.bind(this)(transport, this.element); + }, + onEnterEditMode: function() {}, + onLeaveEditMode: function() {}, + dispose: function() { + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + } + this.leaveEditMode(); + Event.stopObserving(this.element, 'click', this.onclickListener); + Event.stopObserving(this.element, 'mouseover', this.mouseoverListener); + Event.stopObserving(this.element, 'mouseout', this.mouseoutListener); + if (this.options.externalControl) { + Event.stopObserving(this.options.externalControl, 'click', this.onclickListener); + Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener); + Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener); + } + } +}; + +Ajax.InPlaceCollectionEditor = Class.create(); +Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype); +Object.extend(Ajax.InPlaceCollectionEditor.prototype, { + createEditField: function() { + if (!this.cached_selectTag) { + var selectTag = document.createElement("select"); + var collection = this.options.collection || []; + var optionTag; + collection.each(function(e,i) { + optionTag = document.createElement("option"); + optionTag.value = (e instanceof Array) ? e[0] : e; + if(this.options.value==optionTag.value) optionTag.selected = true; + optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e)); + selectTag.appendChild(optionTag); + }.bind(this)); + this.cached_selectTag = selectTag; + } + + this.editField = this.cached_selectTag; + if(this.options.loadTextURL) this.loadExternalText(); + this.form.appendChild(this.editField); + this.options.callback = function(form, value) { + return "value=" + encodeURIComponent(value); + } + } +}); + +// Delayed observer, like Form.Element.Observer, +// but waits for delay after last key input +// Ideal for live-search fields + +Form.Element.DelayedObserver = Class.create(); +Form.Element.DelayedObserver.prototype = { + initialize: function(element, delay, callback) { + this.delay = delay || 0.5; + this.element = $(element); + this.callback = callback; + this.timer = null; + this.lastValue = $F(this.element); + Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); + }, + delayedListener: function(event) { + if(this.lastValue == $F(this.element)) return; + if(this.timer) clearTimeout(this.timer); + this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); + this.lastValue = $F(this.element); + }, + onTimerEvent: function() { + this.timer = null; + this.callback(this.element, $F(this.element)); + } +}; diff --git a/tests/test_tools/selenium/core/lib/scriptaculous/dragdrop.js b/tests/test_tools/selenium/core/lib/scriptaculous/dragdrop.js new file mode 100644 index 00000000..be2a30f5 --- /dev/null +++ b/tests/test_tools/selenium/core/lib/scriptaculous/dragdrop.js @@ -0,0 +1,915 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) +// +// See scriptaculous.js for full license. + +/*--------------------------------------------------------------------------*/ + +var Droppables = { + drops: [], + + remove: function(element) { + this.drops = this.drops.reject(function(d) { return d.element==$(element) }); + }, + + add: function(element) { + element = $(element); + var options = Object.extend({ + greedy: true, + hoverclass: null, + tree: false + }, arguments[1] || {}); + + // cache containers + if(options.containment) { + options._containers = []; + var containment = options.containment; + if((typeof containment == 'object') && + (containment.constructor == Array)) { + containment.each( function(c) { options._containers.push($(c)) }); + } else { + options._containers.push($(containment)); + } + } + + if(options.accept) options.accept = [options.accept].flatten(); + + Element.makePositioned(element); // fix IE + options.element = element; + + this.drops.push(options); + }, + + findDeepestChild: function(drops) { + deepest = drops[0]; + + for (i = 1; i < drops.length; ++i) + if (Element.isParent(drops[i].element, deepest.element)) + deepest = drops[i]; + + return deepest; + }, + + isContained: function(element, drop) { + var containmentNode; + if(drop.tree) { + containmentNode = element.treeNode; + } else { + containmentNode = element.parentNode; + } + return drop._containers.detect(function(c) { return containmentNode == c }); + }, + + isAffected: function(point, element, drop) { + return ( + (drop.element!=element) && + ((!drop._containers) || + this.isContained(element, drop)) && + ((!drop.accept) || + (Element.classNames(element).detect( + function(v) { return drop.accept.include(v) } ) )) && + Position.within(drop.element, point[0], point[1]) ); + }, + + deactivate: function(drop) { + if(drop.hoverclass) + Element.removeClassName(drop.element, drop.hoverclass); + this.last_active = null; + }, + + activate: function(drop) { + if(drop.hoverclass) + Element.addClassName(drop.element, drop.hoverclass); + this.last_active = drop; + }, + + show: function(point, element) { + if(!this.drops.length) return; + var affected = []; + + if(this.last_active) this.deactivate(this.last_active); + this.drops.each( function(drop) { + if(Droppables.isAffected(point, element, drop)) + affected.push(drop); + }); + + if(affected.length>0) { + drop = Droppables.findDeepestChild(affected); + Position.within(drop.element, point[0], point[1]); + if(drop.onHover) + drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); + + Droppables.activate(drop); + } + }, + + fire: function(event, element) { + if(!this.last_active) return; + Position.prepare(); + + if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) + if (this.last_active.onDrop) + this.last_active.onDrop(element, this.last_active.element, event); + }, + + reset: function() { + if(this.last_active) + this.deactivate(this.last_active); + } +} + +var Draggables = { + drags: [], + observers: [], + + register: function(draggable) { + if(this.drags.length == 0) { + this.eventMouseUp = this.endDrag.bindAsEventListener(this); + this.eventMouseMove = this.updateDrag.bindAsEventListener(this); + this.eventKeypress = this.keyPress.bindAsEventListener(this); + + Event.observe(document, "mouseup", this.eventMouseUp); + Event.observe(document, "mousemove", this.eventMouseMove); + Event.observe(document, "keypress", this.eventKeypress); + } + this.drags.push(draggable); + }, + + unregister: function(draggable) { + this.drags = this.drags.reject(function(d) { return d==draggable }); + if(this.drags.length == 0) { + Event.stopObserving(document, "mouseup", this.eventMouseUp); + Event.stopObserving(document, "mousemove", this.eventMouseMove); + Event.stopObserving(document, "keypress", this.eventKeypress); + } + }, + + activate: function(draggable) { + window.focus(); // allows keypress events if window isn't currently focused, fails for Safari + this.activeDraggable = draggable; + }, + + deactivate: function() { + this.activeDraggable = null; + }, + + updateDrag: function(event) { + if(!this.activeDraggable) return; + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + // Mozilla-based browsers fire successive mousemove events with + // the same coordinates, prevent needless redrawing (moz bug?) + if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; + this._lastPointer = pointer; + this.activeDraggable.updateDrag(event, pointer); + }, + + endDrag: function(event) { + if(!this.activeDraggable) return; + this._lastPointer = null; + this.activeDraggable.endDrag(event); + this.activeDraggable = null; + }, + + keyPress: function(event) { + if(this.activeDraggable) + this.activeDraggable.keyPress(event); + }, + + addObserver: function(observer) { + this.observers.push(observer); + this._cacheObserverCallbacks(); + }, + + removeObserver: function(element) { // element instead of observer fixes mem leaks + this.observers = this.observers.reject( function(o) { return o.element==element }); + this._cacheObserverCallbacks(); + }, + + notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' + if(this[eventName+'Count'] > 0) + this.observers.each( function(o) { + if(o[eventName]) o[eventName](eventName, draggable, event); + }); + }, + + _cacheObserverCallbacks: function() { + ['onStart','onEnd','onDrag'].each( function(eventName) { + Draggables[eventName+'Count'] = Draggables.observers.select( + function(o) { return o[eventName]; } + ).length; + }); + } +} + +/*--------------------------------------------------------------------------*/ + +var Draggable = Class.create(); +Draggable.prototype = { + initialize: function(element) { + var options = Object.extend({ + handle: false, + starteffect: function(element) { + element._opacity = Element.getOpacity(element); + new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); + }, + reverteffect: function(element, top_offset, left_offset) { + var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; + element._revert = new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur}); + }, + endeffect: function(element) { + var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0 + new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity}); + }, + zindex: 1000, + revert: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + snap: false // false, or xy or [x,y] or function(x,y){ return [x,y] } + }, arguments[1] || {}); + + this.element = $(element); + + if(options.handle && (typeof options.handle == 'string')) { + var h = Element.childrenWithClassName(this.element, options.handle, true); + if(h.length>0) this.handle = h[0]; + } + if(!this.handle) this.handle = $(options.handle); + if(!this.handle) this.handle = this.element; + + if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) + options.scroll = $(options.scroll); + + Element.makePositioned(this.element); // fix IE + + this.delta = this.currentDelta(); + this.options = options; + this.dragging = false; + + this.eventMouseDown = this.initDrag.bindAsEventListener(this); + Event.observe(this.handle, "mousedown", this.eventMouseDown); + + Draggables.register(this); + }, + + destroy: function() { + Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); + Draggables.unregister(this); + }, + + currentDelta: function() { + return([ + parseInt(Element.getStyle(this.element,'left') || '0'), + parseInt(Element.getStyle(this.element,'top') || '0')]); + }, + + initDrag: function(event) { + if(Event.isLeftClick(event)) { + // abort on form elements, fixes a Firefox issue + var src = Event.element(event); + if(src.tagName && ( + src.tagName=='INPUT' || + src.tagName=='SELECT' || + src.tagName=='OPTION' || + src.tagName=='BUTTON' || + src.tagName=='TEXTAREA')) return; + + if(this.element._revert) { + this.element._revert.cancel(); + this.element._revert = null; + } + + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var pos = Position.cumulativeOffset(this.element); + this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); + + Draggables.activate(this); + Event.stop(event); + } + }, + + startDrag: function(event) { + this.dragging = true; + + if(this.options.zindex) { + this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); + this.element.style.zIndex = this.options.zindex; + } + + if(this.options.ghosting) { + this._clone = this.element.cloneNode(true); + Position.absolutize(this.element); + this.element.parentNode.insertBefore(this._clone, this.element); + } + + if(this.options.scroll) { + if (this.options.scroll == window) { + var where = this._getWindowScroll(this.options.scroll); + this.originalScrollLeft = where.left; + this.originalScrollTop = where.top; + } else { + this.originalScrollLeft = this.options.scroll.scrollLeft; + this.originalScrollTop = this.options.scroll.scrollTop; + } + } + + Draggables.notify('onStart', this, event); + if(this.options.starteffect) this.options.starteffect(this.element); + }, + + updateDrag: function(event, pointer) { + if(!this.dragging) this.startDrag(event); + Position.prepare(); + Droppables.show(pointer, this.element); + Draggables.notify('onDrag', this, event); + this.draw(pointer); + if(this.options.change) this.options.change(this); + + if(this.options.scroll) { + this.stopScrolling(); + + var p; + if (this.options.scroll == window) { + with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } + } else { + p = Position.page(this.options.scroll); + p[0] += this.options.scroll.scrollLeft; + p[1] += this.options.scroll.scrollTop; + p.push(p[0]+this.options.scroll.offsetWidth); + p.push(p[1]+this.options.scroll.offsetHeight); + } + var speed = [0,0]; + if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); + if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); + if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); + if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); + this.startScrolling(speed); + } + + // fix AppleWebKit rendering + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + + Event.stop(event); + }, + + finishDrag: function(event, success) { + this.dragging = false; + + if(this.options.ghosting) { + Position.relativize(this.element); + Element.remove(this._clone); + this._clone = null; + } + + if(success) Droppables.fire(event, this.element); + Draggables.notify('onEnd', this, event); + + var revert = this.options.revert; + if(revert && typeof revert == 'function') revert = revert(this.element); + + var d = this.currentDelta(); + if(revert && this.options.reverteffect) { + this.options.reverteffect(this.element, + d[1]-this.delta[1], d[0]-this.delta[0]); + } else { + this.delta = d; + } + + if(this.options.zindex) + this.element.style.zIndex = this.originalZ; + + if(this.options.endeffect) + this.options.endeffect(this.element); + + Draggables.deactivate(this); + Droppables.reset(); + }, + + keyPress: function(event) { + if(event.keyCode!=Event.KEY_ESC) return; + this.finishDrag(event, false); + Event.stop(event); + }, + + endDrag: function(event) { + if(!this.dragging) return; + this.stopScrolling(); + this.finishDrag(event, true); + Event.stop(event); + }, + + draw: function(point) { + var pos = Position.cumulativeOffset(this.element); + var d = this.currentDelta(); + pos[0] -= d[0]; pos[1] -= d[1]; + + if(this.options.scroll && (this.options.scroll != window)) { + pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; + pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; + } + + var p = [0,1].map(function(i){ + return (point[i]-pos[i]-this.offset[i]) + }.bind(this)); + + if(this.options.snap) { + if(typeof this.options.snap == 'function') { + p = this.options.snap(p[0],p[1],this); + } else { + if(this.options.snap instanceof Array) { + p = p.map( function(v, i) { + return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this)) + } else { + p = p.map( function(v) { + return Math.round(v/this.options.snap)*this.options.snap }.bind(this)) + } + }} + + var style = this.element.style; + if((!this.options.constraint) || (this.options.constraint=='horizontal')) + style.left = p[0] + "px"; + if((!this.options.constraint) || (this.options.constraint=='vertical')) + style.top = p[1] + "px"; + if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering + }, + + stopScrolling: function() { + if(this.scrollInterval) { + clearInterval(this.scrollInterval); + this.scrollInterval = null; + Draggables._lastScrollPointer = null; + } + }, + + startScrolling: function(speed) { + this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; + this.lastScrolled = new Date(); + this.scrollInterval = setInterval(this.scroll.bind(this), 10); + }, + + scroll: function() { + var current = new Date(); + var delta = current - this.lastScrolled; + this.lastScrolled = current; + if(this.options.scroll == window) { + with (this._getWindowScroll(this.options.scroll)) { + if (this.scrollSpeed[0] || this.scrollSpeed[1]) { + var d = delta / 1000; + this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); + } + } + } else { + this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; + this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; + } + + Position.prepare(); + Droppables.show(Draggables._lastPointer, this.element); + Draggables.notify('onDrag', this); + Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); + Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; + Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; + if (Draggables._lastScrollPointer[0] < 0) + Draggables._lastScrollPointer[0] = 0; + if (Draggables._lastScrollPointer[1] < 0) + Draggables._lastScrollPointer[1] = 0; + this.draw(Draggables._lastScrollPointer); + + if(this.options.change) this.options.change(this); + }, + + _getWindowScroll: function(w) { + var T, L, W, H; + with (w.document) { + if (w.document.documentElement && documentElement.scrollTop) { + T = documentElement.scrollTop; + L = documentElement.scrollLeft; + } else if (w.document.body) { + T = body.scrollTop; + L = body.scrollLeft; + } + if (w.innerWidth) { + W = w.innerWidth; + H = w.innerHeight; + } else if (w.document.documentElement && documentElement.clientWidth) { + W = documentElement.clientWidth; + H = documentElement.clientHeight; + } else { + W = body.offsetWidth; + H = body.offsetHeight + } + } + return { top: T, left: L, width: W, height: H }; + } +} + +/*--------------------------------------------------------------------------*/ + +var SortableObserver = Class.create(); +SortableObserver.prototype = { + initialize: function(element, observer) { + this.element = $(element); + this.observer = observer; + this.lastValue = Sortable.serialize(this.element); + }, + + onStart: function() { + this.lastValue = Sortable.serialize(this.element); + }, + + onEnd: function() { + Sortable.unmark(); + if(this.lastValue != Sortable.serialize(this.element)) + this.observer(this.element) + } +} + +var Sortable = { + sortables: {}, + + _findRootElement: function(element) { + while (element.tagName != "BODY") { + if(element.id && Sortable.sortables[element.id]) return element; + element = element.parentNode; + } + }, + + options: function(element) { + element = Sortable._findRootElement($(element)); + if(!element) return; + return Sortable.sortables[element.id]; + }, + + destroy: function(element){ + var s = Sortable.options(element); + + if(s) { + Draggables.removeObserver(s.element); + s.droppables.each(function(d){ Droppables.remove(d) }); + s.draggables.invoke('destroy'); + + delete Sortable.sortables[s.element.id]; + } + }, + + create: function(element) { + element = $(element); + var options = Object.extend({ + element: element, + tag: 'li', // assumes li children, override with tag: 'tagname' + dropOnEmpty: false, + tree: false, + treeTag: 'ul', + overlap: 'vertical', // one of 'vertical', 'horizontal' + constraint: 'vertical', // one of 'vertical', 'horizontal', false + containment: element, // also takes array of elements (or id's); or false + handle: false, // or a CSS class + only: false, + hoverclass: null, + ghosting: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + format: /^[^_]*_(.*)$/, + onChange: Prototype.emptyFunction, + onUpdate: Prototype.emptyFunction + }, arguments[1] || {}); + + // clear any old sortable with same element + this.destroy(element); + + // build options for the draggables + var options_for_draggable = { + revert: true, + scroll: options.scroll, + scrollSpeed: options.scrollSpeed, + scrollSensitivity: options.scrollSensitivity, + ghosting: options.ghosting, + constraint: options.constraint, + handle: options.handle }; + + if(options.starteffect) + options_for_draggable.starteffect = options.starteffect; + + if(options.reverteffect) + options_for_draggable.reverteffect = options.reverteffect; + else + if(options.ghosting) options_for_draggable.reverteffect = function(element) { + element.style.top = 0; + element.style.left = 0; + }; + + if(options.endeffect) + options_for_draggable.endeffect = options.endeffect; + + if(options.zindex) + options_for_draggable.zindex = options.zindex; + + // build options for the droppables + var options_for_droppable = { + overlap: options.overlap, + containment: options.containment, + tree: options.tree, + hoverclass: options.hoverclass, + onHover: Sortable.onHover + //greedy: !options.dropOnEmpty + } + + var options_for_tree = { + onHover: Sortable.onEmptyHover, + overlap: options.overlap, + containment: options.containment, + hoverclass: options.hoverclass + } + + // fix for gecko engine + Element.cleanWhitespace(element); + + options.draggables = []; + options.droppables = []; + + // drop on empty handling + if(options.dropOnEmpty || options.tree) { + Droppables.add(element, options_for_tree); + options.droppables.push(element); + } + + (this.findElements(element, options) || []).each( function(e) { + // handles are per-draggable + var handle = options.handle ? + Element.childrenWithClassName(e, options.handle)[0] : e; + options.draggables.push( + new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); + Droppables.add(e, options_for_droppable); + if(options.tree) e.treeNode = element; + options.droppables.push(e); + }); + + if(options.tree) { + (Sortable.findTreeElements(element, options) || []).each( function(e) { + Droppables.add(e, options_for_tree); + e.treeNode = element; + options.droppables.push(e); + }); + } + + // keep reference + this.sortables[element.id] = options; + + // for onupdate + Draggables.addObserver(new SortableObserver(element, options.onUpdate)); + + }, + + // return all suitable-for-sortable elements in a guaranteed order + findElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.tag); + }, + + findTreeElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.treeTag); + }, + + onHover: function(element, dropon, overlap) { + if(Element.isParent(dropon, element)) return; + + if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { + return; + } else if(overlap>0.5) { + Sortable.mark(dropon, 'before'); + if(dropon.previousSibling != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, dropon); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } else { + Sortable.mark(dropon, 'after'); + var nextElement = dropon.nextSibling || null; + if(nextElement != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, nextElement); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } + }, + + onEmptyHover: function(element, dropon, overlap) { + var oldParentNode = element.parentNode; + var droponOptions = Sortable.options(dropon); + + if(!Element.isParent(dropon, element)) { + var index; + + var children = Sortable.findElements(dropon, {tag: droponOptions.tag}); + var child = null; + + if(children) { + var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); + + for (index = 0; index < children.length; index += 1) { + if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { + offset -= Element.offsetSize (children[index], droponOptions.overlap); + } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { + child = index + 1 < children.length ? children[index + 1] : null; + break; + } else { + child = children[index]; + break; + } + } + } + + dropon.insertBefore(element, child); + + Sortable.options(oldParentNode).onChange(element); + droponOptions.onChange(element); + } + }, + + unmark: function() { + if(Sortable._marker) Element.hide(Sortable._marker); + }, + + mark: function(dropon, position) { + // mark on ghosting only + var sortable = Sortable.options(dropon.parentNode); + if(sortable && !sortable.ghosting) return; + + if(!Sortable._marker) { + Sortable._marker = $('dropmarker') || document.createElement('DIV'); + Element.hide(Sortable._marker); + Element.addClassName(Sortable._marker, 'dropmarker'); + Sortable._marker.style.position = 'absolute'; + document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); + } + var offsets = Position.cumulativeOffset(dropon); + Sortable._marker.style.left = offsets[0] + 'px'; + Sortable._marker.style.top = offsets[1] + 'px'; + + if(position=='after') + if(sortable.overlap == 'horizontal') + Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px'; + else + Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px'; + + Element.show(Sortable._marker); + }, + + _tree: function(element, options, parent) { + var children = Sortable.findElements(element, options) || []; + + for (var i = 0; i < children.length; ++i) { + var match = children[i].id.match(options.format); + + if (!match) continue; + + var child = { + id: encodeURIComponent(match ? match[1] : null), + element: element, + parent: parent, + children: new Array, + position: parent.children.length, + container: Sortable._findChildrenElement(children[i], options.treeTag.toUpperCase()) + } + + /* Get the element containing the children and recurse over it */ + if (child.container) + this._tree(child.container, options, child) + + parent.children.push (child); + } + + return parent; + }, + + /* Finds the first element of the given tag type within a parent element. + Used for finding the first LI[ST] within a L[IST]I[TEM].*/ + _findChildrenElement: function (element, containerTag) { + if (element && element.hasChildNodes) + for (var i = 0; i < element.childNodes.length; ++i) + if (element.childNodes[i].tagName == containerTag) + return element.childNodes[i]; + + return null; + }, + + tree: function(element) { + element = $(element); + var sortableOptions = this.options(element); + var options = Object.extend({ + tag: sortableOptions.tag, + treeTag: sortableOptions.treeTag, + only: sortableOptions.only, + name: element.id, + format: sortableOptions.format + }, arguments[1] || {}); + + var root = { + id: null, + parent: null, + children: new Array, + container: element, + position: 0 + } + + return Sortable._tree (element, options, root); + }, + + /* Construct a [i] index for a particular node */ + _constructIndex: function(node) { + var index = ''; + do { + if (node.id) index = '[' + node.position + ']' + index; + } while ((node = node.parent) != null); + return index; + }, + + sequence: function(element) { + element = $(element); + var options = Object.extend(this.options(element), arguments[1] || {}); + + return $(this.findElements(element, options) || []).map( function(item) { + return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; + }); + }, + + setSequence: function(element, new_sequence) { + element = $(element); + var options = Object.extend(this.options(element), arguments[2] || {}); + + var nodeMap = {}; + this.findElements(element, options).each( function(n) { + if (n.id.match(options.format)) + nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; + n.parentNode.removeChild(n); + }); + + new_sequence.each(function(ident) { + var n = nodeMap[ident]; + if (n) { + n[1].appendChild(n[0]); + delete nodeMap[ident]; + } + }); + }, + + serialize: function(element) { + element = $(element); + var options = Object.extend(Sortable.options(element), arguments[1] || {}); + var name = encodeURIComponent( + (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); + + if (options.tree) { + return Sortable.tree(element, arguments[1]).children.map( function (item) { + return [name + Sortable._constructIndex(item) + "=" + + encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); + }).flatten().join('&'); + } else { + return Sortable.sequence(element, arguments[1]).map( function(item) { + return name + "[]=" + encodeURIComponent(item); + }).join('&'); + } + } +} + +/* Returns true if child is contained within element */ +Element.isParent = function(child, element) { + if (!child.parentNode || child == element) return false; + + if (child.parentNode == element) return true; + + return Element.isParent(child.parentNode, element); +} + +Element.findChildren = function(element, only, recursive, tagName) { + if(!element.hasChildNodes()) return null; + tagName = tagName.toUpperCase(); + if(only) only = [only].flatten(); + var elements = []; + $A(element.childNodes).each( function(e) { + if(e.tagName && e.tagName.toUpperCase()==tagName && + (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) + elements.push(e); + if(recursive) { + var grandchildren = Element.findChildren(e, only, recursive, tagName); + if(grandchildren) elements.push(grandchildren); + } + }); + + return (elements.length>0 ? elements.flatten() : []); +} + +Element.offsetSize = function (element, type) { + if (type == 'vertical' || type == 'height') + return element.offsetHeight; + else + return element.offsetWidth; +}
\ No newline at end of file diff --git a/tests/test_tools/selenium/core/lib/scriptaculous/effects.js b/tests/test_tools/selenium/core/lib/scriptaculous/effects.js new file mode 100644 index 00000000..0864323e --- /dev/null +++ b/tests/test_tools/selenium/core/lib/scriptaculous/effects.js @@ -0,0 +1,958 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Contributors: +// Justin Palmer (http://encytemedia.com/) +// Mark Pilgrim (http://diveintomark.org/) +// Martin Bialasinki +// +// See scriptaculous.js for full license. + +// converts rgb() and #xxx to #xxxxxx format, +// returns self (or first argument) if not convertable +String.prototype.parseColor = function() { + var color = '#'; + if(this.slice(0,4) == 'rgb(') { + var cols = this.slice(4,this.length-1).split(','); + var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); + } else { + if(this.slice(0,1) == '#') { + if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); + if(this.length==7) color = this.toLowerCase(); + } + } + return(color.length==7 ? color : (arguments[0] || this)); +} + +/*--------------------------------------------------------------------------*/ + +Element.collectTextNodes = function(element) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); + }).flatten().join(''); +} + +Element.collectTextNodesIgnoreClass = function(element, className) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? + Element.collectTextNodesIgnoreClass(node, className) : '')); + }).flatten().join(''); +} + +Element.setContentZoom = function(element, percent) { + element = $(element); + Element.setStyle(element, {fontSize: (percent/100) + 'em'}); + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); +} + +Element.getOpacity = function(element){ + var opacity; + if (opacity = Element.getStyle(element, 'opacity')) + return parseFloat(opacity); + if (opacity = (Element.getStyle(element, 'filter') || '').match(/alpha\(opacity=(.*)\)/)) + if(opacity[1]) return parseFloat(opacity[1]) / 100; + return 1.0; +} + +Element.setOpacity = function(element, value){ + element= $(element); + if (value == 1){ + Element.setStyle(element, { opacity: + (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? + 0.999999 : null }); + if(/MSIE/.test(navigator.userAgent)) + Element.setStyle(element, {filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')}); + } else { + if(value < 0.00001) value = 0; + Element.setStyle(element, {opacity: value}); + if(/MSIE/.test(navigator.userAgent)) + Element.setStyle(element, + { filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') + + 'alpha(opacity='+value*100+')' }); + } +} + +Element.getInlineOpacity = function(element){ + return $(element).style.opacity || ''; +} + +Element.childrenWithClassName = function(element, className, findFirst) { + var classNameRegExp = new RegExp("(^|\\s)" + className + "(\\s|$)"); + var results = $A($(element).getElementsByTagName('*'))[findFirst ? 'detect' : 'select']( function(c) { + return (c.className && c.className.match(classNameRegExp)); + }); + if(!results) results = []; + return results; +} + +Element.forceRerendering = function(element) { + try { + element = $(element); + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch(e) { } +}; + +/*--------------------------------------------------------------------------*/ + +Array.prototype.call = function() { + var args = arguments; + this.each(function(f){ f.apply(this, args) }); +} + +/*--------------------------------------------------------------------------*/ + +var Effect = { + tagifyText: function(element) { + var tagifyStyle = 'position:relative'; + if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ';zoom:1'; + element = $(element); + $A(element.childNodes).each( function(child) { + if(child.nodeType==3) { + child.nodeValue.toArray().each( function(character) { + element.insertBefore( + Builder.node('span',{style: tagifyStyle}, + character == ' ' ? String.fromCharCode(160) : character), + child); + }); + Element.remove(child); + } + }); + }, + multiple: function(element, effect) { + var elements; + if(((typeof element == 'object') || + (typeof element == 'function')) && + (element.length)) + elements = element; + else + elements = $(element).childNodes; + + var options = Object.extend({ + speed: 0.1, + delay: 0.0 + }, arguments[2] || {}); + var masterDelay = options.delay; + + $A(elements).each( function(element, index) { + new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); + }); + }, + PAIRS: { + 'slide': ['SlideDown','SlideUp'], + 'blind': ['BlindDown','BlindUp'], + 'appear': ['Appear','Fade'] + }, + toggle: function(element, effect) { + element = $(element); + effect = (effect || 'appear').toLowerCase(); + var options = Object.extend({ + queue: { position:'end', scope:(element.id || 'global'), limit: 1 } + }, arguments[2] || {}); + Effect[element.visible() ? + Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options); + } +}; + +var Effect2 = Effect; // deprecated + +/* ------------- transitions ------------- */ + +Effect.Transitions = {} + +Effect.Transitions.linear = function(pos) { + return pos; +} +Effect.Transitions.sinoidal = function(pos) { + return (-Math.cos(pos*Math.PI)/2) + 0.5; +} +Effect.Transitions.reverse = function(pos) { + return 1-pos; +} +Effect.Transitions.flicker = function(pos) { + return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; +} +Effect.Transitions.wobble = function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; +} +Effect.Transitions.pulse = function(pos) { + return (Math.floor(pos*10) % 2 == 0 ? + (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10))); +} +Effect.Transitions.none = function(pos) { + return 0; +} +Effect.Transitions.full = function(pos) { + return 1; +} + +/* ------------- core effects ------------- */ + +Effect.ScopedQueue = Class.create(); +Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), { + initialize: function() { + this.effects = []; + this.interval = null; + }, + _each: function(iterator) { + this.effects._each(iterator); + }, + add: function(effect) { + var timestamp = new Date().getTime(); + + var position = (typeof effect.options.queue == 'string') ? + effect.options.queue : effect.options.queue.position; + + switch(position) { + case 'front': + // move unstarted effects after this effect + this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { + e.startOn += effect.finishOn; + e.finishOn += effect.finishOn; + }); + break; + case 'end': + // start effect after last queued effect has finished + timestamp = this.effects.pluck('finishOn').max() || timestamp; + break; + } + + effect.startOn += timestamp; + effect.finishOn += timestamp; + + if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) + this.effects.push(effect); + + if(!this.interval) + this.interval = setInterval(this.loop.bind(this), 40); + }, + remove: function(effect) { + this.effects = this.effects.reject(function(e) { return e==effect }); + if(this.effects.length == 0) { + clearInterval(this.interval); + this.interval = null; + } + }, + loop: function() { + var timePos = new Date().getTime(); + this.effects.invoke('loop', timePos); + } +}); + +Effect.Queues = { + instances: $H(), + get: function(queueName) { + if(typeof queueName != 'string') return queueName; + + if(!this.instances[queueName]) + this.instances[queueName] = new Effect.ScopedQueue(); + + return this.instances[queueName]; + } +} +Effect.Queue = Effect.Queues.get('global'); + +Effect.DefaultOptions = { + transition: Effect.Transitions.sinoidal, + duration: 1.0, // seconds + fps: 25.0, // max. 25fps due to Effect.Queue implementation + sync: false, // true for combining + from: 0.0, + to: 1.0, + delay: 0.0, + queue: 'parallel' +} + +Effect.Base = function() {}; +Effect.Base.prototype = { + position: null, + start: function(options) { + this.options = Object.extend(Object.extend({},Effect.DefaultOptions), options || {}); + this.currentFrame = 0; + this.state = 'idle'; + this.startOn = this.options.delay*1000; + this.finishOn = this.startOn + (this.options.duration*1000); + this.event('beforeStart'); + if(!this.options.sync) + Effect.Queues.get(typeof this.options.queue == 'string' ? + 'global' : this.options.queue.scope).add(this); + }, + loop: function(timePos) { + if(timePos >= this.startOn) { + if(timePos >= this.finishOn) { + this.render(1.0); + this.cancel(); + this.event('beforeFinish'); + if(this.finish) this.finish(); + this.event('afterFinish'); + return; + } + var pos = (timePos - this.startOn) / (this.finishOn - this.startOn); + var frame = Math.round(pos * this.options.fps * this.options.duration); + if(frame > this.currentFrame) { + this.render(pos); + this.currentFrame = frame; + } + } + }, + render: function(pos) { + if(this.state == 'idle') { + this.state = 'running'; + this.event('beforeSetup'); + if(this.setup) this.setup(); + this.event('afterSetup'); + } + if(this.state == 'running') { + if(this.options.transition) pos = this.options.transition(pos); + pos *= (this.options.to-this.options.from); + pos += this.options.from; + this.position = pos; + this.event('beforeUpdate'); + if(this.update) this.update(pos); + this.event('afterUpdate'); + } + }, + cancel: function() { + if(!this.options.sync) + Effect.Queues.get(typeof this.options.queue == 'string' ? + 'global' : this.options.queue.scope).remove(this); + this.state = 'finished'; + }, + event: function(eventName) { + if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); + if(this.options[eventName]) this.options[eventName](this); + }, + inspect: function() { + return '#<Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '>'; + } +} + +Effect.Parallel = Class.create(); +Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), { + initialize: function(effects) { + this.effects = effects || []; + this.start(arguments[1]); + }, + update: function(position) { + this.effects.invoke('render', position); + }, + finish: function(position) { + this.effects.each( function(effect) { + effect.render(1.0); + effect.cancel(); + effect.event('beforeFinish'); + if(effect.finish) effect.finish(position); + effect.event('afterFinish'); + }); + } +}); + +Effect.Opacity = Class.create(); +Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + // make this work on IE on elements without 'layout' + if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout)) + this.element.setStyle({zoom: 1}); + var options = Object.extend({ + from: this.element.getOpacity() || 0.0, + to: 1.0 + }, arguments[1] || {}); + this.start(options); + }, + update: function(position) { + this.element.setOpacity(position); + } +}); + +Effect.Move = Class.create(); +Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + var options = Object.extend({ + x: 0, + y: 0, + mode: 'relative' + }, arguments[1] || {}); + this.start(options); + }, + setup: function() { + // Bug in Opera: Opera returns the "real" position of a static element or + // relative element that does not have top/left explicitly set. + // ==> Always set top and left for position relative elements in your stylesheets + // (to 0 if you do not need them) + this.element.makePositioned(); + this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); + this.originalTop = parseFloat(this.element.getStyle('top') || '0'); + if(this.options.mode == 'absolute') { + // absolute movement, so we need to calc deltaX and deltaY + this.options.x = this.options.x - this.originalLeft; + this.options.y = this.options.y - this.originalTop; + } + }, + update: function(position) { + this.element.setStyle({ + left: this.options.x * position + this.originalLeft + 'px', + top: this.options.y * position + this.originalTop + 'px' + }); + } +}); + +// for backwards compatibility +Effect.MoveBy = function(element, toTop, toLeft) { + return new Effect.Move(element, + Object.extend({ x: toLeft, y: toTop }, arguments[3] || {})); +}; + +Effect.Scale = Class.create(); +Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { + initialize: function(element, percent) { + this.element = $(element) + var options = Object.extend({ + scaleX: true, + scaleY: true, + scaleContent: true, + scaleFromCenter: false, + scaleMode: 'box', // 'box' or 'contents' or {} with provided values + scaleFrom: 100.0, + scaleTo: percent + }, arguments[2] || {}); + this.start(options); + }, + setup: function() { + this.restoreAfterFinish = this.options.restoreAfterFinish || false; + this.elementPositioning = this.element.getStyle('position'); + + this.originalStyle = {}; + ['top','left','width','height','fontSize'].each( function(k) { + this.originalStyle[k] = this.element.style[k]; + }.bind(this)); + + this.originalTop = this.element.offsetTop; + this.originalLeft = this.element.offsetLeft; + + var fontSize = this.element.getStyle('font-size') || '100%'; + ['em','px','%'].each( function(fontSizeType) { + if(fontSize.indexOf(fontSizeType)>0) { + this.fontSize = parseFloat(fontSize); + this.fontSizeType = fontSizeType; + } + }.bind(this)); + + this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; + + this.dims = null; + if(this.options.scaleMode=='box') + this.dims = [this.element.offsetHeight, this.element.offsetWidth]; + if(/^content/.test(this.options.scaleMode)) + this.dims = [this.element.scrollHeight, this.element.scrollWidth]; + if(!this.dims) + this.dims = [this.options.scaleMode.originalHeight, + this.options.scaleMode.originalWidth]; + }, + update: function(position) { + var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); + if(this.options.scaleContent && this.fontSize) + this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); + this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); + }, + finish: function(position) { + if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); + }, + setDimensions: function(height, width) { + var d = {}; + if(this.options.scaleX) d.width = width + 'px'; + if(this.options.scaleY) d.height = height + 'px'; + if(this.options.scaleFromCenter) { + var topd = (height - this.dims[0])/2; + var leftd = (width - this.dims[1])/2; + if(this.elementPositioning == 'absolute') { + if(this.options.scaleY) d.top = this.originalTop-topd + 'px'; + if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; + } else { + if(this.options.scaleY) d.top = -topd + 'px'; + if(this.options.scaleX) d.left = -leftd + 'px'; + } + } + this.element.setStyle(d); + } +}); + +Effect.Highlight = Class.create(); +Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {}); + this.start(options); + }, + setup: function() { + // Prevent executing on elements not in the layout flow + if(this.element.getStyle('display')=='none') { this.cancel(); return; } + // Disable background image during the effect + this.oldStyle = { + backgroundImage: this.element.getStyle('background-image') }; + this.element.setStyle({backgroundImage: 'none'}); + if(!this.options.endcolor) + this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); + if(!this.options.restorecolor) + this.options.restorecolor = this.element.getStyle('background-color'); + // init color calculations + this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); + this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); + }, + update: function(position) { + this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ + return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) }); + }, + finish: function() { + this.element.setStyle(Object.extend(this.oldStyle, { + backgroundColor: this.options.restorecolor + })); + } +}); + +Effect.ScrollTo = Class.create(); +Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + this.start(arguments[1] || {}); + }, + setup: function() { + Position.prepare(); + var offsets = Position.cumulativeOffset(this.element); + if(this.options.offset) offsets[1] += this.options.offset; + var max = window.innerHeight ? + window.height - window.innerHeight : + document.body.scrollHeight - + (document.documentElement.clientHeight ? + document.documentElement.clientHeight : document.body.clientHeight); + this.scrollStart = Position.deltaY; + this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart; + }, + update: function(position) { + Position.prepare(); + window.scrollTo(Position.deltaX, + this.scrollStart + (position*this.delta)); + } +}); + +/* ------------- combination effects ------------- */ + +Effect.Fade = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + var options = Object.extend({ + from: element.getOpacity() || 1.0, + to: 0.0, + afterFinishInternal: function(effect) { + if(effect.options.to!=0) return; + effect.element.hide(); + effect.element.setStyle({opacity: oldOpacity}); + }}, arguments[1] || {}); + return new Effect.Opacity(element,options); +} + +Effect.Appear = function(element) { + element = $(element); + var options = Object.extend({ + from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), + to: 1.0, + // force Safari to render floated elements properly + afterFinishInternal: function(effect) { + effect.element.forceRerendering(); + }, + beforeSetup: function(effect) { + effect.element.setOpacity(effect.options.from); + effect.element.show(); + }}, arguments[1] || {}); + return new Effect.Opacity(element,options); +} + +Effect.Puff = function(element) { + element = $(element); + var oldStyle = { opacity: element.getInlineOpacity(), position: element.getStyle('position') }; + return new Effect.Parallel( + [ new Effect.Scale(element, 200, + { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], + Object.extend({ duration: 1.0, + beforeSetupInternal: function(effect) { + effect.effects[0].element.setStyle({position: 'absolute'}); }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide(); + effect.effects[0].element.setStyle(oldStyle); } + }, arguments[1] || {}) + ); +} + +Effect.BlindUp = function(element) { + element = $(element); + element.makeClipping(); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + restoreAfterFinish: true, + afterFinishInternal: function(effect) { + effect.element.hide(); + effect.element.undoClipping(); + } + }, arguments[1] || {}) + ); +} + +Effect.BlindDown = function(element) { + element = $(element); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, + Object.extend({ scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makeClipping(); + effect.element.setStyle({height: '0px'}); + effect.element.show(); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping(); + } + }, arguments[1] || {}) + ); +} + +Effect.SwitchOff = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + return new Effect.Appear(element, { + duration: 0.4, + from: 0, + transition: Effect.Transitions.flicker, + afterFinishInternal: function(effect) { + new Effect.Scale(effect.element, 1, { + duration: 0.3, scaleFromCenter: true, + scaleX: false, scaleContent: false, restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makePositioned(); + effect.element.makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide(); + effect.element.undoClipping(); + effect.element.undoPositioned(); + effect.element.setStyle({opacity: oldOpacity}); + } + }) + } + }); +} + +Effect.DropOut = function(element) { + element = $(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left'), + opacity: element.getInlineOpacity() }; + return new Effect.Parallel( + [ new Effect.Move(element, {x: 0, y: 100, sync: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 }) ], + Object.extend( + { duration: 0.5, + beforeSetup: function(effect) { + effect.effects[0].element.makePositioned(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide(); + effect.effects[0].element.undoPositioned(); + effect.effects[0].element.setStyle(oldStyle); + } + }, arguments[1] || {})); +} + +Effect.Shake = function(element) { + element = $(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left') }; + return new Effect.Move(element, + { x: 20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { + effect.element.undoPositioned(); + effect.element.setStyle(oldStyle); + }}) }}) }}) }}) }}) }}); +} + +Effect.SlideDown = function(element) { + element = $(element); + element.cleanWhitespace(); + // SlideDown need to have the content of the element wrapped in a container element with fixed height! + var oldInnerBottom = $(element.firstChild).getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: window.opera ? 0 : 1, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.firstChild.makePositioned(); + if(window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping(); + effect.element.setStyle({height: '0px'}); + effect.element.show(); }, + afterUpdateInternal: function(effect) { + effect.element.firstChild.setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping(); + // IE will crash if child is undoPositioned first + if(/MSIE/.test(navigator.userAgent)){ + effect.element.undoPositioned(); + effect.element.firstChild.undoPositioned(); + }else{ + effect.element.firstChild.undoPositioned(); + effect.element.undoPositioned(); + } + effect.element.firstChild.setStyle({bottom: oldInnerBottom}); } + }, arguments[1] || {}) + ); +} + +Effect.SlideUp = function(element) { + element = $(element); + element.cleanWhitespace(); + var oldInnerBottom = $(element.firstChild).getStyle('bottom'); + return new Effect.Scale(element, window.opera ? 0 : 1, + Object.extend({ scaleContent: false, + scaleX: false, + scaleMode: 'box', + scaleFrom: 100, + restoreAfterFinish: true, + beforeStartInternal: function(effect) { + effect.element.makePositioned(); + effect.element.firstChild.makePositioned(); + if(window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping(); + effect.element.show(); }, + afterUpdateInternal: function(effect) { + effect.element.firstChild.setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); }, + afterFinishInternal: function(effect) { + effect.element.hide(); + effect.element.undoClipping(); + effect.element.firstChild.undoPositioned(); + effect.element.undoPositioned(); + effect.element.setStyle({bottom: oldInnerBottom}); } + }, arguments[1] || {}) + ); +} + +// Bug in opera makes the TD containing this element expand for a instance after finish +Effect.Squish = function(element) { + return new Effect.Scale(element, window.opera ? 1 : 0, + { restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makeClipping(effect.element); }, + afterFinishInternal: function(effect) { + effect.element.hide(effect.element); + effect.element.undoClipping(effect.element); } + }); +} + +Effect.Grow = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.full + }, arguments[1] || {}); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var initialMoveX, initialMoveY; + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + initialMoveX = initialMoveY = moveX = moveY = 0; + break; + case 'top-right': + initialMoveX = dims.width; + initialMoveY = moveY = 0; + moveX = -dims.width; + break; + case 'bottom-left': + initialMoveX = moveX = 0; + initialMoveY = dims.height; + moveY = -dims.height; + break; + case 'bottom-right': + initialMoveX = dims.width; + initialMoveY = dims.height; + moveX = -dims.width; + moveY = -dims.height; + break; + case 'center': + initialMoveX = dims.width / 2; + initialMoveY = dims.height / 2; + moveX = -dims.width / 2; + moveY = -dims.height / 2; + break; + } + + return new Effect.Move(element, { + x: initialMoveX, + y: initialMoveY, + duration: 0.01, + beforeSetup: function(effect) { + effect.element.hide(); + effect.element.makeClipping(); + effect.element.makePositioned(); + }, + afterFinishInternal: function(effect) { + new Effect.Parallel( + [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), + new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), + new Effect.Scale(effect.element, 100, { + scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, + sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) + ], Object.extend({ + beforeSetup: function(effect) { + effect.effects[0].element.setStyle({height: '0px'}); + effect.effects[0].element.show(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.undoClipping(); + effect.effects[0].element.undoPositioned(); + effect.effects[0].element.setStyle(oldStyle); + } + }, options) + ) + } + }); +} + +Effect.Shrink = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.none + }, arguments[1] || {}); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + moveX = moveY = 0; + break; + case 'top-right': + moveX = dims.width; + moveY = 0; + break; + case 'bottom-left': + moveX = 0; + moveY = dims.height; + break; + case 'bottom-right': + moveX = dims.width; + moveY = dims.height; + break; + case 'center': + moveX = dims.width / 2; + moveY = dims.height / 2; + break; + } + + return new Effect.Parallel( + [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), + new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), + new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) + ], Object.extend({ + beforeStartInternal: function(effect) { + effect.effects[0].element.makePositioned(); + effect.effects[0].element.makeClipping(); }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide(); + effect.effects[0].element.undoClipping(); + effect.effects[0].element.undoPositioned(); + effect.effects[0].element.setStyle(oldStyle); } + }, options) + ); +} + +Effect.Pulsate = function(element) { + element = $(element); + var options = arguments[1] || {}; + var oldOpacity = element.getInlineOpacity(); + var transition = options.transition || Effect.Transitions.sinoidal; + var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) }; + reverser.bind(transition); + return new Effect.Opacity(element, + Object.extend(Object.extend({ duration: 3.0, from: 0, + afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } + }, options), {transition: reverser})); +} + +Effect.Fold = function(element) { + element = $(element); + var oldStyle = { + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height }; + Element.makeClipping(element); + return new Effect.Scale(element, 5, Object.extend({ + scaleContent: false, + scaleX: false, + afterFinishInternal: function(effect) { + new Effect.Scale(element, 1, { + scaleContent: false, + scaleY: false, + afterFinishInternal: function(effect) { + effect.element.hide(); + effect.element.undoClipping(); + effect.element.setStyle(oldStyle); + } }); + }}, arguments[1] || {})); +}; + +['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom', + 'collectTextNodes','collectTextNodesIgnoreClass','childrenWithClassName'].each( + function(f) { Element.Methods[f] = Element[f]; } +); + +Element.Methods.visualEffect = function(element, effect, options) { + s = effect.gsub(/_/, '-').camelize(); + effect_class = s.charAt(0).toUpperCase() + s.substring(1); + new Effect[effect_class](element, options); + return $(element); +}; + +Element.addMethods();
\ No newline at end of file diff --git a/tests/test_tools/selenium/core/lib/scriptaculous/scriptaculous.js b/tests/test_tools/selenium/core/lib/scriptaculous/scriptaculous.js new file mode 100644 index 00000000..f61fc57f --- /dev/null +++ b/tests/test_tools/selenium/core/lib/scriptaculous/scriptaculous.js @@ -0,0 +1,47 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +var Scriptaculous = { + Version: '1.6.1', + require: function(libraryName) { + // inserting via DOM fails in Safari 2.0, so brute force approach + document.write('<script type="text/javascript" src="'+libraryName+'"></script>'); + }, + load: function() { + if((typeof Prototype=='undefined') || + (typeof Element == 'undefined') || + (typeof Element.Methods=='undefined') || + parseFloat(Prototype.Version.split(".")[0] + "." + + Prototype.Version.split(".")[1]) < 1.5) + throw("script.aculo.us requires the Prototype JavaScript framework >= 1.5.0"); + + $A(document.getElementsByTagName("script")).findAll( function(s) { + return (s.src && s.src.match(/scriptaculous\.js(\?.*)?$/)) + }).each( function(s) { + var path = s.src.replace(/scriptaculous\.js(\?.*)?$/,''); + var includes = s.src.match(/\?.*load=([a-z,]*)/); + (includes ? includes[1] : 'builder,effects,dragdrop,controls,slider').split(',').each( + function(include) { Scriptaculous.require(path+include+'.js') }); + }); + } +} + +Scriptaculous.load();
\ No newline at end of file diff --git a/tests/test_tools/selenium/core/lib/scriptaculous/slider.js b/tests/test_tools/selenium/core/lib/scriptaculous/slider.js new file mode 100644 index 00000000..c0f1fc01 --- /dev/null +++ b/tests/test_tools/selenium/core/lib/scriptaculous/slider.js @@ -0,0 +1,283 @@ +// Copyright (c) 2005 Marty Haught, Thomas Fuchs +// +// See http://script.aculo.us for more info +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +if(!Control) var Control = {}; +Control.Slider = Class.create(); + +// options: +// axis: 'vertical', or 'horizontal' (default) +// +// callbacks: +// onChange(value) +// onSlide(value) +Control.Slider.prototype = { + initialize: function(handle, track, options) { + var slider = this; + + if(handle instanceof Array) { + this.handles = handle.collect( function(e) { return $(e) }); + } else { + this.handles = [$(handle)]; + } + + this.track = $(track); + this.options = options || {}; + + this.axis = this.options.axis || 'horizontal'; + this.increment = this.options.increment || 1; + this.step = parseInt(this.options.step || '1'); + this.range = this.options.range || $R(0,1); + + this.value = 0; // assure backwards compat + this.values = this.handles.map( function() { return 0 }); + this.spans = this.options.spans ? this.options.spans.map(function(s){ return $(s) }) : false; + this.options.startSpan = $(this.options.startSpan || null); + this.options.endSpan = $(this.options.endSpan || null); + + this.restricted = this.options.restricted || false; + + this.maximum = this.options.maximum || this.range.end; + this.minimum = this.options.minimum || this.range.start; + + // Will be used to align the handle onto the track, if necessary + this.alignX = parseInt(this.options.alignX || '0'); + this.alignY = parseInt(this.options.alignY || '0'); + + this.trackLength = this.maximumOffset() - this.minimumOffset(); + this.handleLength = this.isVertical() ? this.handles[0].offsetHeight : this.handles[0].offsetWidth; + + this.active = false; + this.dragging = false; + this.disabled = false; + + if(this.options.disabled) this.setDisabled(); + + // Allowed values array + this.allowedValues = this.options.values ? this.options.values.sortBy(Prototype.K) : false; + if(this.allowedValues) { + this.minimum = this.allowedValues.min(); + this.maximum = this.allowedValues.max(); + } + + this.eventMouseDown = this.startDrag.bindAsEventListener(this); + this.eventMouseUp = this.endDrag.bindAsEventListener(this); + this.eventMouseMove = this.update.bindAsEventListener(this); + + // Initialize handles in reverse (make sure first handle is active) + this.handles.each( function(h,i) { + i = slider.handles.length-1-i; + slider.setValue(parseFloat( + (slider.options.sliderValue instanceof Array ? + slider.options.sliderValue[i] : slider.options.sliderValue) || + slider.range.start), i); + Element.makePositioned(h); // fix IE + Event.observe(h, "mousedown", slider.eventMouseDown); + }); + + Event.observe(this.track, "mousedown", this.eventMouseDown); + Event.observe(document, "mouseup", this.eventMouseUp); + Event.observe(document, "mousemove", this.eventMouseMove); + + this.initialized = true; + }, + dispose: function() { + var slider = this; + Event.stopObserving(this.track, "mousedown", this.eventMouseDown); + Event.stopObserving(document, "mouseup", this.eventMouseUp); + Event.stopObserving(document, "mousemove", this.eventMouseMove); + this.handles.each( function(h) { + Event.stopObserving(h, "mousedown", slider.eventMouseDown); + }); + }, + setDisabled: function(){ + this.disabled = true; + }, + setEnabled: function(){ + this.disabled = false; + }, + getNearestValue: function(value){ + if(this.allowedValues){ + if(value >= this.allowedValues.max()) return(this.allowedValues.max()); + if(value <= this.allowedValues.min()) return(this.allowedValues.min()); + + var offset = Math.abs(this.allowedValues[0] - value); + var newValue = this.allowedValues[0]; + this.allowedValues.each( function(v) { + var currentOffset = Math.abs(v - value); + if(currentOffset <= offset){ + newValue = v; + offset = currentOffset; + } + }); + return newValue; + } + if(value > this.range.end) return this.range.end; + if(value < this.range.start) return this.range.start; + return value; + }, + setValue: function(sliderValue, handleIdx){ + if(!this.active) { + this.activeHandle = this.handles[handleIdx]; + this.activeHandleIdx = handleIdx; + this.updateStyles(); + } + handleIdx = handleIdx || this.activeHandleIdx || 0; + if(this.initialized && this.restricted) { + if((handleIdx>0) && (sliderValue<this.values[handleIdx-1])) + sliderValue = this.values[handleIdx-1]; + if((handleIdx < (this.handles.length-1)) && (sliderValue>this.values[handleIdx+1])) + sliderValue = this.values[handleIdx+1]; + } + sliderValue = this.getNearestValue(sliderValue); + this.values[handleIdx] = sliderValue; + this.value = this.values[0]; // assure backwards compat + + this.handles[handleIdx].style[this.isVertical() ? 'top' : 'left'] = + this.translateToPx(sliderValue); + + this.drawSpans(); + if(!this.dragging || !this.event) this.updateFinished(); + }, + setValueBy: function(delta, handleIdx) { + this.setValue(this.values[handleIdx || this.activeHandleIdx || 0] + delta, + handleIdx || this.activeHandleIdx || 0); + }, + translateToPx: function(value) { + return Math.round( + ((this.trackLength-this.handleLength)/(this.range.end-this.range.start)) * + (value - this.range.start)) + "px"; + }, + translateToValue: function(offset) { + return ((offset/(this.trackLength-this.handleLength) * + (this.range.end-this.range.start)) + this.range.start); + }, + getRange: function(range) { + var v = this.values.sortBy(Prototype.K); + range = range || 0; + return $R(v[range],v[range+1]); + }, + minimumOffset: function(){ + return(this.isVertical() ? this.alignY : this.alignX); + }, + maximumOffset: function(){ + return(this.isVertical() ? + this.track.offsetHeight - this.alignY : this.track.offsetWidth - this.alignX); + }, + isVertical: function(){ + return (this.axis == 'vertical'); + }, + drawSpans: function() { + var slider = this; + if(this.spans) + $R(0, this.spans.length-1).each(function(r) { slider.setSpan(slider.spans[r], slider.getRange(r)) }); + if(this.options.startSpan) + this.setSpan(this.options.startSpan, + $R(0, this.values.length>1 ? this.getRange(0).min() : this.value )); + if(this.options.endSpan) + this.setSpan(this.options.endSpan, + $R(this.values.length>1 ? this.getRange(this.spans.length-1).max() : this.value, this.maximum)); + }, + setSpan: function(span, range) { + if(this.isVertical()) { + span.style.top = this.translateToPx(range.start); + span.style.height = this.translateToPx(range.end - range.start + this.range.start); + } else { + span.style.left = this.translateToPx(range.start); + span.style.width = this.translateToPx(range.end - range.start + this.range.start); + } + }, + updateStyles: function() { + this.handles.each( function(h){ Element.removeClassName(h, 'selected') }); + Element.addClassName(this.activeHandle, 'selected'); + }, + startDrag: function(event) { + if(Event.isLeftClick(event)) { + if(!this.disabled){ + this.active = true; + + var handle = Event.element(event); + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + if(handle==this.track) { + var offsets = Position.cumulativeOffset(this.track); + this.event = event; + this.setValue(this.translateToValue( + (this.isVertical() ? pointer[1]-offsets[1] : pointer[0]-offsets[0])-(this.handleLength/2) + )); + var offsets = Position.cumulativeOffset(this.activeHandle); + this.offsetX = (pointer[0] - offsets[0]); + this.offsetY = (pointer[1] - offsets[1]); + } else { + // find the handle (prevents issues with Safari) + while((this.handles.indexOf(handle) == -1) && handle.parentNode) + handle = handle.parentNode; + + this.activeHandle = handle; + this.activeHandleIdx = this.handles.indexOf(this.activeHandle); + this.updateStyles(); + + var offsets = Position.cumulativeOffset(this.activeHandle); + this.offsetX = (pointer[0] - offsets[0]); + this.offsetY = (pointer[1] - offsets[1]); + } + } + Event.stop(event); + } + }, + update: function(event) { + if(this.active) { + if(!this.dragging) this.dragging = true; + this.draw(event); + // fix AppleWebKit rendering + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + Event.stop(event); + } + }, + draw: function(event) { + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var offsets = Position.cumulativeOffset(this.track); + pointer[0] -= this.offsetX + offsets[0]; + pointer[1] -= this.offsetY + offsets[1]; + this.event = event; + this.setValue(this.translateToValue( this.isVertical() ? pointer[1] : pointer[0] )); + if(this.initialized && this.options.onSlide) + this.options.onSlide(this.values.length>1 ? this.values : this.value, this); + }, + endDrag: function(event) { + if(this.active && this.dragging) { + this.finishDrag(event, true); + Event.stop(event); + } + this.active = false; + this.dragging = false; + }, + finishDrag: function(event, success) { + this.active = false; + this.dragging = false; + this.updateFinished(); + }, + updateFinished: function() { + if(this.initialized && this.options.onChange) + this.options.onChange(this.values.length>1 ? this.values : this.value, this); + this.event = null; + } +}
\ No newline at end of file diff --git a/tests/test_tools/selenium/core/lib/scriptaculous/unittest.js b/tests/test_tools/selenium/core/lib/scriptaculous/unittest.js new file mode 100644 index 00000000..d2c2d817 --- /dev/null +++ b/tests/test_tools/selenium/core/lib/scriptaculous/unittest.js @@ -0,0 +1,383 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005 Jon Tirsen (http://www.tirsen.com) +// (c) 2005 Michael Schuerig (http://www.schuerig.de/michael/) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +// experimental, Firefox-only +Event.simulateMouse = function(element, eventName) { + var options = Object.extend({ + pointerX: 0, + pointerY: 0, + buttons: 0 + }, arguments[2] || {}); + var oEvent = document.createEvent("MouseEvents"); + oEvent.initMouseEvent(eventName, true, true, document.defaultView, + options.buttons, options.pointerX, options.pointerY, options.pointerX, options.pointerY, + false, false, false, false, 0, $(element)); + + if(this.mark) Element.remove(this.mark); + this.mark = document.createElement('div'); + this.mark.appendChild(document.createTextNode(" ")); + document.body.appendChild(this.mark); + this.mark.style.position = 'absolute'; + this.mark.style.top = options.pointerY + "px"; + this.mark.style.left = options.pointerX + "px"; + this.mark.style.width = "5px"; + this.mark.style.height = "5px;"; + this.mark.style.borderTop = "1px solid red;" + this.mark.style.borderLeft = "1px solid red;" + + if(this.step) + alert('['+new Date().getTime().toString()+'] '+eventName+'/'+Test.Unit.inspect(options)); + + $(element).dispatchEvent(oEvent); +}; + +// Note: Due to a fix in Firefox 1.0.5/6 that probably fixed "too much", this doesn't work in 1.0.6 or DP2. +// You need to downgrade to 1.0.4 for now to get this working +// See https://bugzilla.mozilla.org/show_bug.cgi?id=289940 for the fix that fixed too much +Event.simulateKey = function(element, eventName) { + var options = Object.extend({ + ctrlKey: false, + altKey: false, + shiftKey: false, + metaKey: false, + keyCode: 0, + charCode: 0 + }, arguments[2] || {}); + + var oEvent = document.createEvent("KeyEvents"); + oEvent.initKeyEvent(eventName, true, true, window, + options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, + options.keyCode, options.charCode ); + $(element).dispatchEvent(oEvent); +}; + +Event.simulateKeys = function(element, command) { + for(var i=0; i<command.length; i++) { + Event.simulateKey(element,'keypress',{charCode:command.charCodeAt(i)}); + } +}; + +var Test = {} +Test.Unit = {}; + +// security exception workaround +Test.Unit.inspect = Object.inspect; + +Test.Unit.Logger = Class.create(); +Test.Unit.Logger.prototype = { + initialize: function(log) { + this.log = $(log); + if (this.log) { + this._createLogTable(); + } + }, + start: function(testName) { + if (!this.log) return; + this.testName = testName; + this.lastLogLine = document.createElement('tr'); + this.statusCell = document.createElement('td'); + this.nameCell = document.createElement('td'); + this.nameCell.appendChild(document.createTextNode(testName)); + this.messageCell = document.createElement('td'); + this.lastLogLine.appendChild(this.statusCell); + this.lastLogLine.appendChild(this.nameCell); + this.lastLogLine.appendChild(this.messageCell); + this.loglines.appendChild(this.lastLogLine); + }, + finish: function(status, summary) { + if (!this.log) return; + this.lastLogLine.className = status; + this.statusCell.innerHTML = status; + this.messageCell.innerHTML = this._toHTML(summary); + }, + message: function(message) { + if (!this.log) return; + this.messageCell.innerHTML = this._toHTML(message); + }, + summary: function(summary) { + if (!this.log) return; + this.logsummary.innerHTML = this._toHTML(summary); + }, + _createLogTable: function() { + this.log.innerHTML = + '<div id="logsummary"></div>' + + '<table id="logtable">' + + '<thead><tr><th>Status</th><th>Test</th><th>Message</th></tr></thead>' + + '<tbody id="loglines"></tbody>' + + '</table>'; + this.logsummary = $('logsummary') + this.loglines = $('loglines'); + }, + _toHTML: function(txt) { + return txt.escapeHTML().replace(/\n/g,"<br/>"); + } +} + +Test.Unit.Runner = Class.create(); +Test.Unit.Runner.prototype = { + initialize: function(testcases) { + this.options = Object.extend({ + testLog: 'testlog' + }, arguments[1] || {}); + this.options.resultsURL = this.parseResultsURLQueryParameter(); + if (this.options.testLog) { + this.options.testLog = $(this.options.testLog) || null; + } + if(this.options.tests) { + this.tests = []; + for(var i = 0; i < this.options.tests.length; i++) { + if(/^test/.test(this.options.tests[i])) { + this.tests.push(new Test.Unit.Testcase(this.options.tests[i], testcases[this.options.tests[i]], testcases["setup"], testcases["teardown"])); + } + } + } else { + if (this.options.test) { + this.tests = [new Test.Unit.Testcase(this.options.test, testcases[this.options.test], testcases["setup"], testcases["teardown"])]; + } else { + this.tests = []; + for(var testcase in testcases) { + if(/^test/.test(testcase)) { + this.tests.push(new Test.Unit.Testcase(testcase, testcases[testcase], testcases["setup"], testcases["teardown"])); + } + } + } + } + this.currentTest = 0; + this.logger = new Test.Unit.Logger(this.options.testLog); + setTimeout(this.runTests.bind(this), 1000); + }, + parseResultsURLQueryParameter: function() { + return window.location.search.parseQuery()["resultsURL"]; + }, + // Returns: + // "ERROR" if there was an error, + // "FAILURE" if there was a failure, or + // "SUCCESS" if there was neither + getResult: function() { + var hasFailure = false; + for(var i=0;i<this.tests.length;i++) { + if (this.tests[i].errors > 0) { + return "ERROR"; + } + if (this.tests[i].failures > 0) { + hasFailure = true; + } + } + if (hasFailure) { + return "FAILURE"; + } else { + return "SUCCESS"; + } + }, + postResults: function() { + if (this.options.resultsURL) { + new Ajax.Request(this.options.resultsURL, + { method: 'get', parameters: 'result=' + this.getResult(), asynchronous: false }); + } + }, + runTests: function() { + var test = this.tests[this.currentTest]; + if (!test) { + // finished! + this.postResults(); + this.logger.summary(this.summary()); + return; + } + if(!test.isWaiting) { + this.logger.start(test.name); + } + test.run(); + if(test.isWaiting) { + this.logger.message("Waiting for " + test.timeToWait + "ms"); + setTimeout(this.runTests.bind(this), test.timeToWait || 1000); + } else { + this.logger.finish(test.status(), test.summary()); + this.currentTest++; + // tail recursive, hopefully the browser will skip the stackframe + this.runTests(); + } + }, + summary: function() { + var assertions = 0; + var failures = 0; + var errors = 0; + var messages = []; + for(var i=0;i<this.tests.length;i++) { + assertions += this.tests[i].assertions; + failures += this.tests[i].failures; + errors += this.tests[i].errors; + } + return ( + this.tests.length + " tests, " + + assertions + " assertions, " + + failures + " failures, " + + errors + " errors"); + } +} + +Test.Unit.Assertions = Class.create(); +Test.Unit.Assertions.prototype = { + initialize: function() { + this.assertions = 0; + this.failures = 0; + this.errors = 0; + this.messages = []; + }, + summary: function() { + return ( + this.assertions + " assertions, " + + this.failures + " failures, " + + this.errors + " errors" + "\n" + + this.messages.join("\n")); + }, + pass: function() { + this.assertions++; + }, + fail: function(message) { + this.failures++; + this.messages.push("Failure: " + message); + }, + info: function(message) { + this.messages.push("Info: " + message); + }, + error: function(error) { + this.errors++; + this.messages.push(error.name + ": "+ error.message + "(" + Test.Unit.inspect(error) +")"); + }, + status: function() { + if (this.failures > 0) return 'failed'; + if (this.errors > 0) return 'error'; + return 'passed'; + }, + assert: function(expression) { + var message = arguments[1] || 'assert: got "' + Test.Unit.inspect(expression) + '"'; + try { expression ? this.pass() : + this.fail(message); } + catch(e) { this.error(e); } + }, + assertEqual: function(expected, actual) { + var message = arguments[2] || "assertEqual"; + try { (expected == actual) ? this.pass() : + this.fail(message + ': expected "' + Test.Unit.inspect(expected) + + '", actual "' + Test.Unit.inspect(actual) + '"'); } + catch(e) { this.error(e); } + }, + assertEnumEqual: function(expected, actual) { + var message = arguments[2] || "assertEnumEqual"; + try { $A(expected).length == $A(actual).length && + expected.zip(actual).all(function(pair) { return pair[0] == pair[1] }) ? + this.pass() : this.fail(message + ': expected ' + Test.Unit.inspect(expected) + + ', actual ' + Test.Unit.inspect(actual)); } + catch(e) { this.error(e); } + }, + assertNotEqual: function(expected, actual) { + var message = arguments[2] || "assertNotEqual"; + try { (expected != actual) ? this.pass() : + this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); } + catch(e) { this.error(e); } + }, + assertNull: function(obj) { + var message = arguments[1] || 'assertNull' + try { (obj==null) ? this.pass() : + this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); } + catch(e) { this.error(e); } + }, + assertHidden: function(element) { + var message = arguments[1] || 'assertHidden'; + this.assertEqual("none", element.style.display, message); + }, + assertNotNull: function(object) { + var message = arguments[1] || 'assertNotNull'; + this.assert(object != null, message); + }, + assertInstanceOf: function(expected, actual) { + var message = arguments[2] || 'assertInstanceOf'; + try { + (actual instanceof expected) ? this.pass() : + this.fail(message + ": object was not an instance of the expected type"); } + catch(e) { this.error(e); } + }, + assertNotInstanceOf: function(expected, actual) { + var message = arguments[2] || 'assertNotInstanceOf'; + try { + !(actual instanceof expected) ? this.pass() : + this.fail(message + ": object was an instance of the not expected type"); } + catch(e) { this.error(e); } + }, + _isVisible: function(element) { + element = $(element); + if(!element.parentNode) return true; + this.assertNotNull(element); + if(element.style && Element.getStyle(element, 'display') == 'none') + return false; + + return this._isVisible(element.parentNode); + }, + assertNotVisible: function(element) { + this.assert(!this._isVisible(element), Test.Unit.inspect(element) + " was not hidden and didn't have a hidden parent either. " + ("" || arguments[1])); + }, + assertVisible: function(element) { + this.assert(this._isVisible(element), Test.Unit.inspect(element) + " was not visible. " + ("" || arguments[1])); + }, + benchmark: function(operation, iterations) { + var startAt = new Date(); + (iterations || 1).times(operation); + var timeTaken = ((new Date())-startAt); + this.info((arguments[2] || 'Operation') + ' finished ' + + iterations + ' iterations in ' + (timeTaken/1000)+'s' ); + return timeTaken; + } +} + +Test.Unit.Testcase = Class.create(); +Object.extend(Object.extend(Test.Unit.Testcase.prototype, Test.Unit.Assertions.prototype), { + initialize: function(name, test, setup, teardown) { + Test.Unit.Assertions.prototype.initialize.bind(this)(); + this.name = name; + this.test = test || function() {}; + this.setup = setup || function() {}; + this.teardown = teardown || function() {}; + this.isWaiting = false; + this.timeToWait = 1000; + }, + wait: function(time, nextPart) { + this.isWaiting = true; + this.test = nextPart; + this.timeToWait = time; + }, + run: function() { + try { + try { + if (!this.isWaiting) this.setup.bind(this)(); + this.isWaiting = false; + this.test.bind(this)(); + } finally { + if(!this.isWaiting) { + this.teardown.bind(this)(); + } + } + } + catch(e) { this.error(e); } + } +}); diff --git a/tests/test_tools/selenium/core/scripts/htmlutils.js b/tests/test_tools/selenium/core/scripts/htmlutils.js index fcb1ee44..4d78e1a6 100644 --- a/tests/test_tools/selenium/core/scripts/htmlutils.js +++ b/tests/test_tools/selenium/core/scripts/htmlutils.js @@ -14,20 +14,21 @@ * limitations under the License. * */ - -// This script contains some HTML utility functions that -// make it possible to handle elements in a way that is -// compatible with both IE-like and Mozilla-like browsers + +// This script contains a badly-organised collection of miscellaneous +// functions that really better homes. String.prototype.trim = function() { - var result = this.replace( /^\s+/g, "" );// strip leading - return result.replace( /\s+$/g, "" );// strip trailing + var result = this.replace(/^\s+/g, ""); + // strip leading + return result.replace(/\s+$/g, ""); + // strip trailing }; String.prototype.lcfirst = function() { - return this.charAt(0).toLowerCase() + this.substr(1); + return this.charAt(0).toLowerCase() + this.substr(1); }; String.prototype.ucfirst = function() { - return this.charAt(0).toUpperCase() + this.substr(1); + return this.charAt(0).toUpperCase() + this.substr(1); }; String.prototype.startsWith = function(str) { return this.indexOf(str) == 0; @@ -37,23 +38,12 @@ String.prototype.startsWith = function(str) { function getText(element) { var text = ""; - if(browserVersion.isFirefox && browserVersion.firefoxVersion >= "1.5") - { - var dummyElement = element.cloneNode(true); - renderWhitespaceInTextContent(dummyElement); - text = dummyElement.textContent; - } else if (browserVersion.isOpera) { - var dummyElement = element.cloneNode(true); - renderWhitespaceInTextContent(dummyElement); - text = dummyElement.innerText; - text = xmlDecode(text); - } - else if(element.textContent) - { + var isRecentFirefox = (browserVersion.isFirefox && browserVersion.firefoxVersion >= "1.5"); + if (isRecentFirefox || browserVersion.isKonqueror || browserVersion.isSafari || browserVersion.isOpera) { + text = getTextContent(element); + } else if (element.textContent) { text = element.textContent; - } - else if(element.innerText) - { + } else if (element.innerText) { text = element.innerText; } @@ -63,62 +53,34 @@ function getText(element) { return text.trim(); } -function renderWhitespaceInTextContent(element) { - // Remove non-visible newlines in text nodes - if (element.nodeType == Node.TEXT_NODE) - { - element.data = element.data.replace(/\n|\r|\t/g, " "); - return; - } - - if (element.nodeType == Node.COMMENT_NODE) - { - element.data = ""; - return; - } - - // Don't modify PRE elements - if (element.tagName == "PRE") - { - return; - } - - // Handle inline element that force newlines - if (tagIs(element, ["BR", "HR"])) - { - // Replace this element with a newline text element - element.parentNode.replaceChild(element.ownerDocument.createTextNode("\n"), element) - } - - for (var i = 0; i < element.childNodes.length; i++) - { - var child = element.childNodes.item(i) - renderWhitespaceInTextContent(child); - } - - // Handle block elements that introduce newlines -// -- From HTML spec: -//<!ENTITY % block -// "P | %heading; | %list; | %preformatted; | DL | DIV | NOSCRIPT | -// BLOCKQUOTE | FORM | HR | TABLE | FIELDSET | ADDRESS"> - if (tagIs(element, ["P", "DIV"])) - { - element.appendChild(element.ownerDocument.createTextNode("\n"), element) +function getTextContent(element, preformatted) { + if (element.nodeType == 3 /*Node.TEXT_NODE*/) { + var text = element.data; + if (!preformatted) { + text = text.replace(/\n|\r|\t/g, " "); + } + return text; } - -} - -function tagIs(element, tags) -{ - var tag = element.tagName; - for (var i = 0; i < tags.length; i++) - { - if (tags[i] == tag) - { - return true; + if (element.nodeType == 1 /*Node.ELEMENT_NODE*/) { + var childrenPreformatted = preformatted || (element.tagName == "PRE"); + var text = ""; + for (var i = 0; i < element.childNodes.length; i++) { + var child = element.childNodes.item(i); + text += getTextContent(child, childrenPreformatted); } + // Handle block elements that introduce newlines + // -- From HTML spec: + //<!ENTITY % block + // "P | %heading; | %list; | %preformatted; | DL | DIV | NOSCRIPT | + // BLOCKQUOTE | F:wORM | HR | TABLE | FIELDSET | ADDRESS"> + // + // TODO: should potentially introduce multiple newlines to separate blocks + if (element.tagName == "P" || element.tagName == "BR" || element.tagName == "HR" || element.tagName == "DIV") { + text += "\n"; + } + return text; } - return false; + return ''; } /** @@ -145,25 +107,36 @@ function normalizeSpaces(text) text = text.replace(/\ +/g, " "); // Replace with a space - var pat = String.fromCharCode(160); // Opera doesn't like /\240/g - var re = new RegExp(pat, "g"); - return text.replace(re, " "); + var nbspPattern = new RegExp(String.fromCharCode(160), "g"); + if (browserVersion.isSafari) { + return replaceAll(text, String.fromCharCode(160), " "); + } else { + return text.replace(nbspPattern, " "); + } +} + +function replaceAll(text, oldText, newText) { + while (text.indexOf(oldText) != -1) { + text = text.replace(oldText, newText); + } + return text; } + function xmlDecode(text) { - text = text.replace(/"/g, '"'); - text = text.replace(/'/g, "'"); - text = text.replace(/</g, "<"); - text = text.replace(/>/g, ">"); - text = text.replace(/&/g, "&"); - return text; + text = text.replace(/"/g, '"'); + text = text.replace(/'/g, "'"); + text = text.replace(/</g, "<"); + text = text.replace(/>/g, ">"); + text = text.replace(/&/g, "&"); + return text; } // Sets the text in this element function setText(element, text) { - if(element.textContent) { + if (element.textContent) { element.textContent = text; - } else if(element.innerText) { + } else if (element.innerText) { element.innerText = text; } } @@ -191,43 +164,105 @@ function triggerEvent(element, eventType, canBubble) { } } -function triggerKeyEvent(element, eventType, keycode, canBubble) { +function getKeyCodeFromKeySequence(keySequence) { + var match = /^\\(\d{1,3})$/.exec(keySequence); + if (match != null) { + return match[1]; + } + match = /^.$/.exec(keySequence); + if (match != null) { + return match[0].charCodeAt(0); + } + // this is for backward compatibility with existing tests + // 1 digit ascii codes will break however because they are used for the digit chars + match = /^\d{2,3}$/.exec(keySequence); + if (match != null) { + return match[0]; + } + throw SeleniumError("invalid keySequence"); +} + +function triggerKeyEvent(element, eventType, keySequence, canBubble) { + var keycode = getKeyCodeFromKeySequence(keySequence); canBubble = (typeof(canBubble) == undefined) ? true : canBubble; if (element.fireEvent) { - keyEvent = parent.frames['myiframe'].document.createEventObject(); - keyEvent.keyCode=keycode; - element.fireEvent('on' + eventType, keyEvent); + keyEvent = element.ownerDocument.createEventObject(); + keyEvent.keyCode = keycode; + element.fireEvent('on' + eventType, keyEvent); } else { - var evt; - if( window.KeyEvent ) { - evt = document.createEvent('KeyEvents'); - evt.initKeyEvent(eventType, true, true, window, false, false, false, false, keycode, keycode); - } else { - evt = document.createEvent('UIEvents'); - evt.initUIEvent( eventType, true, true, window, 1 ); - evt.keyCode = keycode; - } - + var evt; + if (window.KeyEvent) { + evt = document.createEvent('KeyEvents'); + evt.initKeyEvent(eventType, true, true, window, false, false, false, false, keycode, keycode); + } else { + evt = document.createEvent('UIEvents'); + evt.initUIEvent(eventType, true, true, window, 1); + evt.keyCode = keycode; + } + element.dispatchEvent(evt); } } /* Fire a mouse event in a browser-compatible manner */ -function triggerMouseEvent(element, eventType, canBubble) { +function triggerMouseEvent(element, eventType, canBubble, clientX, clientY) { + clientX = clientX ? clientX : 0; + clientY = clientY ? clientY : 0; + + // TODO: set these attributes -- they don't seem to be needed by the initial test cases, but that could change... + var screenX = 0; + var screenY = 0; + canBubble = (typeof(canBubble) == undefined) ? true : canBubble; if (element.fireEvent) { - element.fireEvent('on' + eventType); + LOG.error("element has fireEvent"); + if (!screenX && !screenY && !clientX && !clientY) { + element.fireEvent('on' + eventType); + } + else { + var ieEvent = element.ownerDocument.createEventObject(); + ieEvent.detail = 0; + ieEvent.screenX = screenX; + ieEvent.screenY = screenY; + ieEvent.clientX = clientX; + ieEvent.clientY = clientY; + ieEvent.ctrlKey = false; + ieEvent.altKey = false; + ieEvent.shiftKey = false; + ieEvent.metaKey = false; + ieEvent.button = 1; + ieEvent.relatedTarget = null; + + // when we go this route, window.event is never set to contain the event we have just created. + // ideally we could just slide it in as follows in the try-block below, but this normally + // doesn't work. This is why I try to avoid this code path, which is only required if we need to + // set attributes on the event (e.g., clientX). + try { + window.event = ieEvent; + } + catch(e) { + // getting an "Object does not support this action or property" error. Save the event away + // for future reference. + // TODO: is there a way to update window.event? + + // work around for http://jira.openqa.org/browse/SEL-280 -- make the event available somewhere: + selenium.browserbot.getCurrentWindow().selenium_event = ieEvent; + } + element.fireEvent('on' + eventType, ieEvent); + } } else { + LOG.error("element doesn't have fireEvent"); var evt = document.createEvent('MouseEvents'); if (evt.initMouseEvent) { - evt.initMouseEvent(eventType, canBubble, true, document.defaultView, 1, 0, 0, 0, 0, false, false, false, false, 0, null) + LOG.error("element has initMouseEvent"); + //Safari + evt.initMouseEvent(eventType, canBubble, true, document.defaultView, 1, screenX, screenY, clientX, clientY, false, false, false, false, 0, null) } - else - { - // Safari + else { + LOG.error("element doesen't has initMouseEvent"); // TODO we should be initialising other mouse-event related attributes here evt.initEvent(eventType, canBubble, true); } @@ -236,6 +271,7 @@ function triggerMouseEvent(element, eventType, canBubble) { } function removeLoadListener(element, command) { + LOG.info('Removing loadListenter for ' + element + ', ' + command); if (window.removeEventListener) element.removeEventListener("load", command, true); else if (window.detachEvent) @@ -243,17 +279,11 @@ function removeLoadListener(element, command) { } function addLoadListener(element, command) { + LOG.info('Adding loadListenter for ' + element + ', ' + command); if (window.addEventListener && !browserVersion.isOpera) - element.addEventListener("load",command, true); + element.addEventListener("load", command, true); else if (window.attachEvent) - element.attachEvent("onload",command); -} - -function addUnloadListener(element, command) { - if (window.addEventListener) - element.addEventListener("unload",command, true); - else if (window.attachEvent) - element.attachEvent("onunload",command); + element.attachEvent("onload", command); } /** @@ -261,19 +291,19 @@ function addUnloadListener(element, command) { * This file must be loaded _after_ the jsunitCore.js */ function getFunctionName(aFunction) { - var regexpResult = aFunction.toString().match(/function (\w*)/); - if (regexpResult && regexpResult[1]) { - return regexpResult[1]; - } - return 'anonymous'; + var regexpResult = aFunction.toString().match(/function (\w*)/); + if (regexpResult && regexpResult[1]) { + return regexpResult[1]; + } + return 'anonymous'; } function getDocumentBase(doc) { - var bases = document.getElementsByTagName("base"); - if (bases && bases.length && bases[0].href) { - return bases[0].href; - } - return ""; + var bases = document.getElementsByTagName("base"); + if (bases && bases.length && bases[0].href) { + return bases[0].href; + } + return ""; } function describe(object, delimiter) { @@ -291,10 +321,15 @@ PatternMatcher.prototype = { selectStrategy: function(pattern) { this.pattern = pattern; - var strategyName = 'glob'; // by default + var strategyName = 'glob'; + // by default if (/^([a-z-]+):(.*)/.test(pattern)) { - strategyName = RegExp.$1; - pattern = RegExp.$2; + var possibleNewStrategyName = RegExp.$1; + var possibleNewPattern = RegExp.$2; + if (PatternMatcher.strategies[possibleNewStrategyName]) { + strategyName = possibleNewStrategyName; + pattern = possibleNewPattern; + } } var matchStrategy = PatternMatcher.strategies[strategyName]; if (!matchStrategy) { @@ -320,9 +355,9 @@ PatternMatcher.matches = function(pattern, actual) { PatternMatcher.strategies = { - /** - * Exact matching, e.g. "exact:***" - */ +/** + * Exact matching, e.g. "exact:***" + */ exact: function(expected) { this.expected = expected; this.matches = function(actual) { @@ -330,9 +365,9 @@ PatternMatcher.strategies = { }; }, - /** - * Match by regular expression, e.g. "regexp:^[0-9]+$" - */ +/** + * Match by regular expression, e.g. "regexp:^[0-9]+$" + */ regexp: function(regexpString) { this.regexp = new RegExp(regexpString); this.matches = function(actual) { @@ -340,17 +375,24 @@ PatternMatcher.strategies = { }; }, - /** - * "globContains" (aka "wildmat") patterns, e.g. "glob:one,two,*", - * but don't require a perfect match; instead succeed if actual - * contains something that matches globString. - * Making this distinction is motivated by a bug in IE6 which - * leads to the browser hanging if we implement *TextPresent tests - * by just matching against a regular expression beginning and - * ending with ".*". The globcontains strategy allows us to satisfy - * the functional needs of the *TextPresent ops more efficiently - * and so avoid running into this IE6 freeze. - */ + regex: function(regexpString) { + this.regexp = new RegExp(regexpString); + this.matches = function(actual) { + return this.regexp.test(actual); + }; + }, + +/** + * "globContains" (aka "wildmat") patterns, e.g. "glob:one,two,*", + * but don't require a perfect match; instead succeed if actual + * contains something that matches globString. + * Making this distinction is motivated by a bug in IE6 which + * leads to the browser hanging if we implement *TextPresent tests + * by just matching against a regular expression beginning and + * ending with ".*". The globcontains strategy allows us to satisfy + * the functional needs of the *TextPresent ops more efficiently + * and so avoid running into this IE6 freeze. + */ globContains: function(globString) { this.regexp = new RegExp(PatternMatcher.regexpFromGlobContains(globString)); this.matches = function(actual) { @@ -359,9 +401,9 @@ PatternMatcher.strategies = { }, - /** - * "glob" (aka "wildmat") patterns, e.g. "glob:one,two,*" - */ +/** + * "glob" (aka "wildmat") patterns, e.g. "glob:one,two,*" + */ glob: function(globString) { this.regexp = new RegExp(PatternMatcher.regexpFromGlob(globString)); this.matches = function(actual) { @@ -393,9 +435,9 @@ var Assert = { throw new AssertionFailedError(message); }, - /* - * Assert.equals(comment?, expected, actual) - */ +/* +* Assert.equals(comment?, expected, actual) +*/ equals: function() { var args = new AssertionArguments(arguments); if (args.expected === args.actual) { @@ -406,9 +448,9 @@ var Assert = { "' but was '" + args.actual + "'"); }, - /* - * Assert.matches(comment?, pattern, actual) - */ +/* +* Assert.matches(comment?, pattern, actual) +*/ matches: function() { var args = new AssertionArguments(arguments); if (PatternMatcher.matches(args.expected, args.actual)) { @@ -419,9 +461,9 @@ var Assert = { "' did not match '" + args.expected + "'"); }, - /* - * Assert.notMtches(comment?, pattern, actual) - */ +/* +* Assert.notMtches(comment?, pattern, actual) +*/ notMatches: function() { var args = new AssertionArguments(arguments); if (!PatternMatcher.matches(args.expected, args.actual)) { @@ -447,8 +489,6 @@ function AssertionArguments(args) { } } - - function AssertionFailedError(message) { this.isAssertionFailedError = true; this.isSeleniumError = true; @@ -460,4 +500,130 @@ function SeleniumError(message) { var error = new Error(message); error.isSeleniumError = true; return error; -}; +} + +var Effect = new Object(); + +Object.extend(Effect, { + highlight : function(element) { + var highLightColor = "yellow"; + if (element.originalColor == undefined) { // avoid picking up highlight + element.originalColor = Element.getStyle(element, "background-color"); + } + Element.setStyle(element, {"background-color" : highLightColor}); + window.setTimeout(function() { + //if element is orphan, probably page of it has already gone, so ignore + if (!element.parentNode) { + return; + } + Element.setStyle(element, {"background-color" : element.originalColor}); + }, 200); + } +}); + + +// for use from vs.2003 debugger +function objToString(obj) { + var s = ""; + for (key in obj) { + var line = key + "->" + obj[key]; + line.replace("\n", " "); + s += line + "\n"; + } + return s; +} + +var seenReadyStateWarning = false; + +function openSeparateApplicationWindow(url) { + // resize the Selenium window itself + window.resizeTo(1200, 500); + window.moveTo(window.screenX, 0); + + var appWindow = window.open(url + '?start=true', 'main'); + try { + var windowHeight = 500; + if (window.outerHeight) { + windowHeight = window.outerHeight; + } else if (document.documentElement && document.documentElement.offsetHeight) { + windowHeight = document.documentElement.offsetHeight; + } + + if (window.screenLeft && !window.screenX) window.screenX = window.screenLeft; + if (window.screenTop && !window.screenY) window.screenY = window.screenTop; + + appWindow.resizeTo(1200, screen.availHeight - windowHeight - 60); + appWindow.moveTo(window.screenX, window.screenY + windowHeight + 25); + } catch (e) { + LOG.error("Couldn't resize app window"); + LOG.exception(e); + } + + + if (window.document.readyState == null && !seenReadyStateWarning) { + alert("Beware! Mozilla bug 300992 means that we can't always reliably detect when a new page has loaded. Install the Selenium IDE extension or the readyState extension available from selenium.openqa.org to make page load detection more reliable."); + seenReadyStateWarning = true; + } + + return appWindow; +} + +var URLConfiguration = Class.create(); +Object.extend(URLConfiguration.prototype, { + initialize: function() { + }, + _isQueryParameterTrue: function (name) { + var parameterValue = this._getQueryParameter(name); + if (parameterValue == null) return false; + if (parameterValue.toLowerCase() == "true") return true; + if (parameterValue.toLowerCase() == "on") return true; + return false; + }, + + _getQueryParameter: function(searchKey) { + var str = this.queryString + if (str == null) return null; + var clauses = str.split('&'); + for (var i = 0; i < clauses.length; i++) { + var keyValuePair = clauses[i].split('=', 2); + var key = unescape(keyValuePair[0]); + if (key == searchKey) { + return unescape(keyValuePair[1]); + } + } + return null; + }, + + _extractArgs: function() { + var str = SeleniumHTARunner.commandLine; + if (str == null || str == "") return new Array(); + var matches = str.match(/(?:\"([^\"]+)\"|(?!\"([^\"]+)\")(\S+))/g); + // We either want non quote stuff ([^"]+) surrounded by quotes + // or we want to look-ahead, see that the next character isn't + // a quoted argument, and then grab all the non-space stuff + // this will return for the line: "foo" bar + // the results "\"foo\"" and "bar" + + // So, let's unquote the quoted arguments: + var args = new Array; + for (var i = 0; i < matches.length; i++) { + args[i] = matches[i]; + args[i] = args[i].replace(/^"(.*)"$/, "$1"); + } + return args; + }, + + isMultiWindowMode:function() { + return this._isQueryParameterTrue('multiWindow'); + } +}); + + +function safeScrollIntoView(element) { + if (element.scrollIntoView) { + element.scrollIntoView(false); + return; + } + // TODO: work out how to scroll browsers that don't support + // scrollIntoView (like Konqueror) +} diff --git a/tests/test_tools/selenium/core/scripts/injection.html b/tests/test_tools/selenium/core/scripts/injection.html new file mode 100644 index 00000000..d41fbe69 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/injection.html @@ -0,0 +1,72 @@ +<script language="JavaScript"> + if (window["selenium_has_been_loaded_into_this_window"]==null) + { +__SELENIUM_JS__ + +// Some background on the code below: broadly speaking, where we are relative to other windows +// when running in proxy injection mode depends on whether we are in a frame set file or not. +// +// In regular HTML files, the selenium JavaScript is injected into an iframe called "selenium" +// in order to reduce its impact on the JavaScript environment (through namespace pollution, +// etc.). So in regular HTML files, we need to look at the parent of the current window when we want +// a handle to, e.g., the application window. +// +// In frame set files, we can't use an iframe, so we put the JavaScript in the head element and share +// the window with the frame set. So in this case, we need to look at the current window, not the +// parent when looking for, e.g., the application window. (TODO: Perhaps I should have just +// assigned a regular frame for selenium?) +// +BrowserBot.prototype.getContentWindow = function() { + if (window["seleniumInSameWindow"] != null) return window; + return window.parent; +}; + +BrowserBot.prototype.getTargetWindow = function(windowName) { + if (window["seleniumInSameWindow"] != null) return window; + return window.parent; +}; + +BrowserBot.prototype.getCurrentWindow = function() { + if (window["seleniumInSameWindow"] != null) return window; + return window.parent; +}; + +LOG.openLogWindow = function(message, className) { + // disable for now +}; + +BrowserBot.prototype.relayToRC = function(name) { + var object = eval(name); + var s = 'state:' + serializeObject(name, object) + "\n"; + sendToRC(s); +} + +BrowserBot.prototype.relayBotToRC = function(s) { + this.relayToRC("selenium." + s); +} + +function selenium_frameRunTest(oldOnLoadRoutine) { + if (oldOnLoadRoutine) { + eval(oldOnLoadRoutine); + } + runSeleniumTest(); +} + +function seleniumOnLoad() { + injectedSessionId = @SESSION_ID@; + window["selenium_has_been_loaded_into_this_window"] = true; + runSeleniumTest(); +} + +if (window.addEventListener) { + window.addEventListener("load", seleniumOnLoad, false); // firefox +} else if (window.attachEvent){ + window.attachEvent("onload", seleniumOnLoad); // IE +} +else { + throw "causing a JavaScript error to tell the world that I did not arrange to be run on load"; +} + +injectedSessionId = @SESSION_ID@; +} +</script> diff --git a/tests/test_tools/selenium/core/scripts/injection_iframe.html b/tests/test_tools/selenium/core/scripts/injection_iframe.html new file mode 100644 index 00000000..bc26e859 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/injection_iframe.html @@ -0,0 +1,7 @@ +<script language="JavaScript"> + // Ideally I would avoid polluting the namespace by enclosing this snippet with + // curly braces, but I want to make it easy to look at what URL I used for anyone + // who is interested in looking into http://jira.openqa.org/browse/SRC-101: + var _sel_url_ = "http://" + location.host + "/selenium-server/core/scripts/injection.html"; + document.write('<iframe name="selenium" width=0 height=0 id="selenium" src="' + _sel_url_ + '"></iframe>'); +</script> diff --git a/tests/test_tools/selenium/core/scripts/js2html.js b/tests/test_tools/selenium/core/scripts/js2html.js new file mode 100644 index 00000000..a384dce3 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/js2html.js @@ -0,0 +1,70 @@ +/*
+
+This is an experiment in using the Narcissus JavaScript engine
+to allow Selenium scripts to be written in plain JavaScript.
+
+The 'jsparse' function will compile each high level block into a Selenium table script.
+
+
+TODO:
+1) Test! (More browsers, more sample scripts)
+2) Stepping and walking lower levels of the parse tree
+3) Calling Selenium commands directly from JavaScript
+4) Do we want comments to appear in the TestRunner?
+5) Fix context so variables don't have to be global
+ For now, variables defined with "var" won't be found
+ if used later on in a script.
+6) Fix formatting
+*/
+
+
+function jsparse() {
+ var script = document.getElementById('sejs')
+ var fname = 'javascript script';
+ parse_result = parse(script.text, fname, 0);
+
+ var x2 = new ExecutionContext(GLOBAL_CODE);
+ ExecutionContext.current = x2;
+
+
+ var new_test_source = '';
+ var new_line = '';
+
+ for (i=0;i<parse_result.$length;i++){
+ var the_start = parse_result[i].start;
+ var the_end;
+ if ( i == (parse_result.$length-1)) {
+ the_end = parse_result.tokenizer.source.length;
+ } else {
+ the_end = parse_result[i+1].start;
+ }
+
+ var script_fragment = parse_result.tokenizer.source.slice(the_start,the_end)
+
+ new_line = '<tr><td style="display:none;" class="js">getEval</td>' +
+ '<td style="display:none;">currentTest.doNextCommand()</td>' +
+ '<td style="white-space: pre;">' + script_fragment + '</td>' +
+ '<td></td></tr>\n';
+ new_test_source += new_line;
+ //eval(script_fragment);
+
+
+ };
+
+
+
+ execute(parse_result,x2)
+
+ // Create HTML Table
+ body = document.body
+ body.innerHTML += "<table class='selenium' id='se-js-table'>"+
+ "<tbody>" +
+ "<tr><td>// " + document.title + "</td></tr>" +
+ new_test_source +
+ "</tbody" +
+ "</table>";
+
+ //body.innerHTML = "<pre>" + parse_result + "</pre>"
+}
+
+
diff --git a/tests/test_tools/selenium/core/scripts/narcissus-defs.js b/tests/test_tools/selenium/core/scripts/narcissus-defs.js new file mode 100644 index 00000000..5869397d --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/narcissus-defs.js @@ -0,0 +1,175 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is the Narcissus JavaScript engine. + * + * The Initial Developer of the Original Code is + * Brendan Eich <brendan@mozilla.org>. + * Portions created by the Initial Developer are Copyright (C) 2004 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/* + * Narcissus - JS implemented in JS. + * + * Well-known constants and lookup tables. Many consts are generated from the + * tokens table via eval to minimize redundancy, so consumers must be compiled + * separately to take advantage of the simple switch-case constant propagation + * done by SpiderMonkey. + */ + +// jrh +//module('JS.Defs'); + +GLOBAL = this; + +var tokens = [ + // End of source. + "END", + + // Operators and punctuators. Some pair-wise order matters, e.g. (+, -) + // and (UNARY_PLUS, UNARY_MINUS). + "\n", ";", + ",", + "=", + "?", ":", "CONDITIONAL", + "||", + "&&", + "|", + "^", + "&", + "==", "!=", "===", "!==", + "<", "<=", ">=", ">", + "<<", ">>", ">>>", + "+", "-", + "*", "/", "%", + "!", "~", "UNARY_PLUS", "UNARY_MINUS", + "++", "--", + ".", + "[", "]", + "{", "}", + "(", ")", + + // Nonterminal tree node type codes. + "SCRIPT", "BLOCK", "LABEL", "FOR_IN", "CALL", "NEW_WITH_ARGS", "INDEX", + "ARRAY_INIT", "OBJECT_INIT", "PROPERTY_INIT", "GETTER", "SETTER", + "GROUP", "LIST", + + // Terminals. + "IDENTIFIER", "NUMBER", "STRING", "REGEXP", + + // Keywords. + "break", + "case", "catch", "const", "continue", + "debugger", "default", "delete", "do", + "else", "enum", + "false", "finally", "for", "function", + "if", "in", "instanceof", + "new", "null", + "return", + "switch", + "this", "throw", "true", "try", "typeof", + "var", "void", + "while", "with", + // Extensions + "require", "bless", "mixin", "import" +]; + +// Operator and punctuator mapping from token to tree node type name. +// NB: superstring tokens (e.g., ++) must come before their substring token +// counterparts (+ in the example), so that the opRegExp regular expression +// synthesized from this list makes the longest possible match. +var opTypeNames = { + '\n': "NEWLINE", + ';': "SEMICOLON", + ',': "COMMA", + '?': "HOOK", + ':': "COLON", + '||': "OR", + '&&': "AND", + '|': "BITWISE_OR", + '^': "BITWISE_XOR", + '&': "BITWISE_AND", + '===': "STRICT_EQ", + '==': "EQ", + '=': "ASSIGN", + '!==': "STRICT_NE", + '!=': "NE", + '<<': "LSH", + '<=': "LE", + '<': "LT", + '>>>': "URSH", + '>>': "RSH", + '>=': "GE", + '>': "GT", + '++': "INCREMENT", + '--': "DECREMENT", + '+': "PLUS", + '-': "MINUS", + '*': "MUL", + '/': "DIV", + '%': "MOD", + '!': "NOT", + '~': "BITWISE_NOT", + '.': "DOT", + '[': "LEFT_BRACKET", + ']': "RIGHT_BRACKET", + '{': "LEFT_CURLY", + '}': "RIGHT_CURLY", + '(': "LEFT_PAREN", + ')': "RIGHT_PAREN" +}; + +// Hash of keyword identifier to tokens index. NB: we must null __proto__ to +// avoid toString, etc. namespace pollution. +var keywords = {__proto__: null}; + +// Define const END, etc., based on the token names. Also map name to index. +var consts = " "; +for (var i = 0, j = tokens.length; i < j; i++) { + if (i > 0) + consts += "; "; + var t = tokens[i]; + if (/^[a-z]/.test(t)) { + consts += t.toUpperCase(); + keywords[t] = i; + } else { + consts += (/^\W/.test(t) ? opTypeNames[t] : t); + } + consts += " = " + i; + tokens[t] = i; +} +eval(consts + ";"); + +// Map assignment operators to their indexes in the tokens array. +var assignOps = ['|', '^', '&', '<<', '>>', '>>>', '+', '-', '*', '/', '%']; + +for (i = 0, j = assignOps.length; i < j; i++) { + t = assignOps[i]; + assignOps[t] = tokens[t]; +} diff --git a/tests/test_tools/selenium/core/scripts/narcissus-exec.js b/tests/test_tools/selenium/core/scripts/narcissus-exec.js new file mode 100644 index 00000000..e2c88f81 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/narcissus-exec.js @@ -0,0 +1,1054 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * vim: set ts=4 sw=4 et tw=80: + * + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is the Narcissus JavaScript engine. + * + * The Initial Developer of the Original Code is + * Brendan Eich <brendan@mozilla.org>. + * Portions created by the Initial Developer are Copyright (C) 2004 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/* + * Narcissus - JS implemented in JS. + * + * Execution of parse trees. + * + * Standard classes except for eval, Function, Array, and String are borrowed + * from the host JS environment. Function is metacircular. Array and String + * are reflected via wrapping the corresponding native constructor and adding + * an extra level of prototype-based delegation. + */ + +// jrh +//module('JS.Exec'); +// end jrh + +GLOBAL_CODE = 0; EVAL_CODE = 1; FUNCTION_CODE = 2; + +function ExecutionContext(type) { + this.type = type; +} + +// jrh +var agenda = new Array(); +var skip_setup = 0; +// end jrh + +var global = { + // Value properties. + NaN: NaN, Infinity: Infinity, undefined: undefined, + alert : function(msg) { alert(msg) }, + confirm : function(msg) { return confirm(msg) }, + document : document, + window : window, + // jrh + //debug: window.open('','debugwindow','width=600,height=400,scrollbars=yes,resizable=yes'), + // end jrh + navigator : navigator, + XMLHttpRequest : function() { return new XMLHttpRequest() }, + // Function properties. + eval: function(s) { + if (typeof s != "string") { + return s; + } + + var x = ExecutionContext.current; + var x2 = new ExecutionContext(EVAL_CODE); + x2.thisObject = x.thisObject; + x2.caller = x.caller; + x2.callee = x.callee; + x2.scope = x.scope; + ExecutionContext.current = x2; + try { + execute(parse(s), x2); + } catch (e) { + x.result = x2.result; + throw e; + } finally { + ExecutionContext.current = x; + } + return x2.result; + }, + parseInt: parseInt, parseFloat: parseFloat, + isNaN: isNaN, isFinite: isFinite, + decodeURI: decodeURI, encodeURI: encodeURI, + decodeURIComponent: decodeURIComponent, + encodeURIComponent: encodeURIComponent, + + // Class constructors. Where ECMA-262 requires C.length == 1, we declare + // a dummy formal parameter. + Object: Object, + Function: function(dummy) { + var p = "", b = "", n = arguments.length; + if (n) { + var m = n - 1; + if (m) { + p += arguments[0]; + for (var k = 1; k < m; k++) + p += "," + arguments[k]; + } + b += arguments[m]; + } + + // XXX We want to pass a good file and line to the tokenizer. + // Note the anonymous name to maintain parity with Spidermonkey. + var t = new Tokenizer("anonymous(" + p + ") {" + b + "}"); + + // NB: Use the STATEMENT_FORM constant since we don't want to push this + // function onto the null compilation context. + var f = FunctionDefinition(t, null, false, STATEMENT_FORM); + var s = {object: global, parent: null}; + return new FunctionObject(f, s); + }, + Array: function(dummy) { + // Array when called as a function acts as a constructor. + return GLOBAL.Array.apply(this, arguments); + }, + String: function(s) { + // Called as function or constructor: convert argument to string type. + s = arguments.length ? "" + s : ""; + if (this instanceof String) { + // Called as constructor: save the argument as the string value + // of this String object and return this object. + this.value = s; + return this; + } + return s; + }, + Boolean: Boolean, Number: Number, Date: Date, RegExp: RegExp, + Error: Error, EvalError: EvalError, RangeError: RangeError, + ReferenceError: ReferenceError, SyntaxError: SyntaxError, + TypeError: TypeError, URIError: URIError, + + // Other properties. + Math: Math, + + // Extensions to ECMA. + //snarf: snarf, + evaluate: evaluate, + load: function(s) { + if (typeof s != "string") + return s; + var req = new XMLHttpRequest(); + req.open('GET', s, false); + req.send(null); + + evaluate(req.responseText, s, 1) + }, + print: print, version: null +}; + +// jrh +//global.debug.document.body.innerHTML = '' +// end jrh + +// Helper to avoid Object.prototype.hasOwnProperty polluting scope objects. +function hasDirectProperty(o, p) { + return Object.prototype.hasOwnProperty.call(o, p); +} + +// Reflect a host class into the target global environment by delegation. +function reflectClass(name, proto) { + var gctor = global[name]; + gctor.prototype = proto; + proto.constructor = gctor; + return proto; +} + +// Reflect Array -- note that all Array methods are generic. +reflectClass('Array', new Array); + +// Reflect String, overriding non-generic methods. +var gSp = reflectClass('String', new String); +gSp.toSource = function () { return this.value.toSource(); }; +gSp.toString = function () { return this.value; }; +gSp.valueOf = function () { return this.value; }; +global.String.fromCharCode = String.fromCharCode; + +var XCp = ExecutionContext.prototype; +ExecutionContext.current = XCp.caller = XCp.callee = null; +XCp.scope = {object: global, parent: null}; +XCp.thisObject = global; +XCp.result = undefined; +XCp.target = null; +XCp.ecmaStrictMode = false; + +function Reference(base, propertyName, node) { + this.base = base; + this.propertyName = propertyName; + this.node = node; +} + +Reference.prototype.toString = function () { return this.node.getSource(); } + +function getValue(v) { + if (v instanceof Reference) { + if (!v.base) { + throw new ReferenceError(v.propertyName + " is not defined", + v.node.filename(), v.node.lineno); + } + return v.base[v.propertyName]; + } + return v; +} + +function putValue(v, w, vn) { + if (v instanceof Reference) + return (v.base || global)[v.propertyName] = w; + throw new ReferenceError("Invalid assignment left-hand side", + vn.filename(), vn.lineno); +} + +function isPrimitive(v) { + var t = typeof v; + return (t == "object") ? v === null : t != "function"; +} + +function isObject(v) { + var t = typeof v; + return (t == "object") ? v !== null : t == "function"; +} + +// If r instanceof Reference, v == getValue(r); else v === r. If passed, rn +// is the node whose execute result was r. +function toObject(v, r, rn) { + switch (typeof v) { + case "boolean": + return new global.Boolean(v); + case "number": + return new global.Number(v); + case "string": + return new global.String(v); + case "function": + return v; + case "object": + if (v !== null) + return v; + } + var message = r + " (type " + (typeof v) + ") has no properties"; + throw rn ? new TypeError(message, rn.filename(), rn.lineno) + : new TypeError(message); +} + +function execute(n, x) { + if (!this.new_block) + new_block = new Array(); + //alert (n) + var a, f, i, j, r, s, t, u, v; + switch (n.type) { + case FUNCTION: + if (n.functionForm != DECLARED_FORM) { + if (!n.name || n.functionForm == STATEMENT_FORM) { + v = new FunctionObject(n, x.scope); + if (n.functionForm == STATEMENT_FORM) + x.scope.object[n.name] = v; + } else { + t = new Object; + x.scope = {object: t, parent: x.scope}; + try { + v = new FunctionObject(n, x.scope); + t[n.name] = v; + } finally { + x.scope = x.scope.parent; + } + } + } + break; + + case SCRIPT: + t = x.scope.object; + a = n.funDecls; + for (i = 0, j = a.length; i < j; i++) { + s = a[i].name; + f = new FunctionObject(a[i], x.scope); + t[s] = f; + } + a = n.varDecls; + for (i = 0, j = a.length; i < j; i++) { + u = a[i]; + s = u.name; + if (u.readOnly && hasDirectProperty(t, s)) { + throw new TypeError("Redeclaration of const " + s, + u.filename(), u.lineno); + } + if (u.readOnly || !hasDirectProperty(t, s)) { + t[s] = null; + } + } + // FALL THROUGH + + case BLOCK: + for (i = 0, j = n.$length; i < j; i++) { + //jrh + //execute(n[i], x); + //new_block.unshift([n[i], x]); + new_block.push([n[i], x]); + } + new_block.reverse(); + agenda = agenda.concat(new_block); + //agenda = new_block.concat(agenda) + // end jrh + break; + + case IF: + if (getValue(execute(n.condition, x))) + execute(n.thenPart, x); + else if (n.elsePart) + execute(n.elsePart, x); + break; + + case SWITCH: + s = getValue(execute(n.discriminant, x)); + a = n.cases; + var matchDefault = false; + switch_loop: + for (i = 0, j = a.length; ; i++) { + if (i == j) { + if (n.defaultIndex >= 0) { + i = n.defaultIndex - 1; // no case matched, do default + matchDefault = true; + continue; + } + break; // no default, exit switch_loop + } + t = a[i]; // next case (might be default!) + if (t.type == CASE) { + u = getValue(execute(t.caseLabel, x)); + } else { + if (!matchDefault) // not defaulting, skip for now + continue; + u = s; // force match to do default + } + if (u === s) { + for (;;) { // this loop exits switch_loop + if (t.statements.length) { + try { + execute(t.statements, x); + } catch (e) { + if (!(e == BREAK && x.target == n)) { throw e } + break switch_loop; + } + } + if (++i == j) + break switch_loop; + t = a[i]; + } + // NOT REACHED + } + } + break; + + case FOR: + // jrh + // added "skip_setup" so initialization doesn't get called + // on every call.. + if (!skip_setup) + n.setup && getValue(execute(n.setup, x)); + // FALL THROUGH + case WHILE: + // jrh + //while (!n.condition || getValue(execute(n.condition, x))) { + if (!n.condition || getValue(execute(n.condition, x))) { + try { + // jrh + //execute(n.body, x); + new_block.push([n.body, x]); + agenda.push([n.body, x]) + //agenda.unshift([n.body, x]) + // end jrh + } catch (e) { + if (e == BREAK && x.target == n) { + break; + } else if (e == CONTINUE && x.target == n) { + // jrh + // 'continue' is invalid inside an 'if' clause + // I don't know what commenting this out will break! + //continue; + // end jrh + + } else { + throw e; + } + } + n.update && getValue(execute(n.update, x)); + // jrh + new_block.unshift([n, x]) + agenda.splice(agenda.length-1,0,[n, x]) + //agenda.splice(1,0,[n, x]) + skip_setup = 1 + // end jrh + } else { + skip_setup = 0 + } + + break; + + case FOR_IN: + u = n.varDecl; + if (u) + execute(u, x); + r = n.iterator; + s = execute(n.object, x); + v = getValue(s); + + // ECMA deviation to track extant browser JS implementation behavior. + t = (v == null && !x.ecmaStrictMode) ? v : toObject(v, s, n.object); + a = []; + for (i in t) + a.push(i); + for (i = 0, j = a.length; i < j; i++) { + putValue(execute(r, x), a[i], r); + try { + execute(n.body, x); + } catch (e) { + if (e == BREAK && x.target == n) { + break; + } else if (e == CONTINUE && x.target == n) { + continue; + } else { + throw e; + } + } + } + break; + + case DO: + do { + try { + execute(n.body, x); + } catch (e) { + if (e == BREAK && x.target == n) { + break; + } else if (e == CONTINUE && x.target == n) { + continue; + } else { + throw e; + } + } + } while (getValue(execute(n.condition, x))); + break; + + case BREAK: + case CONTINUE: + x.target = n.target; + throw n.type; + + case TRY: + try { + execute(n.tryBlock, x); + } catch (e) { + if (!(e == THROW && (j = n.catchClauses.length))) { + throw e; + } + e = x.result; + x.result = undefined; + for (i = 0; ; i++) { + if (i == j) { + x.result = e; + throw THROW; + } + t = n.catchClauses[i]; + x.scope = {object: {}, parent: x.scope}; + x.scope.object[t.varName] = e; + try { + if (t.guard && !getValue(execute(t.guard, x))) + continue; + execute(t.block, x); + break; + } finally { + x.scope = x.scope.parent; + } + } + } finally { + if (n.finallyBlock) + execute(n.finallyBlock, x); + } + break; + + case THROW: + x.result = getValue(execute(n.exception, x)); + throw THROW; + + case RETURN: + x.result = getValue(execute(n.value, x)); + throw RETURN; + + case WITH: + r = execute(n.object, x); + t = toObject(getValue(r), r, n.object); + x.scope = {object: t, parent: x.scope}; + try { + execute(n.body, x); + } finally { + x.scope = x.scope.parent; + } + break; + + case VAR: + case CONST: + for (i = 0, j = n.$length; i < j; i++) { + u = n[i].initializer; + if (!u) + continue; + t = n[i].name; + for (s = x.scope; s; s = s.parent) { + if (hasDirectProperty(s.object, t)) + break; + } + u = getValue(execute(u, x)); + if (n.type == CONST) + s.object[t] = u; + else + s.object[t] = u; + } + break; + + case DEBUGGER: + throw "NYI: " + tokens[n.type]; + + case REQUIRE: + var req = new XMLHttpRequest(); + req.open('GET', n.filename, 'false'); + + case SEMICOLON: + if (n.expression) + // print debugging statements + + var the_start = n.start + var the_end = n.end + var the_statement = parse_result.tokenizer.source.slice(the_start,the_end) + //global.debug.document.body.innerHTML += ('<pre>>>> <b>' + the_statement + '</b></pre>') + LOG.info('>>>' + the_statement) + x.result = getValue(execute(n.expression, x)); + //if (x.result) + //global.debug.document.body.innerHTML += ( '<pre>>>> ' + x.result + '</pre>') + + break; + + case LABEL: + try { + execute(n.statement, x); + } catch (e) { + if (!(e == BREAK && x.target == n)) { throw e } + } + break; + + case COMMA: + for (i = 0, j = n.$length; i < j; i++) + v = getValue(execute(n[i], x)); + break; + + case ASSIGN: + r = execute(n[0], x); + t = n[0].assignOp; + if (t) + u = getValue(r); + v = getValue(execute(n[1], x)); + if (t) { + switch (t) { + case BITWISE_OR: v = u | v; break; + case BITWISE_XOR: v = u ^ v; break; + case BITWISE_AND: v = u & v; break; + case LSH: v = u << v; break; + case RSH: v = u >> v; break; + case URSH: v = u >>> v; break; + case PLUS: v = u + v; break; + case MINUS: v = u - v; break; + case MUL: v = u * v; break; + case DIV: v = u / v; break; + case MOD: v = u % v; break; + } + } + putValue(r, v, n[0]); + break; + + case CONDITIONAL: + v = getValue(execute(n[0], x)) ? getValue(execute(n[1], x)) + : getValue(execute(n[2], x)); + break; + + case OR: + v = getValue(execute(n[0], x)) || getValue(execute(n[1], x)); + break; + + case AND: + v = getValue(execute(n[0], x)) && getValue(execute(n[1], x)); + break; + + case BITWISE_OR: + v = getValue(execute(n[0], x)) | getValue(execute(n[1], x)); + break; + + case BITWISE_XOR: + v = getValue(execute(n[0], x)) ^ getValue(execute(n[1], x)); + break; + + case BITWISE_AND: + v = getValue(execute(n[0], x)) & getValue(execute(n[1], x)); + break; + + case EQ: + v = getValue(execute(n[0], x)) == getValue(execute(n[1], x)); + break; + + case NE: + v = getValue(execute(n[0], x)) != getValue(execute(n[1], x)); + break; + + case STRICT_EQ: + v = getValue(execute(n[0], x)) === getValue(execute(n[1], x)); + break; + + case STRICT_NE: + v = getValue(execute(n[0], x)) !== getValue(execute(n[1], x)); + break; + + case LT: + v = getValue(execute(n[0], x)) < getValue(execute(n[1], x)); + break; + + case LE: + v = getValue(execute(n[0], x)) <= getValue(execute(n[1], x)); + break; + + case GE: + v = getValue(execute(n[0], x)) >= getValue(execute(n[1], x)); + break; + + case GT: + v = getValue(execute(n[0], x)) > getValue(execute(n[1], x)); + break; + + case IN: + v = getValue(execute(n[0], x)) in getValue(execute(n[1], x)); + break; + + case INSTANCEOF: + t = getValue(execute(n[0], x)); + u = getValue(execute(n[1], x)); + if (isObject(u) && typeof u.__hasInstance__ == "function") + v = u.__hasInstance__(t); + else + v = t instanceof u; + break; + + case LSH: + v = getValue(execute(n[0], x)) << getValue(execute(n[1], x)); + break; + + case RSH: + v = getValue(execute(n[0], x)) >> getValue(execute(n[1], x)); + break; + + case URSH: + v = getValue(execute(n[0], x)) >>> getValue(execute(n[1], x)); + break; + + case PLUS: + v = getValue(execute(n[0], x)) + getValue(execute(n[1], x)); + break; + + case MINUS: + v = getValue(execute(n[0], x)) - getValue(execute(n[1], x)); + break; + + case MUL: + v = getValue(execute(n[0], x)) * getValue(execute(n[1], x)); + break; + + case DIV: + v = getValue(execute(n[0], x)) / getValue(execute(n[1], x)); + break; + + case MOD: + v = getValue(execute(n[0], x)) % getValue(execute(n[1], x)); + break; + + case DELETE: + t = execute(n[0], x); + v = !(t instanceof Reference) || delete t.base[t.propertyName]; + break; + + case VOID: + getValue(execute(n[0], x)); + break; + + case TYPEOF: + t = execute(n[0], x); + if (t instanceof Reference) + t = t.base ? t.base[t.propertyName] : undefined; + v = typeof t; + break; + + case NOT: + v = !getValue(execute(n[0], x)); + break; + + case BITWISE_NOT: + v = ~getValue(execute(n[0], x)); + break; + + case UNARY_PLUS: + v = +getValue(execute(n[0], x)); + break; + + case UNARY_MINUS: + v = -getValue(execute(n[0], x)); + break; + + case INCREMENT: + case DECREMENT: + t = execute(n[0], x); + u = Number(getValue(t)); + if (n.postfix) + v = u; + putValue(t, (n.type == INCREMENT) ? ++u : --u, n[0]); + if (!n.postfix) + v = u; + break; + + case DOT: + r = execute(n[0], x); + t = getValue(r); + u = n[1].value; + v = new Reference(toObject(t, r, n[0]), u, n); + break; + + case INDEX: + r = execute(n[0], x); + t = getValue(r); + u = getValue(execute(n[1], x)); + v = new Reference(toObject(t, r, n[0]), String(u), n); + break; + + case LIST: + // Curse ECMA for specifying that arguments is not an Array object! + v = {}; + for (i = 0, j = n.$length; i < j; i++) { + u = getValue(execute(n[i], x)); + v[i] = u; + } + v.length = i; + break; + + case CALL: + r = execute(n[0], x); + a = execute(n[1], x); + f = getValue(r); + if (isPrimitive(f) || typeof f.__call__ != "function") { + throw new TypeError(r + " is not callable", + n[0].filename(), n[0].lineno); + } + t = (r instanceof Reference) ? r.base : null; + if (t instanceof Activation) + t = null; + v = f.__call__(t, a, x); + break; + + case NEW: + case NEW_WITH_ARGS: + r = execute(n[0], x); + f = getValue(r); + if (n.type == NEW) { + a = {}; + a.length = 0; + } else { + a = execute(n[1], x); + } + if (isPrimitive(f) || typeof f.__construct__ != "function") { + throw new TypeError(r + " is not a constructor", + n[0].filename(), n[0].lineno); + } + v = f.__construct__(a, x); + break; + + case ARRAY_INIT: + v = []; + for (i = 0, j = n.$length; i < j; i++) { + if (n[i]) + v[i] = getValue(execute(n[i], x)); + } + v.length = j; + break; + + case OBJECT_INIT: + v = {}; + for (i = 0, j = n.$length; i < j; i++) { + t = n[i]; + if (t.type == PROPERTY_INIT) { + v[t[0].value] = getValue(execute(t[1], x)); + } else { + f = new FunctionObject(t, x.scope); + /* + u = (t.type == GETTER) ? '__defineGetter__' + : '__defineSetter__'; + v[u](t.name, thunk(f, x)); + */ + } + } + break; + + case NULL: + v = null; + break; + + case THIS: + v = x.thisObject; + break; + + case TRUE: + v = true; + break; + + case FALSE: + v = false; + break; + + case IDENTIFIER: + for (s = x.scope; s; s = s.parent) { + if (n.value in s.object) + break; + } + v = new Reference(s && s.object, n.value, n); + break; + + case NUMBER: + case STRING: + case REGEXP: + v = n.value; + break; + + case GROUP: + v = execute(n[0], x); + break; + + default: + throw "PANIC: unknown operation " + n.type + ": " + uneval(n); + } + return v; +} + +function Activation(f, a) { + for (var i = 0, j = f.params.length; i < j; i++) + this[f.params[i]] = a[i]; + this.arguments = a; +} + +// Null Activation.prototype's proto slot so that Object.prototype.* does not +// pollute the scope of heavyweight functions. Also delete its 'constructor' +// property so that it doesn't pollute function scopes. + +Activation.prototype.__proto__ = null; +delete Activation.prototype.constructor; + +function FunctionObject(node, scope) { + this.node = node; + this.scope = scope; + this.length = node.params.length; + var proto = {}; + this.prototype = proto; + proto.constructor = this; +} + +var FOp = FunctionObject.prototype = { + // Internal methods. + __call__: function (t, a, x) { + var x2 = new ExecutionContext(FUNCTION_CODE); + x2.thisObject = t || global; + x2.caller = x; + x2.callee = this; + a.callee = this; + var f = this.node; + x2.scope = {object: new Activation(f, a), parent: this.scope}; + + ExecutionContext.current = x2; + try { + execute(f.body, x2); + } catch (e) { + if (!(e == RETURN)) { throw e } else if (e == RETURN) { + return x2.result; + } + if (e != THROW) { throw e } + x.result = x2.result; + throw THROW; + } finally { + ExecutionContext.current = x; + } + return undefined; + }, + + __construct__: function (a, x) { + var o = new Object; + var p = this.prototype; + if (isObject(p)) + o.__proto__ = p; + // else o.__proto__ defaulted to Object.prototype + + var v = this.__call__(o, a, x); + if (isObject(v)) + return v; + return o; + }, + + __hasInstance__: function (v) { + if (isPrimitive(v)) + return false; + var p = this.prototype; + if (isPrimitive(p)) { + throw new TypeError("'prototype' property is not an object", + this.node.filename(), this.node.lineno); + } + var o; + while ((o = v.__proto__)) { + if (o == p) + return true; + v = o; + } + return false; + }, + + // Standard methods. + toString: function () { + return this.node.getSource(); + }, + + apply: function (t, a) { + // Curse ECMA again! + if (typeof this.__call__ != "function") { + throw new TypeError("Function.prototype.apply called on" + + " uncallable object"); + } + + if (t === undefined || t === null) + t = global; + else if (typeof t != "object") + t = toObject(t, t); + + if (a === undefined || a === null) { + a = {}; + a.length = 0; + } else if (a instanceof Array) { + var v = {}; + for (var i = 0, j = a.length; i < j; i++) + v[i] = a[i]; + v.length = i; + a = v; + } else if (!(a instanceof Object)) { + // XXX check for a non-arguments object + throw new TypeError("Second argument to Function.prototype.apply" + + " must be an array or arguments object", + this.node.filename(), this.node.lineno); + } + + return this.__call__(t, a, ExecutionContext.current); + }, + + call: function (t) { + // Curse ECMA a third time! + var a = Array.prototype.splice.call(arguments, 1); + return this.apply(t, a); + } +}; + +// Connect Function.prototype and Function.prototype.constructor in global. +reflectClass('Function', FOp); + +// Help native and host-scripted functions be like FunctionObjects. +var Fp = Function.prototype; +var REp = RegExp.prototype; + +if (!('__call__' in Fp)) { + Fp.__call__ = function (t, a, x) { + // Curse ECMA yet again! + a = Array.prototype.splice.call(a, 0, a.length); + return this.apply(t, a); + }; + + REp.__call__ = function (t, a, x) { + a = Array.prototype.splice.call(a, 0, a.length); + return this.exec.apply(this, a); + }; + + Fp.__construct__ = function (a, x) { + switch (a.length) { + case 0: + return new this(); + case 1: + return new this(a[0]); + case 2: + return new this(a[0], a[1]); + case 3: + return new this(a[0], a[1], a[2]); + case 4: + return new this(a[0], a[1], a[2], a[3]); + case 5: + return new this(a[0], a[1], a[2], a[3], a[4]); + case 6: + return new this(a[0], a[1], a[2], a[3], a[4], a[5]); + case 7: + return new this(a[0], a[1], a[2], a[3], a[4], a[5], a[6]); + } + throw "PANIC: too many arguments to constructor"; + } + + // Since we use native functions such as Date along with host ones such + // as global.eval, we want both to be considered instances of the native + // Function constructor. + Fp.__hasInstance__ = function (v) { + return v instanceof Function || v instanceof global.Function; + }; +} + +function thunk(f, x) { + return function () { return f.__call__(this, arguments, x); }; +} + +function evaluate(s, f, l) { + if (typeof s != "string") + return s; + + var x = ExecutionContext.current; + var x2 = new ExecutionContext(GLOBAL_CODE); + ExecutionContext.current = x2; + try { + execute(parse(s, f, l), x2); + } catch (e) { + if (e != THROW) { throw e } + if (x) { + x.result = x2.result; + throw(THROW); + } + throw x2.result; + } finally { + ExecutionContext.current = x; + } + return x2.result; +} diff --git a/tests/test_tools/selenium/core/scripts/narcissus-parse.js b/tests/test_tools/selenium/core/scripts/narcissus-parse.js new file mode 100644 index 00000000..d6acb836 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/narcissus-parse.js @@ -0,0 +1,1003 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is the Narcissus JavaScript engine. + * + * The Initial Developer of the Original Code is + * Brendan Eich <brendan@mozilla.org>. + * Portions created by the Initial Developer are Copyright (C) 2004 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): Richard Hundt <www.plextk.org> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/* + * Narcissus - JS implemented in JS. + * + * Lexical scanner and parser. + */ + +// jrh +//module('JS.Parse'); + +// Build a regexp that recognizes operators and punctuators (except newline). +var opRegExp = +/^;|^,|^\?|^:|^\|\||^\&\&|^\||^\^|^\&|^===|^==|^=|^!==|^!=|^<<|^<=|^<|^>>>|^>>|^>=|^>|^\+\+|^\-\-|^\+|^\-|^\*|^\/|^%|^!|^~|^\.|^\[|^\]|^\{|^\}|^\(|^\)/; + +// A regexp to match floating point literals (but not integer literals). +var fpRegExp = /^\d+\.\d*(?:[eE][-+]?\d+)?|^\d+(?:\.\d*)?[eE][-+]?\d+|^\.\d+(?:[eE][-+]?\d+)?/; + +function Tokenizer(s, f, l) { + this.cursor = 0; + this.source = String(s); + this.tokens = []; + this.tokenIndex = 0; + this.lookahead = 0; + this.scanNewlines = false; + this.scanOperand = true; + this.filename = f || ""; + this.lineno = l || 1; +} + +Tokenizer.prototype = { + input : function() { + return this.source.substring(this.cursor); + }, + + done : function() { + return this.peek() == END; + }, + + token : function() { + return this.tokens[this.tokenIndex]; + }, + + match: function (tt) { + return this.get() == tt || this.unget(); + }, + + mustMatch: function (tt) { + if (!this.match(tt)) + throw this.newSyntaxError("Missing " + this.tokens[tt].toLowerCase()); + return this.token(); + }, + + peek: function () { + var tt; + if (this.lookahead) { + tt = this.tokens[(this.tokenIndex + this.lookahead) & 3].type; + } else { + tt = this.get(); + this.unget(); + } + return tt; + }, + + peekOnSameLine: function () { + this.scanNewlines = true; + var tt = this.peek(); + this.scanNewlines = false; + return tt; + }, + + get: function () { + var token; + while (this.lookahead) { + --this.lookahead; + this.tokenIndex = (this.tokenIndex + 1) & 3; + token = this.tokens[this.tokenIndex]; + if (token.type != NEWLINE || this.scanNewlines) + return token.type; + } + + for (;;) { + var input = this.input(); + var rx = this.scanNewlines ? /^[ \t]+/ : /^\s+/; + var match = input.match(rx); + if (match) { + var spaces = match[0]; + this.cursor += spaces.length; + var newlines = spaces.match(/\n/g); + if (newlines) + this.lineno += newlines.length; + input = this.input(); + } + + if (!(match = input.match(/^\/(?:\*(?:.|\n)*?\*\/|\/.*)/))) + break; + var comment = match[0]; + this.cursor += comment.length; + newlines = comment.match(/\n/g); + if (newlines) + this.lineno += newlines.length + } + + this.tokenIndex = (this.tokenIndex + 1) & 3; + token = this.tokens[this.tokenIndex]; + if (!token) + this.tokens[this.tokenIndex] = token = {}; + if (!input) + return token.type = END; + if ((match = input.match(fpRegExp))) { + token.type = NUMBER; + token.value = parseFloat(match[0]); + } else if ((match = input.match(/^0[xX][\da-fA-F]+|^0[0-7]*|^\d+/))) { + token.type = NUMBER; + token.value = parseInt(match[0]); + } else if ((match = input.match(/^((\$\w*)|(\w+))/))) { + var id = match[0]; + token.type = keywords[id] || IDENTIFIER; + token.value = id; + } else if ((match = input.match(/^"(?:\\.|[^"])*"|^'(?:[^']|\\.)*'/))) { + token.type = STRING; + token.value = eval(match[0]); + } else if (this.scanOperand && + (match = input.match(/^\/((?:\\.|[^\/])+)\/([gi]*)/))) { + token.type = REGEXP; + token.value = new RegExp(match[1], match[2]); + } else if ((match = input.match(opRegExp))) { + var op = match[0]; + if (assignOps[op] && input[op.length] == '=') { + token.type = ASSIGN; + token.assignOp = GLOBAL[opTypeNames[op]]; + match[0] += '='; + } else { + token.type = GLOBAL[opTypeNames[op]]; + if (this.scanOperand && + (token.type == PLUS || token.type == MINUS)) { + token.type += UNARY_PLUS - PLUS; + } + token.assignOp = null; + } + //debug('token.value => '+op+', token.type => '+token.type); + token.value = op; + } else { + throw this.newSyntaxError("Illegal token"); + } + + token.start = this.cursor; + this.cursor += match[0].length; + token.end = this.cursor; + token.lineno = this.lineno; + return token.type; + }, + + unget: function () { + if (++this.lookahead == 4) throw "PANIC: too much lookahead!"; + this.tokenIndex = (this.tokenIndex - 1) & 3; + }, + + newSyntaxError: function (m) { + var e = new SyntaxError(m, this.filename, this.lineno); + e.source = this.source; + e.cursor = this.cursor; + return e; + } +}; + +function CompilerContext(inFunction) { + this.inFunction = inFunction; + this.stmtStack = []; + this.funDecls = []; + this.varDecls = []; +} + +var CCp = CompilerContext.prototype; +CCp.bracketLevel = CCp.curlyLevel = CCp.parenLevel = CCp.hookLevel = 0; +CCp.ecmaStrictMode = CCp.inForLoopInit = false; + +function Script(t, x) { + var n = Statements(t, x); + n.type = SCRIPT; + n.funDecls = x.funDecls; + n.varDecls = x.varDecls; + return n; +} + +// Node extends Array, which we extend slightly with a top-of-stack method. +Array.prototype.top = function() { + return this.length && this[this.length-1]; +} + +function NarcNode(t, type) { + var token = t.token(); + if (token) { + this.type = type || token.type; + this.value = token.value; + this.lineno = token.lineno; + this.start = token.start; + this.end = token.end; + } else { + this.type = type; + this.lineno = t.lineno; + } + this.tokenizer = t; + for (var i = 2; i < arguments.length; i++) + this.push(arguments[i]); +} + +var Np = NarcNode.prototype = new Array(); +Np.constructor = NarcNode; +Np.$length = 0; +Np.toSource = Object.prototype.toSource; + +// Always use push to add operands to an expression, to update start and end. +Np.push = function (kid) { + if (kid.start < this.start) + this.start = kid.start; + if (this.end < kid.end) + this.end = kid.end; + //debug('length before => '+this.$length); + this[this.$length] = kid; + this.$length++; + //debug('length after => '+this.$length); +} + +NarcNode.indentLevel = 0; + +function tokenstr(tt) { + var t = tokens[tt]; + return /^\W/.test(t) ? opTypeNames[t] : t.toUpperCase(); +} + +Np.toString = function () { + var a = []; + for (var i in this) { + if (this.hasOwnProperty(i) && i != 'type') + a.push({id: i, value: this[i]}); + } + a.sort(function (a,b) { return (a.id < b.id) ? -1 : 1; }); + INDENTATION = " "; + var n = ++NarcNode.indentLevel; + var s = "{\n" + INDENTATION.repeat(n) + "type: " + tokenstr(this.type); + for (i = 0; i < a.length; i++) + s += ",\n" + INDENTATION.repeat(n) + a[i].id + ": " + a[i].value; + n = --NarcNode.indentLevel; + s += "\n" + INDENTATION.repeat(n) + "}"; + return s; +} + +Np.getSource = function () { + return this.tokenizer.source.slice(this.start, this.end); +}; + +Np.filename = function () { return this.tokenizer.filename; }; + +String.prototype.repeat = function (n) { + var s = "", t = this + s; + while (--n >= 0) + s += t; + return s; +} + +// Statement stack and nested statement handler. +function nest(t, x, node, func, end) { + x.stmtStack.push(node); + var n = func(t, x); + x.stmtStack.pop(); + end && t.mustMatch(end); + return n; +} + +function Statements(t, x) { + var n = new NarcNode(t, BLOCK); + x.stmtStack.push(n); + while (!t.done() && t.peek() != RIGHT_CURLY) + n.push(Statement(t, x)); + x.stmtStack.pop(); + return n; +} + +function Block(t, x) { + t.mustMatch(LEFT_CURLY); + var n = Statements(t, x); + t.mustMatch(RIGHT_CURLY); + return n; +} + +DECLARED_FORM = 0; EXPRESSED_FORM = 1; STATEMENT_FORM = 2; + +function Statement(t, x) { + var i, label, n, n2, ss, tt = t.get(); + + // Cases for statements ending in a right curly return early, avoiding the + // common semicolon insertion magic after this switch. + switch (tt) { + case FUNCTION: + return FunctionDefinition(t, x, true, + (x.stmtStack.length > 1) + ? STATEMENT_FORM + : DECLARED_FORM); + + case LEFT_CURLY: + n = Statements(t, x); + t.mustMatch(RIGHT_CURLY); + return n; + + case IF: + n = new NarcNode(t); + n.condition = ParenExpression(t, x); + x.stmtStack.push(n); + n.thenPart = Statement(t, x); + n.elsePart = t.match(ELSE) ? Statement(t, x) : null; + x.stmtStack.pop(); + return n; + + case SWITCH: + n = new NarcNode(t); + t.mustMatch(LEFT_PAREN); + n.discriminant = Expression(t, x); + t.mustMatch(RIGHT_PAREN); + n.cases = []; + n.defaultIndex = -1; + x.stmtStack.push(n); + t.mustMatch(LEFT_CURLY); + while ((tt = t.get()) != RIGHT_CURLY) { + switch (tt) { + case DEFAULT: + if (n.defaultIndex >= 0) + throw t.newSyntaxError("More than one switch default"); + // FALL THROUGH + case CASE: + n2 = new NarcNode(t); + if (tt == DEFAULT) + n.defaultIndex = n.cases.length; + else + n2.caseLabel = Expression(t, x, COLON); + break; + default: + throw t.newSyntaxError("Invalid switch case"); + } + t.mustMatch(COLON); + n2.statements = new NarcNode(t, BLOCK); + while ((tt=t.peek()) != CASE && tt != DEFAULT && tt != RIGHT_CURLY) + n2.statements.push(Statement(t, x)); + n.cases.push(n2); + } + x.stmtStack.pop(); + return n; + + case FOR: + n = new NarcNode(t); + n.isLoop = true; + t.mustMatch(LEFT_PAREN); + if ((tt = t.peek()) != SEMICOLON) { + x.inForLoopInit = true; + if (tt == VAR || tt == CONST) { + t.get(); + n2 = Variables(t, x); + } else { + n2 = Expression(t, x); + } + x.inForLoopInit = false; + } + if (n2 && t.match(IN)) { + n.type = FOR_IN; + if (n2.type == VAR) { + if (n2.$length != 1) { + throw new SyntaxError("Invalid for..in left-hand side", + t.filename, n2.lineno); + } + + // NB: n2[0].type == IDENTIFIER and n2[0].value == n2[0].name. + n.iterator = n2[0]; + n.varDecl = n2; + } else { + n.iterator = n2; + n.varDecl = null; + } + n.object = Expression(t, x); + } else { + n.setup = n2 || null; + t.mustMatch(SEMICOLON); + n.condition = (t.peek() == SEMICOLON) ? null : Expression(t, x); + t.mustMatch(SEMICOLON); + n.update = (t.peek() == RIGHT_PAREN) ? null : Expression(t, x); + } + t.mustMatch(RIGHT_PAREN); + n.body = nest(t, x, n, Statement); + return n; + + case WHILE: + n = new NarcNode(t); + n.isLoop = true; + n.condition = ParenExpression(t, x); + n.body = nest(t, x, n, Statement); + return n; + + case DO: + n = new NarcNode(t); + n.isLoop = true; + n.body = nest(t, x, n, Statement, WHILE); + n.condition = ParenExpression(t, x); + if (!x.ecmaStrictMode) { + // <script language="JavaScript"> (without version hints) may need + // automatic semicolon insertion without a newline after do-while. + // See http://bugzilla.mozilla.org/show_bug.cgi?id=238945. + t.match(SEMICOLON); + return n; + } + break; + + case BREAK: + case CONTINUE: + n = new NarcNode(t); + if (t.peekOnSameLine() == IDENTIFIER) { + t.get(); + n.label = t.token().value; + } + ss = x.stmtStack; + i = ss.length; + label = n.label; + if (label) { + do { + if (--i < 0) + throw t.newSyntaxError("Label not found"); + } while (ss[i].label != label); + } else { + do { + if (--i < 0) { + throw t.newSyntaxError("Invalid " + ((tt == BREAK) + ? "break" + : "continue")); + } + } while (!ss[i].isLoop && (tt != BREAK || ss[i].type != SWITCH)); + } + n.target = ss[i]; + break; + + case TRY: + n = new NarcNode(t); + n.tryBlock = Block(t, x); + n.catchClauses = []; + while (t.match(CATCH)) { + n2 = new NarcNode(t); + t.mustMatch(LEFT_PAREN); + n2.varName = t.mustMatch(IDENTIFIER).value; + if (t.match(IF)) { + if (x.ecmaStrictMode) + throw t.newSyntaxError("Illegal catch guard"); + if (n.catchClauses.length && !n.catchClauses.top().guard) + throw t.newSyntaxError("Guarded catch after unguarded"); + n2.guard = Expression(t, x); + } else { + n2.guard = null; + } + t.mustMatch(RIGHT_PAREN); + n2.block = Block(t, x); + n.catchClauses.push(n2); + } + if (t.match(FINALLY)) + n.finallyBlock = Block(t, x); + if (!n.catchClauses.length && !n.finallyBlock) + throw t.newSyntaxError("Invalid try statement"); + return n; + + case CATCH: + case FINALLY: + throw t.newSyntaxError(tokens[tt] + " without preceding try"); + + case THROW: + n = new NarcNode(t); + n.exception = Expression(t, x); + break; + + case RETURN: + if (!x.inFunction) + throw t.newSyntaxError("Invalid return"); + n = new NarcNode(t); + tt = t.peekOnSameLine(); + if (tt != END && tt != NEWLINE && tt != SEMICOLON && tt != RIGHT_CURLY) + n.value = Expression(t, x); + break; + + case WITH: + n = new NarcNode(t); + n.object = ParenExpression(t, x); + n.body = nest(t, x, n, Statement); + return n; + + case VAR: + case CONST: + n = Variables(t, x); + break; + + case DEBUGGER: + n = new NarcNode(t); + break; + + case REQUIRE: + n = new NarcNode(t); + n.classPath = ParenExpression(t, x); + break; + + case NEWLINE: + case SEMICOLON: + n = new NarcNode(t, SEMICOLON); + n.expression = null; + return n; + + default: + if (tt == IDENTIFIER && t.peek() == COLON) { + label = t.token().value; + ss = x.stmtStack; + for (i = ss.length-1; i >= 0; --i) { + if (ss[i].label == label) + throw t.newSyntaxError("Duplicate label"); + } + t.get(); + n = new NarcNode(t, LABEL); + n.label = label; + n.statement = nest(t, x, n, Statement); + return n; + } + + n = new NarcNode(t, SEMICOLON); + t.unget(); + n.expression = Expression(t, x); + n.end = n.expression.end; + break; + } + + if (t.lineno == t.token().lineno) { + tt = t.peekOnSameLine(); + if (tt != END && tt != NEWLINE && tt != SEMICOLON && tt != RIGHT_CURLY) + throw t.newSyntaxError("Missing ; before statement"); + } + t.match(SEMICOLON); + return n; +} + +function FunctionDefinition(t, x, requireName, functionForm) { + var f = new NarcNode(t); + if (f.type != FUNCTION) + f.type = (f.value == "get") ? GETTER : SETTER; + if (t.match(IDENTIFIER)) { + f.name = t.token().value; + } + else if (requireName) + throw t.newSyntaxError("Missing function identifier"); + + t.mustMatch(LEFT_PAREN); + f.params = []; + var tt; + while ((tt = t.get()) != RIGHT_PAREN) { + if (tt != IDENTIFIER) + throw t.newSyntaxError("Missing formal parameter"); + f.params.push(t.token().value); + if (t.peek() != RIGHT_PAREN) + t.mustMatch(COMMA); + } + + t.mustMatch(LEFT_CURLY); + var x2 = new CompilerContext(true); + f.body = Script(t, x2); + t.mustMatch(RIGHT_CURLY); + f.end = t.token().end; + + f.functionForm = functionForm; + if (functionForm == DECLARED_FORM) { + x.funDecls.push(f); + } + + return f; +} + +function Variables(t, x) { + var n = new NarcNode(t); + do { + t.mustMatch(IDENTIFIER); + var n2 = new NarcNode(t); + n2.name = n2.value; + if (t.match(ASSIGN)) { + if (t.token().assignOp) + throw t.newSyntaxError("Invalid variable initialization"); + n2.initializer = Expression(t, x, COMMA); + } + n2.readOnly = (n.type == CONST); + n.push(n2); + x.varDecls.push(n2); + } while (t.match(COMMA)); + return n; +} + +function ParenExpression(t, x) { + t.mustMatch(LEFT_PAREN); + var n = Expression(t, x); + t.mustMatch(RIGHT_PAREN); + return n; +} + +var opPrecedence = { + SEMICOLON: 0, + COMMA: 1, + ASSIGN: 2, HOOK: 2, COLON: 2, CONDITIONAL: 2, + // The above all have to have the same precedence, see bug 330975. + OR: 4, + AND: 5, + BITWISE_OR: 6, + BITWISE_XOR: 7, + BITWISE_AND: 8, + EQ: 9, NE: 9, STRICT_EQ: 9, STRICT_NE: 9, + LT: 10, LE: 10, GE: 10, GT: 10, IN: 10, INSTANCEOF: 10, + LSH: 11, RSH: 11, URSH: 11, + PLUS: 12, MINUS: 12, + MUL: 13, DIV: 13, MOD: 13, + DELETE: 14, VOID: 14, TYPEOF: 14, // PRE_INCREMENT: 14, PRE_DECREMENT: 14, + NOT: 14, BITWISE_NOT: 14, UNARY_PLUS: 14, UNARY_MINUS: 14, + INCREMENT: 15, DECREMENT: 15, // postfix + NEW: 16, + DOT: 17 +}; + +// Map operator type code to precedence. +for (i in opPrecedence) + opPrecedence[GLOBAL[i]] = opPrecedence[i]; + +var opArity = { + COMMA: -2, + ASSIGN: 2, + CONDITIONAL: 3, + OR: 2, + AND: 2, + BITWISE_OR: 2, + BITWISE_XOR: 2, + BITWISE_AND: 2, + EQ: 2, NE: 2, STRICT_EQ: 2, STRICT_NE: 2, + LT: 2, LE: 2, GE: 2, GT: 2, IN: 2, INSTANCEOF: 2, + LSH: 2, RSH: 2, URSH: 2, + PLUS: 2, MINUS: 2, + MUL: 2, DIV: 2, MOD: 2, + DELETE: 1, VOID: 1, TYPEOF: 1, // PRE_INCREMENT: 1, PRE_DECREMENT: 1, + NOT: 1, BITWISE_NOT: 1, UNARY_PLUS: 1, UNARY_MINUS: 1, + INCREMENT: 1, DECREMENT: 1, // postfix + NEW: 1, NEW_WITH_ARGS: 2, DOT: 2, INDEX: 2, CALL: 2, + ARRAY_INIT: 1, OBJECT_INIT: 1, GROUP: 1 +}; + +// Map operator type code to arity. +for (i in opArity) + opArity[GLOBAL[i]] = opArity[i]; + +function Expression(t, x, stop) { + var n, id, tt, operators = [], operands = []; + var bl = x.bracketLevel, cl = x.curlyLevel, pl = x.parenLevel, + hl = x.hookLevel; + + function reduce() { + //debug('OPERATORS => '+operators); + var n = operators.pop(); + var op = n.type; + var arity = opArity[op]; + if (arity == -2) { + // Flatten left-associative trees. + var left = operands.length >= 2 && operands[operands.length-2]; + if (left.type == op) { + var right = operands.pop(); + left.push(right); + return left; + } + arity = 2; + } + + // Always use push to add operands to n, to update start and end. + var a = operands.splice(operands.length - arity, operands.length); + for (var i = 0; i < arity; i++) { + n.push(a[i]); + } + + // Include closing bracket or postfix operator in [start,end). + if (n.end < t.token().end) + n.end = t.token().end; + + operands.push(n); + return n; + } + +loop: + while ((tt = t.get()) != END) { + //debug('TT => '+tokens[tt]); + if (tt == stop && + x.bracketLevel == bl && x.curlyLevel == cl && x.parenLevel == pl && + x.hookLevel == hl) { + // Stop only if tt matches the optional stop parameter, and that + // token is not quoted by some kind of bracket. + break; + } + switch (tt) { + case SEMICOLON: + // NB: cannot be empty, Statement handled that. + break loop; + + case ASSIGN: + case HOOK: + case COLON: + if (t.scanOperand) + break loop; + // Use >, not >=, for right-associative ASSIGN and HOOK/COLON. + while (operators.length && opPrecedence[operators.top().type] > opPrecedence[tt] || + (tt == COLON && operators.top().type == ASSIGN)) { + reduce(); + } + if (tt == COLON) { + n = operators.top(); + if (n.type != HOOK) + throw t.newSyntaxError("Invalid label"); + n.type = CONDITIONAL; + --x.hookLevel; + } else { + operators.push(new NarcNode(t)); + if (tt == ASSIGN) + operands.top().assignOp = t.token().assignOp; + else + ++x.hookLevel; // tt == HOOK + } + t.scanOperand = true; + break; + + case IN: + // An in operator should not be parsed if we're parsing the head of + // a for (...) loop, unless it is in the then part of a conditional + // expression, or parenthesized somehow. + if (x.inForLoopInit && !x.hookLevel && + !x.bracketLevel && !x.curlyLevel && !x.parenLevel) { + break loop; + } + // FALL THROUGH + case COMMA: + // Treat comma as left-associative so reduce can fold left-heavy + // COMMA trees into a single array. + // FALL THROUGH + case OR: + case AND: + case BITWISE_OR: + case BITWISE_XOR: + case BITWISE_AND: + case EQ: case NE: case STRICT_EQ: case STRICT_NE: + case LT: case LE: case GE: case GT: + case INSTANCEOF: + case LSH: case RSH: case URSH: + case PLUS: case MINUS: + case MUL: case DIV: case MOD: + case DOT: + if (t.scanOperand) + break loop; + while (operators.length && opPrecedence[operators.top().type] >= opPrecedence[tt]) + reduce(); + if (tt == DOT) { + t.mustMatch(IDENTIFIER); + operands.push(new NarcNode(t, DOT, operands.pop(), new NarcNode(t))); + } else { + operators.push(new NarcNode(t)); + t.scanOperand = true; + } + break; + + case DELETE: case VOID: case TYPEOF: + case NOT: case BITWISE_NOT: case UNARY_PLUS: case UNARY_MINUS: + case NEW: + if (!t.scanOperand) + break loop; + operators.push(new NarcNode(t)); + break; + + case INCREMENT: case DECREMENT: + if (t.scanOperand) { + operators.push(new NarcNode(t)); // prefix increment or decrement + } else { + // Use >, not >=, so postfix has higher precedence than prefix. + while (operators.length && opPrecedence[operators.top().type] > opPrecedence[tt]) + reduce(); + n = new NarcNode(t, tt, operands.pop()); + n.postfix = true; + operands.push(n); + } + break; + + case FUNCTION: + if (!t.scanOperand) + break loop; + operands.push(FunctionDefinition(t, x, false, EXPRESSED_FORM)); + t.scanOperand = false; + break; + + case NULL: case THIS: case TRUE: case FALSE: + case IDENTIFIER: case NUMBER: case STRING: case REGEXP: + if (!t.scanOperand) + break loop; + operands.push(new NarcNode(t)); + t.scanOperand = false; + break; + + case LEFT_BRACKET: + if (t.scanOperand) { + // Array initialiser. Parse using recursive descent, as the + // sub-grammar here is not an operator grammar. + n = new NarcNode(t, ARRAY_INIT); + while ((tt = t.peek()) != RIGHT_BRACKET) { + if (tt == COMMA) { + t.get(); + n.push(null); + continue; + } + n.push(Expression(t, x, COMMA)); + if (!t.match(COMMA)) + break; + } + t.mustMatch(RIGHT_BRACKET); + operands.push(n); + t.scanOperand = false; + } else { + // Property indexing operator. + operators.push(new NarcNode(t, INDEX)); + t.scanOperand = true; + ++x.bracketLevel; + } + break; + + case RIGHT_BRACKET: + if (t.scanOperand || x.bracketLevel == bl) + break loop; + while (reduce().type != INDEX) + continue; + --x.bracketLevel; + break; + + case LEFT_CURLY: + if (!t.scanOperand) + break loop; + // Object initialiser. As for array initialisers (see above), + // parse using recursive descent. + ++x.curlyLevel; + n = new NarcNode(t, OBJECT_INIT); + object_init: + if (!t.match(RIGHT_CURLY)) { + do { + tt = t.get(); + if ((t.token().value == "get" || t.token().value == "set") && + t.peek() == IDENTIFIER) { + if (x.ecmaStrictMode) + throw t.newSyntaxError("Illegal property accessor"); + n.push(FunctionDefinition(t, x, true, EXPRESSED_FORM)); + } else { + switch (tt) { + case IDENTIFIER: + case NUMBER: + case STRING: + id = new NarcNode(t); + break; + case RIGHT_CURLY: + if (x.ecmaStrictMode) + throw t.newSyntaxError("Illegal trailing ,"); + break object_init; + default: + throw t.newSyntaxError("Invalid property name"); + } + t.mustMatch(COLON); + n.push(new NarcNode(t, PROPERTY_INIT, id, + Expression(t, x, COMMA))); + } + } while (t.match(COMMA)); + t.mustMatch(RIGHT_CURLY); + } + operands.push(n); + t.scanOperand = false; + --x.curlyLevel; + break; + + case RIGHT_CURLY: + if (!t.scanOperand && x.curlyLevel != cl) + throw "PANIC: right curly botch"; + break loop; + + case LEFT_PAREN: + if (t.scanOperand) { + operators.push(new NarcNode(t, GROUP)); + } else { + while (operators.length && opPrecedence[operators.top().type] > opPrecedence[NEW]) + reduce(); + + // Handle () now, to regularize the n-ary case for n > 0. + // We must set scanOperand in case there are arguments and + // the first one is a regexp or unary+/-. + n = operators.top(); + t.scanOperand = true; + if (t.match(RIGHT_PAREN)) { + if (n.type == NEW) { + --operators.length; + n.push(operands.pop()); + } else { + n = new NarcNode(t, CALL, operands.pop(), + new NarcNode(t, LIST)); + } + operands.push(n); + t.scanOperand = false; + break; + } + if (n.type == NEW) + n.type = NEW_WITH_ARGS; + else + operators.push(new NarcNode(t, CALL)); + } + ++x.parenLevel; + break; + + case RIGHT_PAREN: + if (t.scanOperand || x.parenLevel == pl) + break loop; + while ((tt = reduce().type) != GROUP && tt != CALL && + tt != NEW_WITH_ARGS) { + continue; + } + if (tt != GROUP) { + n = operands.top(); + if (n[1].type != COMMA) + n[1] = new NarcNode(t, LIST, n[1]); + else + n[1].type = LIST; + } + --x.parenLevel; + break; + + // Automatic semicolon insertion means we may scan across a newline + // and into the beginning of another statement. If so, break out of + // the while loop and let the t.scanOperand logic handle errors. + default: + break loop; + } + } + if (x.hookLevel != hl) + throw t.newSyntaxError("Missing : after ?"); + if (x.parenLevel != pl) + throw t.newSyntaxError("Missing ) in parenthetical"); + if (x.bracketLevel != bl) + throw t.newSyntaxError("Missing ] in index expression"); + if (t.scanOperand) + throw t.newSyntaxError("Missing operand"); + + // Resume default mode, scanning for operands, not operators. + t.scanOperand = true; + t.unget(); + + while (operators.length) + reduce(); + return operands.pop(); +} + +function parse(s, f, l) { + var t = new Tokenizer(s, f, l); + var x = new CompilerContext(false); + var n = Script(t, x); + if (!t.done()) + throw t.newSyntaxError("Syntax error"); + return n; +} + +debug = function(msg) { + document.body.appendChild(document.createTextNode(msg)); + document.body.appendChild(document.createElement('br')); +} + diff --git a/tests/test_tools/selenium/core/scripts/se2html.js b/tests/test_tools/selenium/core/scripts/se2html.js new file mode 100644 index 00000000..67054a49 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/se2html.js @@ -0,0 +1,63 @@ +/*
+
+This is an experiment in creating a "selenese" parser that drastically
+cuts down on the line noise associated with writing tests in HTML.
+
+The 'parse' function will accept the follow sample commands.
+
+test-cases:
+ //comment
+ command "param"
+ command "param" // comment
+ command "param" "param2"
+ command "param" "param2" // this is a comment
+
+TODO:
+1) Deal with multiline parameters
+2) Escape quotes properly
+3) Determine whether this should/will become the "preferred" syntax
+ for delivered Selenium self-test scripts
+*/
+
+
+function separse(doc) {
+ // Get object
+ script = doc.getElementById('testcase')
+ // Split into lines
+ lines = script.text.split('\n');
+
+
+ var command_pattern = / *(\w+) *"([^"]*)" *(?:"([^"]*)"){0,1}(?: *(\/\/ *.+))*/i;
+ var comment_pattern = /^ *(\/\/ *.+)/
+
+ // Regex each line into selenium command and convert into table row.
+ // eg. "<command> <quote> <quote> <comment>"
+ var new_test_source = '';
+ var new_line = '';
+ for (var x=0; x < lines.length; x++) {
+ result = lines[x].match(command_pattern);
+ if (result != null) {
+ new_line = "<tr><td>" + (result[1] || ' ') + "</td>" +
+ "<td>" + (result[2] || ' ') + "</td>" +
+ "<td>" + (result[3] || ' ') + "</td>" +
+ "<td>" + (result[4] || ' ') + "</td></tr>\n";
+ new_test_source += new_line;
+ }
+ result = lines[x].match(comment_pattern);
+ if (result != null) {
+ new_line = '<tr><td rowspan="1" colspan="4">' +
+ (result[1] || ' ') +
+ '</td></tr>';
+ new_test_source += new_line;
+ }
+ }
+
+ // Create HTML Table
+ body = doc.body
+ body.innerHTML += "<table class='selenium' id='testtable'>"+
+ new_test_source +
+ "</table>";
+
+}
+
+
diff --git a/tests/test_tools/selenium/core/scripts/selenium-api.js b/tests/test_tools/selenium/core/scripts/selenium-api.js index ad0509ee..e8e587f7 100644 --- a/tests/test_tools/selenium/core/scripts/selenium-api.js +++ b/tests/test_tools/selenium/core/scripts/selenium-api.js @@ -15,12 +15,14 @@ * */ +// TODO: stop navigating this.page().document() ... it breaks encapsulation + var storedVars = new Object(); function Selenium(browserbot) { /** * Defines an object that runs Selenium commands. - * + * * <h3><a name="locators"></a>Element Locators</h3> * <p> * Element Locators tell Selenium which HTML element a command refers to. @@ -28,7 +30,7 @@ function Selenium(browserbot) { * <blockquote> * <em>locatorType</em><strong>=</strong><em>argument</em> * </blockquote> - * + * * <p> * We support the following strategies for locating elements: * </p> @@ -40,7 +42,7 @@ function Selenium(browserbot) { * (This is normally the default; see below.)</dd> * <dt><strong>id</strong>=<em>id</em></dt> * <dd>Select the element with the specified @id attribute.</dd> - * + * * <dt><strong>name</strong>=<em>name</em></dt> * <dd>Select the first element with the specified @name attribute.</dd> * <dd><ul class="first last simple"> @@ -49,15 +51,15 @@ function Selenium(browserbot) { * </ul> * </dd> * <dd>The name may optionally be followed by one or more <em>element-filters</em>, separated from the name by whitespace. If the <em>filterType</em> is not specified, <strong>value</strong> is assumed.</dd> - * + * * <dd><ul class="first last simple"> * <li>name=flavour value=chocolate</li> * </ul> * </dd> * <dt><strong>dom</strong>=<em>javascriptExpression</em></dt> - * + * * <dd> - * + * * <dd>Find an element using JavaScript traversal of the HTML Document Object * Model. DOM locators <em>must</em> begin with "document.". * <ul class="first last simple"> @@ -65,15 +67,15 @@ function Selenium(browserbot) { * <li>dom=document.images[56]</li> * </ul> * </dd> - * + * * </dd> - * + * * <dt><strong>xpath</strong>=<em>xpathExpression</em></dt> * <dd>Locate an element using an XPath expression. * <ul class="first last simple"> * <li>xpath=//img[@alt='The image alt text']</li> * <li>xpath=//table[@id='table1']//tr[4]/td[2]</li> - * + * * </ul> * </dd> * <dt><strong>link</strong>=<em>textPattern</em></dt> @@ -82,15 +84,24 @@ function Selenium(browserbot) { * <ul class="first last simple"> * <li>link=The link text</li> * </ul> - * + * + * </dd> + * + * <dt><strong>css</strong>=<em>cssSelectorSyntax</em></dt> + * <dd>Select the element using css selectors. Please refer to <a href="http://www.w3.org/TR/REC-CSS2/selector.html">CSS2 selectors</a>, <a href="http://www.w3.org/TR/2001/CR-css3-selectors-20011113/">CSS3 selectors</a> for more information. You can also check the TestCssLocators test in the selenium test suite for an example of usage, which is included in the downloaded selenium core package. + * <ul class="first last simple"> + * <li>css=a[href="#id3"]</li> + * <li>css=span#firstChild + span</li> + * </ul> * </dd> + * <dd>Currently the css selector locator supports all css1, css2 and css3 selectors except namespace in css3, some pseudo classes(:nth-of-type, :nth-last-of-type, :first-of-type, :last-of-type, :only-of-type, :visited, :hover, :active, :focus, :indeterminate) and pseudo elements(::first-line, ::first-letter, ::selection, ::before, ::after). </dd> * </dl> * </blockquote> * <p> * Without an explicit locator prefix, Selenium uses the following default * strategies: * </p> - * + * * <ul class="simple"> * <li><strong>dom</strong>, for locators starting with "document."</li> * <li><strong>xpath</strong>, for locators starting with "//"</li> @@ -103,7 +114,7 @@ function Selenium(browserbot) { * <p>Filters look much like locators, ie.</p> * <blockquote> * <em>filterType</em><strong>=</strong><em>argument</em></blockquote> - * + * * <p>Supported element-filters are:</p> * <p><strong>value=</strong><em>valuePattern</em></p> * <blockquote> @@ -114,7 +125,7 @@ function Selenium(browserbot) { * </blockquote> * * <h3><a name="patterns"></a>String-match Patterns</h3> - * + * * <p> * Various Pattern syntaxes are available for matching string values: * </p> @@ -130,7 +141,7 @@ function Selenium(browserbot) { * <dd>Match a string using a regular-expression. The full power of JavaScript * regular-expressions is available.</dd> * <dt><strong>exact:</strong><em>string</em></dt> - * + * * <dd>Match a string exactly, verbatim, without any of that fancy wildcard * stuff.</dd> * </dl> @@ -145,18 +156,23 @@ function Selenium(browserbot) { this.page = function() { return browserbot.getCurrentPage(); }; + this.defaultTimeout = Selenium.DEFAULT_TIMEOUT; } -Selenium.createForFrame = function(frame) { - return new Selenium(BrowserBot.createForFrame(frame)); +Selenium.DEFAULT_TIMEOUT = 30 * 1000; + +Selenium.createForWindow = function(window) { + if (!window.location) { + throw "error: not a window!"; + } + return new Selenium(BrowserBot.createForWindow(window)); }; Selenium.prototype.reset = function() { - /** - * Clear out all stored variables and select the null (starting) window - */ - storedVars = new Object(); + this.defaultTimeout = Selenium.DEFAULT_TIMEOUT; + // todo: this.browserbot.reset() this.browserbot.selectWindow("null"); + this.browserbot.resetPopups(); }; Selenium.prototype.doClick = function(locator) { @@ -164,12 +180,31 @@ Selenium.prototype.doClick = function(locator) { * Clicks on a link, button, checkbox or radio button. If the click action * causes a new page to load (like a link usually does), call * waitForPageToLoad. - * + * + * @param locator an element locator + * + */ + var element = this.page().findElement(locator); + this.page().clickElement(element); +}; + +Selenium.prototype.doClickAt = function(locator, coordString) { + /** + * Clicks on a link, button, checkbox or radio button. If the click action + * causes a new page to load (like a link usually does), call + * waitForPageToLoad. + * + * Beware of http://jira.openqa.org/browse/SEL-280, which will lead some event handlers to + * get null event arguments. Read the bug for more details, including a workaround. + * * @param locator an element locator - * + * @param coordString specifies the x,y position (i.e. - 10,20) of the mouse + * event relative to the element returned by the locator. + * */ var element = this.page().findElement(locator); - this.page().clickElement(element); + var clientXY = getClientXY(element, coordString) + this.page().clickElement(element, clientXY[0], clientXY[1]); }; Selenium.prototype.doFireEvent = function(locator, eventName) { @@ -184,70 +219,179 @@ Selenium.prototype.doFireEvent = function(locator, eventName) { triggerEvent(element, eventName, false); }; -Selenium.prototype.doKeyPress = function(locator, keycode) { +Selenium.prototype.doKeyPress = function(locator, keySequence) { /** * Simulates a user pressing and releasing a key. - * + * * @param locator an <a href="#locators">element locator</a> - * @param keycode the numeric keycode of the key to be pressed, normally the - * ASCII value of that key. + * @param keySequence Either be a string("\" followed by the numeric keycode + * of the key to be pressed, normally the ASCII value of that key), or a single + * character. For example: "w", "\119". */ var element = this.page().findElement(locator); - triggerKeyEvent(element, 'keypress', keycode, true); + triggerKeyEvent(element, 'keypress', keySequence, true); }; -Selenium.prototype.doKeyDown = function(locator, keycode) { +Selenium.prototype.doKeyDown = function(locator, keySequence) { /** * Simulates a user pressing a key (without releasing it yet). - * + * * @param locator an <a href="#locators">element locator</a> - * @param keycode the numeric keycode of the key to be pressed, normally the - * ASCII value of that key. + * @param keySequence Either be a string("\" followed by the numeric keycode + * of the key to be pressed, normally the ASCII value of that key), or a single + * character. For example: "w", "\119". */ var element = this.page().findElement(locator); - triggerKeyEvent(element, 'keydown', keycode, true); + triggerKeyEvent(element, 'keydown', keySequence, true); }; -Selenium.prototype.doKeyUp = function(locator, keycode) { +Selenium.prototype.doKeyUp = function(locator, keySequence) { /** * Simulates a user releasing a key. - * + * * @param locator an <a href="#locators">element locator</a> - * @param keycode the numeric keycode of the key to be released, normally the - * ASCII value of that key. + * @param keySequence Either be a string("\" followed by the numeric keycode + * of the key to be pressed, normally the ASCII value of that key), or a single + * character. For example: "w", "\119". */ var element = this.page().findElement(locator); - triggerKeyEvent(element, 'keyup', keycode, true); + triggerKeyEvent(element, 'keyup', keySequence, true); }; +function getClientXY(element, coordString) { + // Parse coordString + var coords = null; + var x; + var y; + if (coordString) { + coords = coordString.split(/,/); + x = Number(coords[0]); + y = Number(coords[1]); + } + else { + x = y = 0; + } + + // Get position of element, + // Return 2 item array with clientX and clientY + return [Selenium.prototype.getElementPositionLeft(element) + x, Selenium.prototype.getElementPositionTop(element) + y]; +} + Selenium.prototype.doMouseOver = function(locator) { /** * Simulates a user hovering a mouse over the specified element. - * + * * @param locator an <a href="#locators">element locator</a> */ var element = this.page().findElement(locator); triggerMouseEvent(element, 'mouseover', true); }; +Selenium.prototype.doMouseOut = function(locator) { + /** + * Simulates a user moving the mouse pointer away from the specified element. + * + * @param locator an <a href="#locators">element locator</a> + */ + var element = this.page().findElement(locator); + triggerMouseEvent(element, 'mouseout', true); +}; + Selenium.prototype.doMouseDown = function(locator) { /** * Simulates a user pressing the mouse button (without releasing it yet) on * the specified element. - * + * + * @param locator an <a href="#locators">element locator</a> + */ + var element = this.page().findElement(locator); + triggerMouseEvent(element, 'mousedown', true); +}; + +Selenium.prototype.doMouseDownAt = function(locator, coordString) { + /** + * Simulates a user pressing the mouse button (without releasing it yet) on + * the specified element. + * + * Beware of http://jira.openqa.org/browse/SEL-280, which will lead some event handlers to + * get null event arguments. Read the bug for more details, including a workaround. + * + * @param locator an <a href="#locators">element locator</a> + * @param coordString specifies the x,y position (i.e. - 10,20) of the mouse + * event relative to the element returned by the locator. + */ + var element = this.page().findElement(locator); + var clientXY = getClientXY(element, coordString) + + triggerMouseEvent(element, 'mousedown', true, clientXY[0], clientXY[1]); +}; + +Selenium.prototype.doMouseUp = function(locator) { + /** + * Simulates a user pressing the mouse button (without releasing it yet) on + * the specified element. + * + * @param locator an <a href="#locators">element locator</a> + */ + var element = this.page().findElement(locator); + triggerMouseEvent(element, 'mouseup', true); +}; + +Selenium.prototype.doMouseUpAt = function(locator, coordString) { + /** + * Simulates a user pressing the mouse button (without releasing it yet) on + * the specified element. + * + * Beware of http://jira.openqa.org/browse/SEL-280, which will lead some event handlers to + * get null event arguments. Read the bug for more details, including a workaround. + * + * @param locator an <a href="#locators">element locator</a> + * @param coordString specifies the x,y position (i.e. - 10,20) of the mouse + * event relative to the element returned by the locator. + */ + var element = this.page().findElement(locator); + var clientXY = getClientXY(element, coordString) + + triggerMouseEvent(element, 'mouseup', true, clientXY[0], clientXY[1]); +}; + +Selenium.prototype.doMouseMove = function(locator) { + /** + * Simulates a user pressing the mouse button (without releasing it yet) on + * the specified element. + * + * @param locator an <a href="#locators">element locator</a> + */ + var element = this.page().findElement(locator); + triggerMouseEvent(element, 'mousemove', true); +}; + +Selenium.prototype.doMouseMoveAt = function(locator, coordString) { + /** + * Simulates a user pressing the mouse button (without releasing it yet) on + * the specified element. + * + * Beware of http://jira.openqa.org/browse/SEL-280, which will lead some event handlers to + * get null event arguments. Read the bug for more details, including a workaround. + * * @param locator an <a href="#locators">element locator</a> + * @param coordString specifies the x,y position (i.e. - 10,20) of the mouse + * event relative to the element returned by the locator. */ + var element = this.page().findElement(locator); - triggerMouseEvent(element, 'mousedown', true); + var clientXY = getClientXY(element, coordString) + + triggerMouseEvent(element, 'mousemove', true, clientXY[0], clientXY[1]); }; Selenium.prototype.doType = function(locator, value) { /** * Sets the value of an input field, as though you typed it in. - * + * * <p>Can also be used to set the value of combo boxes, check boxes, etc. In these cases, * value should be the value of the option selected, not the visible text.</p> - * + * * @param locator an <a href="#locators">element locator</a> * @param value the value to type */ @@ -267,7 +411,7 @@ Selenium.prototype.findToggleButton = function(locator) { Selenium.prototype.doCheck = function(locator) { /** * Check a toggle-button (checkbox/radio) - * + * * @param locator an <a href="#locators">element locator</a> */ this.findToggleButton(locator).checked = true; @@ -276,7 +420,7 @@ Selenium.prototype.doCheck = function(locator) { Selenium.prototype.doUncheck = function(locator) { /** * Uncheck a toggle-button (checkbox/radio) - * + * * @param locator an <a href="#locators">element locator</a> */ this.findToggleButton(locator).checked = false; @@ -285,7 +429,7 @@ Selenium.prototype.doUncheck = function(locator) { Selenium.prototype.doSelect = function(selectLocator, optionLocator) { /** * Select an option from a drop-down using an option locator. - * + * * <p> * Option locators provide different ways of specifying options of an HTML * Select element (e.g. for selecting a specific option, or for asserting @@ -305,11 +449,11 @@ Selenium.prototype.doSelect = function(selectLocator, optionLocator) { * <ul class="first last simple"> * <li>value=other</li> * </ul> - * - * + * + * * </dd> * <dt><strong>id</strong>=<em>id</em></dt> - * + * * <dd>matches options based on their ids. * <ul class="first last simple"> * <li>id=option1</li> @@ -318,7 +462,7 @@ Selenium.prototype.doSelect = function(selectLocator, optionLocator) { * <dt><strong>index</strong>=<em>index</em></dt> * <dd>matches an option based on its index (offset from zero). * <ul class="first last simple"> - * + * * <li>index=2</li> * </ul> * </dd> @@ -326,8 +470,8 @@ Selenium.prototype.doSelect = function(selectLocator, optionLocator) { * <p> * If no option locator prefix is provided, the default behaviour is to match on <strong>label</strong>. * </p> - * - * + * + * * @param selectLocator an <a href="#locators">element locator</a> identifying a drop-down menu * @param optionLocator an option locator (a label by default) */ @@ -381,26 +525,47 @@ Selenium.prototype.doSubmit = function(formLocator) { /** * Submit the specified form. This is particularly useful for forms without * submit buttons, e.g. single-input "Search" forms. - * + * * @param formLocator an <a href="#locators">element locator</a> for the form you want to submit */ var form = this.page().findElement(formLocator); var actuallySubmit = true; if (form.onsubmit) { - // apply this to the correct window so alerts are properly handled, even in IE HTA mode - actuallySubmit = form.onsubmit.apply(this.browserbot.getContentWindow()); + if (browserVersion.isHTA) { + // run the code in the correct window so alerts are handled correctly even in HTA mode + var win = this.browserbot.getCurrentWindow(); + var now = new Date().getTime(); + var marker = 'marker' + now; + win[marker] = form; + win.setTimeout("var actuallySubmit = "+marker+".onsubmit(); if (actuallySubmit) { "+marker+".submit(); };", 0); + // pause for at least 20ms for this command to run + testLoop.waitForCondition = function () { + return new Date().getTime() > (now + 20); + } + } else { + actuallySubmit = form.onsubmit(); + if (actuallySubmit) { + form.submit(); + } + } + } else { + form.submit(); } - if (actuallySubmit) { - form.submit(); + +}; + +Selenium.prototype.makePageLoadCondition = function(timeout) { + if (timeout == null) { + timeout = this.defaultTimeout; } - + return decorateFunctionWithTimeout(this._isNewPageLoaded.bind(this), timeout); }; Selenium.prototype.doOpen = function(url) { /** * Opens an URL in the test frame. This accepts both relative and absolute * URLs. - * + * * The "open" command waits for the page to load before proceeding, * ie. the "AndWait" suffix is implicit. * @@ -408,11 +573,11 @@ Selenium.prototype.doOpen = function(url) { * due to security restrictions in the browser (Same Origin Policy). If you * need to open an URL on another domain, use the Selenium Server to start a * new browser session on that domain. - * + * * @param url the URL to open; may be relative or absolute */ this.browserbot.openLocation(url); - return SELENIUM_PROCESS_WAIT; + return this.makePageLoadCondition(); }; Selenium.prototype.doSelectWindow = function(windowID) { @@ -420,12 +585,99 @@ Selenium.prototype.doSelectWindow = function(windowID) { * Selects a popup window; once a popup window has been selected, all * commands go to that window. To select the main window again, use "null" * as the target. - * + * * @param windowID the JavaScript window ID of the window to select */ this.browserbot.selectWindow(windowID); }; +Selenium.prototype.doSelectFrame = function(locator) { + /** + * Selects a frame within the current window. (You may invoke this command + * multiple times to select nested frames.) To select the parent frame, use + * "relative=parent" as a locator; to select the top frame, use "relative=top". + * + * <p>You may also use a DOM expression to identify the frame you want directly, + * like this: <code>dom=frames["main"].frames["subframe"]</code></p> + * + * @param locator an <a href="#locators">element locator</a> identifying a frame or iframe + */ + this.browserbot.selectFrame(locator); +}; + +Selenium.prototype.getLogMessages = function() { + /** + * Return the contents of the log. + * + * <p>This is a placeholder intended to make the code generator make this API + * available to clients. The selenium server will intercept this call, however, + * and return its recordkeeping of log messages since the last call to this API. + * Thus this code in JavaScript will never be called.</p> + * + * <p>The reason I opted for a servercentric solution is to be able to support + * multiple frames served from different domains, which would break a + * centralized JavaScript logging mechanism under some conditions.</p> + * + * @return string all log messages seen since the last call to this API + */ + return "getLogMessages should be implemented in the selenium server"; +}; + + +Selenium.prototype.getWhetherThisFrameMatchFrameExpression = function(currentFrameString, target) { + /** + * Determine whether current/locator identify the frame containing this running code. + * + * <p>This is useful in proxy injection mode, where this code runs in every + * browser frame and window, and sometimes the selenium server needs to identify + * the "current" frame. In this case, when the test calls selectFrame, this + * routine is called for each frame to figure out which one has been selected. + * The selected frame will return true, while all others will return false.</p> + * + * @param currentFrameString starting frame + * @param target new frame (which might be relative to the current one) + * @return boolean true if the new frame is this code's window + */ + var isDom = false; + if (target.indexOf("dom=") == 0) { + target = target.substr(4); + isDom = true; + } + var t; + try { + eval("t=" + currentFrameString + "." + target); + } catch (e) { + } + var autWindow = this.browserbot.getCurrentWindow(); + if (t != null) { + if (t.window == autWindow) { + return true; + } + return false; + } + if (isDom) { + return false; + } + var currentFrame; + eval("currentFrame=" + currentFrameString); + if (target == "relative=up") { + if (currentFrame.window.parent == autWindow) { + return true; + } + return false; + } + if (target == "relative=top") { + if (currentFrame.window.top == autWindow) { + return true; + } + return false; + } + if (autWindow.name == target && currentFrame.window == autWindow.parent) { + return true; + } + return false; +}; + Selenium.prototype.doWaitForPopUp = function(windowID, timeout) { /** * Waits for a popup window to appear and load up. @@ -436,21 +688,36 @@ Selenium.prototype.doWaitForPopUp = function(windowID, timeout) { if (isNaN(timeout)) { throw new SeleniumError("Timeout is not a number: " + timeout); } - - testLoop.waitForCondition = function () { - var targetWindow = selenium.browserbot.getTargetWindow(windowID); + + var popupLoadedPredicate = function () { + var targetWindow = selenium.browserbot.getWindowByName(windowID, true); if (!targetWindow) return false; if (!targetWindow.location) return false; if ("about:blank" == targetWindow.location) return false; + if (browserVersion.isKonqueror) { + if ("/" == targetWindow.location.href) { + // apparently Konqueror uses this as the temporary location, instead of about:blank + return false; + } + } + if (browserVersion.isSafari) { + if(targetWindow.location.href == selenium.browserbot.buttonWindow.location.href) { + // Apparently Safari uses this as the temporary location, instead of about:blank + // what a world! + LOG.debug("DGF what a world!"); + return false; + } + } if (!targetWindow.document) return false; - if (!targetWindow.document.readyState) return true; + if (!selenium.browserbot.getCurrentWindow().document.readyState) { + // This is Firefox, with no readyState extension + return true; + } if ('complete' != targetWindow.document.readyState) return false; return true; }; - - testLoop.waitForConditionStart = new Date().getTime(); - testLoop.waitForConditionTimeout = timeout; - + + return decorateFunctionWithTimeout(popupLoadedPredicate, timeout); } Selenium.prototype.doWaitForPopUp.dontCheckAlertsAndConfirms = true; @@ -461,7 +728,7 @@ Selenium.prototype.doChooseCancelOnNextConfirmation = function() { * return true, as if the user had manually clicked OK. After running * this command, the next call to confirm() will return false, as if * the user had clicked Cancel. - * + * */ this.browserbot.cancelNextConfirmation(); }; @@ -471,8 +738,8 @@ Selenium.prototype.doAnswerOnNextPrompt = function(answer) { /** * Instructs Selenium to return the specified answer string in response to * the next JavaScript prompt [window.prompt()]. - * - * + * + * * @param answer the answer to give in response to the prompt pop-up */ this.browserbot.setNextPromptResult(answer); @@ -481,7 +748,7 @@ Selenium.prototype.doAnswerOnNextPrompt = function(answer) { Selenium.prototype.doGoBack = function() { /** * Simulates the user clicking the "back" button on their browser. - * + * */ this.page().goBack(); }; @@ -489,23 +756,32 @@ Selenium.prototype.doGoBack = function() { Selenium.prototype.doRefresh = function() { /** * Simulates the user clicking the "Refresh" button on their browser. - * + * */ this.page().refresh(); }; Selenium.prototype.doClose = function() { - /** - * Simulates the user clicking the "close" button in the titlebar of a popup - * window or tab. - */ + /** + * Simulates the user clicking the "close" button in the titlebar of a popup + * window or tab. + */ this.page().close(); }; +Selenium.prototype.ensureNoUnhandledPopups = function() { + if (this.browserbot.hasAlerts()) { + throw new SeleniumError("There was an unexpected Alert! [" + this.browserbot.getNextAlert() + "]"); + } + if ( this.browserbot.hasConfirmations() ) { + throw new SeleniumError("There was an unexpected Confirmation! [" + this.browserbot.getNextConfirmation() + "]"); + } +}; + Selenium.prototype.isAlertPresent = function() { /** * Has an alert occurred? - * + * * <p> * This function never throws an exception * </p> @@ -513,10 +789,11 @@ Selenium.prototype.isAlertPresent = function() { */ return this.browserbot.hasAlerts(); }; + Selenium.prototype.isPromptPresent = function() { /** * Has a prompt occurred? - * + * * <p> * This function never throws an exception * </p> @@ -524,10 +801,11 @@ Selenium.prototype.isPromptPresent = function() { */ return this.browserbot.hasPrompts(); }; + Selenium.prototype.isConfirmationPresent = function() { /** * Has confirm() been called? - * + * * <p> * This function never throws an exception * </p> @@ -538,14 +816,14 @@ Selenium.prototype.isConfirmationPresent = function() { Selenium.prototype.getAlert = function() { /** * Retrieves the message of a JavaScript alert generated during the previous action, or fail if there were no alerts. - * + * * <p>Getting an alert has the same effect as manually clicking OK. If an * alert is generated but you do not get/verify it, the next Selenium action * will fail.</p> - * + * * <p>NOTE: under Selenium, JavaScript alerts will NOT pop up a visible alert * dialog.</p> - * + * * <p>NOTE: Selenium does NOT support JavaScript alerts that are generated in a * page's onload() event handler. In this case a visible dialog WILL be * generated and Selenium will hang until someone manually clicks OK.</p> @@ -562,26 +840,26 @@ Selenium.prototype.getConfirmation = function() { /** * Retrieves the message of a JavaScript confirmation dialog generated during * the previous action. - * + * * <p> * By default, the confirm function will return true, having the same effect * as manually clicking OK. This can be changed by prior execution of the * chooseCancelOnNextConfirmation command. If an confirmation is generated * but you do not get/verify it, the next Selenium action will fail. * </p> - * + * * <p> * NOTE: under Selenium, JavaScript confirmations will NOT pop up a visible * dialog. * </p> - * + * * <p> * NOTE: Selenium does NOT support JavaScript confirmations that are * generated in a page's onload() event handler. In this case a visible * dialog WILL be generated and Selenium will hang until you manually click * OK. * </p> - * + * * @return string the message of the most recent JavaScript confirmation dialog */ if (!this.browserbot.hasConfirmations()) { @@ -590,19 +868,19 @@ Selenium.prototype.getConfirmation = function() { return this.browserbot.getNextConfirmation(); }; Selenium.prototype.getConfirmation.dontCheckAlertsAndConfirms = true; - + Selenium.prototype.getPrompt = function() { /** * Retrieves the message of a JavaScript question prompt dialog generated during * the previous action. - * + * * <p>Successful handling of the prompt requires prior execution of the * answerOnNextPrompt command. If a prompt is generated but you * do not get/verify it, the next Selenium action will fail.</p> - * + * * <p>NOTE: under Selenium, JavaScript prompts will NOT pop up a visible * dialog.</p> - * + * * <p>NOTE: Selenium does NOT support JavaScript prompts that are generated in a * page's onload() event handler. In this case a visible dialog WILL be * generated and Selenium will hang until someone manually clicks OK.</p> @@ -616,18 +894,18 @@ Selenium.prototype.getPrompt = function() { Selenium.prototype.getLocation = function() { /** Gets the absolute URL of the current page. - * + * * @return string the absolute URL of the current page */ - return this.page().location; + return this.page().getCurrentWindow().location; }; Selenium.prototype.getTitle = function() { /** Gets the title of the current page. - * + * * @return string the title of the current page */ - return this.page().title(); + return this.page().getTitle(); }; @@ -645,7 +923,7 @@ Selenium.prototype.getValue = function(locator) { * Gets the (whitespace-trimmed) value of an input field (or anything else with a value parameter). * For checkbox/radio elements, the value will be "on" or "off" depending on * whether the element is checked or not. - * + * * @param locator an <a href="#locators">element locator</a> * @return string the element value, or "on/off" for checkbox/radio elements */ @@ -659,7 +937,7 @@ Selenium.prototype.getText = function(locator) { * text. This command uses either the textContent (Mozilla-like browsers) or * the innerText (IE-like browsers) of the element, which is the rendered * text shown to the user. - * + * * @param locator an <a href="#locators">element locator</a> * @return string the text of the element */ @@ -668,9 +946,9 @@ Selenium.prototype.getText = function(locator) { }; Selenium.prototype.getEval = function(script) { - /** Gets the result of evaluating the specified JavaScript snippet. The snippet may + /** Gets the result of evaluating the specified JavaScript snippet. The snippet may * have multiple lines, but only the result of the last line will be returned. - * + * * <p>Note that, by default, the snippet will run in the context of the "selenium" * object itself, so <code>this</code> will refer to the Selenium object, and <code>window</code> will * refer to the top-level runner test window, not the window of your application.</p> @@ -679,7 +957,7 @@ Selenium.prototype.getEval = function(script) { * to <code>this.browserbot.getCurrentWindow()</code> and if you need to use * a locator to refer to a single element in your application page, you can * use <code>this.page().findElement("foo")</code> where "foo" is your locator.</p> - * + * * @param script the JavaScript snippet to run * @return string the results of evaluating the snippet */ @@ -697,7 +975,7 @@ Selenium.prototype.isChecked = function(locator) { /** * Gets whether a toggle-button (checkbox/radio) is checked. Fails if the specified element doesn't exist or isn't a toggle-button. * @param locator an <a href="#locators">element locator</a> pointing to a checkbox or radio button - * @return string either "true" or "false" depending on whether the checkbox is checked + * @return boolean true if the checkbox is checked, false otherwise */ var element = this.page().findElement(locator); if (element.checked == null) { @@ -710,7 +988,7 @@ Selenium.prototype.getTable = function(tableCellAddress) { /** * Gets the text from a cell of a table. The cellAddress syntax * tableLocator.row.column, where row and column start at 0. - * + * * @param tableCellAddress a cell address, e.g. "foo.1.4" * @return string the text from the specified cell */ @@ -742,24 +1020,6 @@ Selenium.prototype.getTable = function(tableCellAddress) { return null; }; -Selenium.prototype.assertSelected = function(selectLocator, optionLocator) { - /** - * Verifies that the selected option of a drop-down satisfies the optionSpecifier. - * - * <p>See the select command for more information about option locators.</p> - * - * @param selectLocator an <a href="#locators">element locator</a> identifying a drop-down menu - * @param optionLocator an option locator, typically just an option label (e.g. "John Smith") - */ - var element = this.page().findElement(selectLocator); - var locator = this.optionLocatorFactory.fromLocatorString(optionLocator); - if (element.selectedIndex == -1) - { - Assert.fail("No option selected"); - } - locator.assertSelected(element); -}; - Selenium.prototype.getSelectedLabels = function(selectLocator) { /** Gets all option labels (visible text) for selected options in the specified select or multi-select element. * @@ -842,9 +1102,9 @@ Selenium.prototype.isSomethingSelected = function(selectLocator) { if (!("options" in element)) { throw new SeleniumError("Specified element is not a Select (has no options)"); } - + var selectedOptions = []; - + for (var i = 0; i < element.options.length; i++) { if (element.options[i].selected) { @@ -886,7 +1146,7 @@ Selenium.prototype.findSelectedOptionProperty = function(locator, property) { Selenium.prototype.getSelectOptions = function(selectLocator) { /** Gets all option labels in the specified select drop-down. - * + * * @param selectLocator an <a href="#locators">element locator</a> identifying a drop-down menu * @return string[] an array of all option labels in the specified select drop-down */ @@ -898,7 +1158,7 @@ Selenium.prototype.getSelectOptions = function(selectLocator) { var option = element.options[i].text.replace(/,/g, "\\,"); selectOptions.push(option); } - + return selectOptions.join(","); }; @@ -906,6 +1166,10 @@ Selenium.prototype.getSelectOptions = function(selectLocator) { Selenium.prototype.getAttribute = function(attributeLocator) { /** * Gets the value of an element attribute. + * + * Beware of http://jira.openqa.org/browse/SEL-280, which will lead some event handlers to + * get null event arguments. Read the bug for more details, including a workaround. + * * @param attributeLocator an element locator followed by an @ sign and then the name of the attribute, e.g. "foo@bar" * @return string the value of the specified attribute */ @@ -924,15 +1188,15 @@ Selenium.prototype.isTextPresent = function(pattern) { */ var allText = this.page().bodyText(); - if(allText == "") { - Assert.fail("Page text not found"); - } else { - var patternMatcher = new PatternMatcher(pattern); - if (patternMatcher.strategy == PatternMatcher.strategies.glob) { - patternMatcher.matcher = new PatternMatcher.strategies.globContains(pattern); - } - return patternMatcher.matches(allText); + var patternMatcher = new PatternMatcher(pattern); + if (patternMatcher.strategy == PatternMatcher.strategies.glob) { + patternMatcher.matcher = new PatternMatcher.strategies.globContains(pattern); + } + else if (patternMatcher.strategy == PatternMatcher.strategies.exact) { + pattern = pattern.substring("exact:".length); // strip off "exact:" + return allText.indexOf(pattern) != -1; } + return patternMatcher.matches(allText); }; Selenium.prototype.isElementPresent = function(locator) { @@ -956,19 +1220,14 @@ Selenium.prototype.isVisible = function(locator) { * property to "hidden", or the "display" property to "none", either for the * element itself or one if its ancestors. This method will fail if * the element is not present. - * + * * @param locator an <a href="#locators">element locator</a> * @return boolean true if the specified element is visible, false otherwise */ var element; - element = this.page().findElement(locator); - - if(/Konqueror|Safari|KHTML/.test(navigator.userAgent)) - var visibility = element.style["visibility"]; - else - var visibility = this.findEffectiveStyleProperty(element, "visibility"); - - var _isDisplayed = this._isDisplayed(element); + element = this.page().findElement(locator); + var visibility = this.findEffectiveStyleProperty(element, "visibility"); + var _isDisplayed = this._isDisplayed(element); return (visibility != "hidden" && _isDisplayed); }; @@ -982,10 +1241,7 @@ Selenium.prototype.findEffectiveStyleProperty = function(element, property) { }; Selenium.prototype._isDisplayed = function(element) { - if(/Konqueror|Safari|KHTML/.test(navigator.userAgent)) - var display = element.style["display"]; - else - var display = this.findEffectiveStyleProperty(element, "display"); + var display = this.findEffectiveStyleProperty(element, "display"); if (display == "none") return false; if (element.parentNode.style) { return this._isDisplayed(element.parentNode); @@ -997,8 +1253,8 @@ Selenium.prototype.findEffectiveStyle = function(element) { if (element.style == undefined) { return undefined; // not a styled element } - var window = this.browserbot.getContentWindow(); - if (window.getComputedStyle) { + var window = this.browserbot.getCurrentWindow(); + if (window.getComputedStyle) { // DOM-Level-2-CSS return window.getComputedStyle(element, null); } @@ -1016,7 +1272,7 @@ Selenium.prototype.isEditable = function(locator) { /** * Determines whether the specified input element is editable, ie hasn't been disabled. * This method will fail if the specified element isn't an input element. - * + * * @param locator an <a href="#locators">element locator</a> * @return boolean true if the input element is editable, false otherwise */ @@ -1029,9 +1285,9 @@ Selenium.prototype.isEditable = function(locator) { Selenium.prototype.getAllButtons = function() { /** Returns the IDs of all buttons on the page. - * + * * <p>If a given button has no ID, it will appear as "" in this array.</p> - * + * * @return string[] the IDs of all buttons on the page */ return this.page().getAllButtons(); @@ -1039,9 +1295,9 @@ Selenium.prototype.getAllButtons = function() { Selenium.prototype.getAllLinks = function() { /** Returns the IDs of all links on the page. - * + * * <p>If a given link has no ID, it will appear as "" in this array.</p> - * + * * @return string[] the IDs of all links on the page */ return this.page().getAllLinks(); @@ -1049,28 +1305,175 @@ Selenium.prototype.getAllLinks = function() { Selenium.prototype.getAllFields = function() { /** Returns the IDs of all input fields on the page. - * + * * <p>If a given field has no ID, it will appear as "" in this array.</p> - * + * * @return string[] the IDs of all field on the page */ return this.page().getAllFields(); }; +Selenium.prototype._getTestAppParentOfAllWindows = function() { + /** Returns the IDs of all input fields on the page. + * + * <p>If a given field has no ID, it will appear as "" in this array.</p> + * + * @return string[] the IDs of all field on the page + */ + if (this.browserbot.getCurrentWindow().opener!=null) { + return this.browserbot.getCurrentWindow().opener; + } + if (this.browserbot.buttonWindow!=null) { + return this.browserbot.buttonWindow; + } + return top; // apparently we are in proxy injection mode +}; + +Selenium.prototype.getAttributeFromAllWindows = function(attributeName) { + /** Returns every instance of some attribute from all known windows. + * + * @param attributeName name of an attribute on the windows + * @return string[] the set of values of this attribute from all known windows. + */ + var attributes = new Array(); + var testAppParentOfAllWindows = this._getTestAppParentOfAllWindows(); + attributes.push(eval("testAppParentOfAllWindows." + attributeName)); + var selenium = testAppParentOfAllWindows.selenium==null ? testAppParentOfAllWindows.parent.selenium : testAppParentOfAllWindows.selenium; + for (windowName in selenium.browserbot.openedWindows) + { + attributes.push(eval("selenium.browserbot.openedWindows[windowName]." + attributeName)); + } + return attributes; +}; + +Selenium.prototype.findWindow = function(soughtAfterWindowPropertyValue) { + var testAppParentOfAllWindows = this._getTestAppParentOfAllWindows(); + var targetPropertyName = "name"; + if (soughtAfterWindowPropertyValue.match("^title=")) { + targetPropertyName = "document.title"; + soughtAfterWindowPropertyValue = soughtAfterWindowPropertyValue.replace(/^title=/, ""); + } + else { + // matching "name": + // If we are not in proxy injection mode, then the top-level test window will be named myiframe. + // But as far as the interface goes, we are expected to match a blank string to this window, if + // we are searching with respect to the widow name. + // So make a special case so that this logic will work: + if (PatternMatcher.matches(soughtAfterWindowPropertyValue, "")) { + return this.browserbot.getCurrentWindow(); + } + } + + if (PatternMatcher.matches(soughtAfterWindowPropertyValue, eval("testAppParentOfAllWindows." + targetPropertyName))) { + return testAppParentOfAllWindows; + } + for (windowName in selenium.browserbot.openedWindows) { + var openedWindow = selenium.browserbot.openedWindows[windowName]; + if (PatternMatcher.matches(soughtAfterWindowPropertyValue, eval("openedWindow." + targetPropertyName))) { + return openedWindow; + } + } + throw new SeleniumError("could not find window with property " + targetPropertyName + " matching " + soughtAfterWindowPropertyValue); +}; + +Selenium.prototype.doDragdrop = function(locator, movementsString) { + /** Drags an element a certain distance and then drops it + * Beware of http://jira.openqa.org/browse/SEL-280, which will lead some event handlers to + * get null event arguments. Read the bug for more details, including a workaround. + * + * @param movementsString offset in pixels from the current location to which the element should be moved, e.g., "+70,-300" + * @param locator an element locator + */ + var element = this.page().findElement(locator); + var clientStartXY = getClientXY(element) + var clientStartX = clientStartXY[0]; + var clientStartY = clientStartXY[1]; + + var movements = movementsString.split(/,/); + var movementX = Number(movements[0]); + var movementY = Number(movements[1]); + + var clientFinishX = ((clientStartX + movementX) < 0) ? 0 : (clientStartX + movementX); + var clientFinishY = ((clientStartY + movementY) < 0) ? 0 : (clientStartY + movementY); + + var movementXincrement = (movementX > 0) ? 1 : -1; + var movementYincrement = (movementY > 0) ? 1 : -1; + + triggerMouseEvent(element, 'mousedown', true, clientStartX, clientStartY); + var clientX = clientStartX; + var clientY = clientStartY; + while ((clientX != clientFinishX) || (clientY != clientFinishY)) { + if (clientX != clientFinishX) { + clientX += movementXincrement; + } + if (clientY != clientFinishY) { + clientY += movementYincrement; + } + triggerMouseEvent(element, 'mousemove', true, clientX, clientY); + } + triggerMouseEvent(element, 'mouseup', true, clientFinishX, clientFinishY); +}; + +Selenium.prototype.doWindowFocus = function(windowName) { +/** Gives focus to a window + * + * @param windowName name of the window to be given focus + */ + this.findWindow(windowName).focus(); +}; + + +Selenium.prototype.doWindowMaximize = function(windowName) { +/** Resize window to take up the entire screen + * + * @param windowName name of the window to be enlarged + */ + var window = this.findWindow(windowName); + if (window!=null && window.screen) { + window.moveTo(0,0); + window.outerHeight = screen.availHeight; + window.outerWidth = screen.availWidth; + } +}; + +Selenium.prototype.getAllWindowIds = function() { + /** Returns the IDs of all windows that the browser knows about. + * + * @return string[] the IDs of all windows that the browser knows about. + */ + return this.getAttributeFromAllWindows("id"); +}; + +Selenium.prototype.getAllWindowNames = function() { + /** Returns the names of all windows that the browser knows about. + * + * @return string[] the names of all windows that the browser knows about. + */ + return this.getAttributeFromAllWindows("name"); +}; + +Selenium.prototype.getAllWindowTitles = function() { + /** Returns the titles of all windows that the browser knows about. + * + * @return string[] the titles of all windows that the browser knows about. + */ + return this.getAttributeFromAllWindows("document.title"); +}; + Selenium.prototype.getHtmlSource = function() { /** Returns the entire HTML source between the opening and * closing "html" tags. * * @return string the entire HTML source */ - return this.page().currentDocument.getElementsByTagName("html")[0].innerHTML; + return this.page().document().getElementsByTagName("html")[0].innerHTML; }; Selenium.prototype.doSetCursorPosition = function(locator, position) { /** * Moves the text cursor to the specified position in the given input element or textarea. * This method will fail if the specified element isn't an input element or textarea. - * + * * @param locator an <a href="#locators">element locator</a> pointing to an input element or textarea * @param position the numerical position of the cursor in the field; position should be 0 to move the position to the beginning of the field. You can also set the cursor to -1 to move it to the end of the field. */ @@ -1081,13 +1484,13 @@ Selenium.prototype.doSetCursorPosition = function(locator, position) { if (position == -1) { position = element.value.length; } - + if( element.setSelectionRange && !browserVersion.isOpera) { element.focus(); - element.setSelectionRange(/*start*/position,/*end*/position); - } + element.setSelectionRange(/*start*/position,/*end*/position); + } else if( element.createTextRange ) { - triggerEvent(element, 'focus', false); + triggerEvent(element, 'focus', false); var range = element.createTextRange(); range.collapse(true); range.moveEnd('character',position); @@ -1096,22 +1499,205 @@ Selenium.prototype.doSetCursorPosition = function(locator, position) { } } +Selenium.prototype.getElementIndex = function(locator) { + /** + * Get the relative index of an element to its parent (starting from 0). The comment node and empty text node + * will be ignored. + * + * @param locator an <a href="#locators">element locator</a> pointing to an element + * @return number of relative index of the element to its parent (starting from 0) + */ + var element = this.page().findElement(locator); + var previousSibling; + var index = 0; + while ((previousSibling = element.previousSibling) != null) { + if (!this._isCommentOrEmptyTextNode(previousSibling)) { + index++; + } + element = previousSibling; + } + return index; +} + +Selenium.prototype.isOrdered = function(locator1, locator2) { + /** + * Check if these two elements have same parent and are ordered. Two same elements will + * not be considered ordered. + * + * @param locator1 an <a href="#locators">element locator</a> pointing to the first element + * @param locator2 an <a href="#locators">element locator</a> pointing to the second element + * @return boolean true if two elements are ordered and have same parent, false otherwise + */ + var element1 = this.page().findElement(locator1); + var element2 = this.page().findElement(locator2); + if (element1 === element2) return false; + + var previousSibling; + while ((previousSibling = element2.previousSibling) != null) { + if (previousSibling === element1) { + return true; + } + element2 = previousSibling; + } + return false; +} + +Selenium.prototype._isCommentOrEmptyTextNode = function(node) { + return node.nodeType == 8 || ((node.nodeType == 3) && !(/[^\t\n\r ]/.test(node.data))); +} + +Selenium.prototype.getElementPositionLeft = function(locator) { + /** + * Retrieves the horizontal position of an element + * + * @param locator an <a href="#locators">element locator</a> pointing to an element OR an element itself + * @return number of pixels from the edge of the frame. + */ + var element; + if ("string"==typeof locator) { + element = this.page().findElement(locator); + } + else { + element = locator; + } + var x = element.offsetLeft; + var elementParent = element.offsetParent; + + while (elementParent != null) + { + if(document.all) + { + if( (elementParent.tagName != "TABLE") && (elementParent.tagName != "BODY") ) + { + x += elementParent.clientLeft; + } + } + else // Netscape/DOM + { + if(elementParent.tagName == "TABLE") + { + var parentBorder = parseInt(elementParent.border); + if(isNaN(parentBorder)) + { + var parentFrame = elementParent.getAttribute('frame'); + if(parentFrame != null) + { + x += 1; + } + } + else if(parentBorder > 0) + { + x += parentBorder; + } + } + } + x += elementParent.offsetLeft; + elementParent = elementParent.offsetParent; + } + return x; +}; + +Selenium.prototype.getElementPositionTop = function(locator) { + /** + * Retrieves the vertical position of an element + * + * @param locator an <a href="#locators">element locator</a> pointing to an element OR an element itself + * @return number of pixels from the edge of the frame. + */ + var element; + if ("string"==typeof locator) { + element = this.page().findElement(locator); + } + else { + element = locator; + } + + var y = 0; + + while (element != null) + { + if(document.all) + { + if( (element.tagName != "TABLE") && (element.tagName != "BODY") ) + { + y += element.clientTop; + } + } + else // Netscape/DOM + { + if(element.tagName == "TABLE") + { + var parentBorder = parseInt(element.border); + if(isNaN(parentBorder)) + { + var parentFrame = element.getAttribute('frame'); + if(parentFrame != null) + { + y += 1; + } + } + else if(parentBorder > 0) + { + y += parentBorder; + } + } + } + y += element.offsetTop; + + // Netscape can get confused in some cases, such that the height of the parent is smaller + // than that of the element (which it shouldn't really be). If this is the case, we need to + // exclude this element, since it will result in too large a 'top' return value. + if (element.offsetParent && element.offsetParent.offsetHeight && element.offsetParent.offsetHeight < element.offsetHeight) + { + // skip the parent that's too small + element = element.offsetParent.offsetParent; + } + else + { + // Next up... + element = element.offsetParent; + } + } + return y; +}; + +Selenium.prototype.getElementWidth = function(locator) { + /** + * Retrieves the width of an element + * + * @param locator an <a href="#locators">element locator</a> pointing to an element + * @return number width of an element in pixels + */ + var element = this.page().findElement(locator); + return element.offsetWidth; +}; + +Selenium.prototype.getElementHeight = function(locator) { + /** + * Retrieves the height of an element + * + * @param locator an <a href="#locators">element locator</a> pointing to an element + * @return number height of an element in pixels + */ + var element = this.page().findElement(locator); + return element.offsetHeight; +}; + Selenium.prototype.getCursorPosition = function(locator) { /** * Retrieves the text cursor position in the given input element or textarea; beware, this may not work perfectly on all browsers. - * + * * <p>Specifically, if the cursor/selection has been cleared by JavaScript, this command will tend to * return the position of the last location of the cursor, even though the cursor is now gone from the page. This is filed as <a href="http://jira.openqa.org/browse/SEL-243">SEL-243</a>.</p> * This method will fail if the specified element isn't an input element or textarea, or there is no cursor in the element. - * + * * @param locator an <a href="#locators">element locator</a> pointing to an input element or textarea * @return number the numerical position of the cursor in the field */ var element = this.page().findElement(locator); - var doc = this.page().currentDocument; + var doc = this.page().getDocument(); var win = this.browserbot.getCurrentWindow(); if( doc.selection && !browserVersion.isOpera){ - var selectRange = doc.selection.createRange().duplicate(); var elementRange = element.createTextRange(); selectRange.move("character",0); @@ -1126,28 +1712,28 @@ Selenium.prototype.getCursorPosition = function(locator) { var answer = String(elementRange.text).replace(/\r/g,"").length; return answer; } else { - if (typeof(element.selectionStart) != undefined) { + if (typeof(element.selectionStart) != "undefined") { if (win.getSelection && typeof(win.getSelection().rangeCount) != undefined && win.getSelection().rangeCount == 0) { Assert.fail("There is no cursor on this page!"); } return element.selectionStart; - } + } } throw new Error("Couldn't detect cursor position on this browser!"); } - + Selenium.prototype.doSetContext = function(context, logLevelThreshold) { /** * Writes a message to the status bar and adds a note to the browser-side * log. - * + * * <p>If logLevelThreshold is specified, set the threshold for logging * to that level (debug, info, warn, error).</p> - * + * * <p>(Note that the browser-side logs will <i>not</i> be sent back to the * server, and are invisible to the Client Driver.)</p> - * + * * @param context * the message to be sent to the browser * @param logLevelThreshold one of "debug", "info", "warn", "error", sets the threshold for browser-side logging @@ -1163,8 +1749,8 @@ Selenium.prototype.getExpression = function(expression) { * Returns the specified expression. * * <p>This is useful because of JavaScript preprocessing. - * It is used to generate commands like assertExpression and storeExpression.</p> - * + * It is used to generate commands like assertExpression and waitForExpression.</p> + * * @param expression the value to return * @return string the value passed in */ @@ -1176,7 +1762,7 @@ Selenium.prototype.doWaitForCondition = function(script, timeout) { * Runs the specified JavaScript snippet repeatedly until it evaluates to "true". * The snippet may have multiple lines, but only the result of the last line * will be considered. - * + * * <p>Note that, by default, the snippet will be run in the runner's test window, not in the window * of your application. To get the window of your application, you can use * the JavaScript snippet <code>selenium.browserbot.getCurrentWindow()</code>, and then @@ -1187,13 +1773,9 @@ Selenium.prototype.doWaitForCondition = function(script, timeout) { if (isNaN(timeout)) { throw new SeleniumError("Timeout is not a number: " + timeout); } - - testLoop.waitForCondition = function () { + return decorateFunctionWithTimeout(function () { return eval(script); - }; - - testLoop.waitForConditionStart = new Date().getTime(); - testLoop.waitForConditionTimeout = timeout; + }, timeout); }; Selenium.prototype.doWaitForCondition.dontCheckAlertsAndConfirms = true; @@ -1206,23 +1788,33 @@ Selenium.prototype.doSetTimeout = function(timeout) { * The default timeout is 30 seconds. * @param timeout a timeout in milliseconds, after which the action will return with an error */ - testLoop.waitForConditionTimeout = timeout; + this.defaultTimeout = parseInt(timeout); } Selenium.prototype.doWaitForPageToLoad = function(timeout) { /** * Waits for a new page to load. - * + * * <p>You can use this command instead of the "AndWait" suffixes, "clickAndWait", "selectAndWait", "typeAndWait" etc. * (which are only available in the JS API).</p> - * + * * <p>Selenium constantly keeps track of new pages loading, and sets a "newPageLoaded" * flag when it first notices a page load. Running any other Selenium command after * turns the flag to false. Hence, if you want to wait for a page to load, you must * wait immediately after a Selenium command that caused a page-load.</p> * @param timeout a timeout in milliseconds, after which this command will return with an error */ - this.doWaitForCondition("selenium.browserbot.isNewPageLoaded()", timeout); + if (isNaN(timeout)) { + throw new SeleniumError("Timeout is not a number: " + timeout); + } + // in pi-mode, the test and the harness share the window; thus if we are executing this code, then we have loaded + if (window["proxyInjectionMode"] == null || !window["proxyInjectionMode"]) { + return this.makePageLoadCondition(timeout); + } +}; + +Selenium.prototype._isNewPageLoaded = function() { + return this.browserbot.isNewPageLoaded(); }; Selenium.prototype.doWaitForPageToLoad.dontCheckAlertsAndConfirms = true; @@ -1264,6 +1856,55 @@ Selenium.prototype.replaceVariables = function(str) { return stringResult; }; +Selenium.prototype.getCookie = function() { + /** + * Return all cookies of the current page under test. + * + * @return string all cookies of the current page under test + */ + var doc = this.page().document(); + return doc.cookie; +}; + +Selenium.prototype.doCreateCookie = function(nameValuePair, optionsString) { + /** + * Create a new cookie whose path and domain are same with those of current page + * under test, unless you specified a path for this cookie explicitly. + * + * @param nameValuePair name and value of the cookie in a format "name=value" + * @param optionsString options for the cookie. Currently supported options include 'path' and 'max_age'. + * the optionsString's format is "path=/path/, max_age=60". The order of options are irrelevant, the unit + * of the value of 'max_age' is second. + */ + var results = /[^\s=\[\]\(\),"\/\?@:;]+=[^\s=\[\]\(\),"\/\?@:;]*/.test(nameValuePair); + if (!results) { + throw new SeleniumError("Invalid parameter."); + } + var cookie = nameValuePair.trim(); + results = /max_age=(\d+)/.exec(optionsString); + if (results) { + var expireDateInMilliseconds = (new Date()).getTime() + results[1] * 1000; + cookie += "; expires=" + new Date(expireDateInMilliseconds).toGMTString(); + } + results = /path=([^\s,]+)[,]?/.exec(optionsString); + if (results) { + cookie += "; path=" + results[1]; + } + this.page().document().cookie = cookie; +} + +Selenium.prototype.doDeleteCookie = function(name,path) { + /** + * Delete a named cookie with specified path. + * + * @param name the name of the cookie to be deleted + * @param path the path property of the cookie to be deleted + */ + // set the expire time of the cookie to be deleted to one minute before now. + var expireDateInMilliseconds = (new Date()).getTime() + (-1 * 1000); + this.page().document().cookie = name.trim() + "=deleted; path=" + path.trim() + "; expires=" + new Date(expireDateInMilliseconds).toGMTString(); +} + /** * Factory for creating "Option Locators". @@ -1398,5 +2039,3 @@ OptionLocatorFactory.prototype.OptionLocatorById = function(id) { Assert.matches(this.id, selectedId) }; }; - - diff --git a/tests/test_tools/selenium/core/scripts/selenium-browserbot.js b/tests/test_tools/selenium/core/scripts/selenium-browserbot.js index 8df46865..22df0fdb 100644 --- a/tests/test_tools/selenium/core/scripts/selenium-browserbot.js +++ b/tests/test_tools/selenium/core/scripts/selenium-browserbot.js @@ -27,9 +27,17 @@ // The window to which the commands will be sent. For example, to click on a // popup window, first select that window, and then do a normal click command. -var BrowserBot = function(frame) { - this.frame = frame; +var BrowserBot = function(topLevelApplicationWindow) { + this.topWindow = topLevelApplicationWindow; + + // the buttonWindow is the Selenium window + // it contains the Run/Pause buttons... this should *not* be the AUT window + // todo: Here the buttonWindow is not Selenium window. It will be set to Selenium window in pollForLoad. + // Change this!!! + this.buttonWindow = this.topWindow; + // not sure what this is used for this.currentPage = null; + this.currentWindow = this.topWindow; this.currentWindowName = null; this.modalDialogTest = null; @@ -42,49 +50,60 @@ var BrowserBot = function(frame) { this.newPageLoaded = false; this.pageLoadError = null; + this.uniqueId = new Date().getTime(); + this.pollingForLoad = new Object(); + this.windowPollers = new Array(); + var self = this; this.recordPageLoad = function() { - LOG.debug("Page load detected"); + LOG.debug("Page load detected"); try { - LOG.debug("Page load location=" + self.getCurrentWindow().location); + LOG.debug("Page load location=" + self.getCurrentWindow(true).location); } catch (e) { - self.pageLoadError = e; - return; + self.pageLoadError = e; + return; } self.currentPage = null; self.newPageLoaded = true; }; this.isNewPageLoaded = function() { - if (this.pageLoadError) throw this.pageLoadError; + if (this.pageLoadError) { + var e = this.pageLoadError; + this.pageLoadError = null; + throw e; + } return self.newPageLoaded; }; }; -BrowserBot.createForFrame = function(frame) { +BrowserBot.createForWindow = function(window) { var browserbot; + LOG.debug('createForWindow'); LOG.debug("browserName: " + browserVersion.name); LOG.debug("userAgent: " + navigator.userAgent); if (browserVersion.isIE) { - browserbot = new IEBrowserBot(frame); + browserbot = new IEBrowserBot(window); } else if (browserVersion.isKonqueror) { - browserbot = new KonquerorBrowserBot(frame); + browserbot = new KonquerorBrowserBot(window); + } + else if (browserVersion.isOpera) { + browserbot = new OperaBrowserBot(window); } else if (browserVersion.isSafari) { - browserbot = new SafariBrowserBot(frame); + browserbot = new SafariBrowserBot(window); } else { - LOG.info("Using MozillaBrowserBot") // Use mozilla by default - browserbot = new MozillaBrowserBot(frame); + browserbot = new MozillaBrowserBot(window); } - - // Modify the test IFrame so that page loads are detected. - addLoadListener(browserbot.getFrame(), browserbot.recordPageLoad); + browserbot.getCurrentWindow(); + // todo: why? return browserbot; }; +// todo: rename? This doesn't actually "do" anything. BrowserBot.prototype.doModalDialogTest = function(test) { this.modalDialogTest = test; }; @@ -98,67 +117,138 @@ BrowserBot.prototype.setNextPromptResult = function(result) { }; BrowserBot.prototype.hasAlerts = function() { - return (this.recordedAlerts.length > 0) ; + return (this.recordedAlerts.length > 0); }; +BrowserBot.prototype.relayBotToRC = function() { +}; +// override in injection.html + +BrowserBot.prototype.resetPopups = function() { + this.recordedAlerts = []; + this.recordedConfirmations = []; + this.recordedPrompts = []; +} + BrowserBot.prototype.getNextAlert = function() { - return this.recordedAlerts.shift(); + var t = this.recordedAlerts.shift(); + this.relayBotToRC("browserbot.recordedAlerts"); + return t; }; BrowserBot.prototype.hasConfirmations = function() { - return (this.recordedConfirmations.length > 0) ; + return (this.recordedConfirmations.length > 0); }; BrowserBot.prototype.getNextConfirmation = function() { - return this.recordedConfirmations.shift(); + var t = this.recordedConfirmations.shift(); + this.relayBotToRC("browserbot.recordedConfirmations"); + return t; }; BrowserBot.prototype.hasPrompts = function() { - return (this.recordedPrompts.length > 0) ; + return (this.recordedPrompts.length > 0); }; BrowserBot.prototype.getNextPrompt = function() { - return this.recordedPrompts.shift(); + var t = this.recordedPrompts.shift(); + this.relayBotToRC("browserbot.recordedPrompts"); + return t; }; -BrowserBot.prototype.getFrame = function() { - return this.frame; +BrowserBot.prototype._windowClosed = function(win) { + var c = win.closed; + if (c == null) return true; + return c; +}; + +BrowserBot.prototype._modifyWindow = function(win) { + if (this._windowClosed(win)) { + LOG.error("modifyWindow: Window was closed!"); + return null; + } + LOG.debug('modifyWindow ' + this.uniqueId + ":" + win[this.uniqueId]); + if (!win[this.uniqueId]) { + win[this.uniqueId] = true; + this.modifyWindowToRecordPopUpDialogs(win, this); + this.currentPage = PageBot.createForWindow(this); + this.newPageLoaded = false; + } + this.modifySeparateTestWindowToDetectPageLoads(win); + return win; }; BrowserBot.prototype.selectWindow = function(target) { // we've moved to a new page - clear the current one this.currentPage = null; - this.currentWindowName = null; + if (target && target != "null") { - // If window exists - if (this.getTargetWindow(target)) { - this.currentWindowName = target; + this._selectWindowByName(target); + } else { + this._selectTopWindow(); + } +}; + +BrowserBot.prototype._selectTopWindow = function() { + this.currentWindowName = null; + this.currentWindow = this.topWindow; +} + +BrowserBot.prototype._selectWindowByName = function(target) { + this.currentWindow = this.getWindowByName(target, false); + this.currentWindowName = target; +} + +BrowserBot.prototype.selectFrame = function(target) { + if (target == "relative=up") { + this.currentWindow = this.getCurrentWindow().parent; + } else if (target == "relative=top") { + this.currentWindow = this.topWindow; + } else { + var frame = this.getCurrentPage().findElement(target); + if (frame == null) { + throw new SeleniumError("Not found: " + target); + } + // now, did they give us a frame or a frame ELEMENT? + if (frame.contentWindow) { + // this must be a frame element + this.currentWindow = frame.contentWindow; + } else if (frame.document) { + // must be an actual window frame + this.currentWindow = frame; + } else { + // neither + throw new SeleniumError("Not a frame: " + target); } } + this.currentPage = null; }; BrowserBot.prototype.openLocation = function(target) { // We're moving to a new page - clear the current one + var win = this.getCurrentWindow(); + LOG.debug("openLocation newPageLoaded = false"); this.currentPage = null; this.newPageLoaded = false; - this.setOpenLocation(target); + this.setOpenLocation(win, target); }; BrowserBot.prototype.setIFrameLocation = function(iframe, location) { iframe.src = location; }; -BrowserBot.prototype.setOpenLocation = function(location) { - this.getCurrentWindow().location.href = location; +BrowserBot.prototype.setOpenLocation = function(win, loc) { + + // is there a Permission Denied risk here? setting a timeout breaks Firefox + //win.setTimeout(function() { win.location.href = loc; }, 0); + win.location.href = loc; }; BrowserBot.prototype.getCurrentPage = function() { if (this.currentPage == null) { var testWindow = this.getCurrentWindow(); - this.modifyWindowToRecordPopUpDialogs(testWindow, this); - this.modifySeparateTestWindowToDetectPageLoads(testWindow); - this.currentPage = PageBot.createForWindow(testWindow); + this.currentPage = PageBot.createForWindow(this); this.newPageLoaded = false; } @@ -166,14 +256,18 @@ BrowserBot.prototype.getCurrentPage = function() { }; BrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToModify, browserBot) { + var self = this; + windowToModify.alert = function(alert) { browserBot.recordedAlerts.push(alert); + self.relayBotToRC("browserbot.recordedAlerts"); }; windowToModify.confirm = function(message) { browserBot.recordedConfirmations.push(message); var result = browserBot.nextConfirmResult; browserBot.nextConfirmResult = true; + self.relayBotToRC("browserbot.recordedConfirmations"); return result; }; @@ -182,6 +276,7 @@ BrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToModify, var result = !browserBot.nextConfirmResult ? null : browserBot.nextPromptResult; browserBot.nextConfirmResult = true; browserBot.nextPromptResult = ''; + self.relayBotToRC("browserbot.recordedPrompts"); return result; }; @@ -196,102 +291,249 @@ BrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToModify, }; /** - * The main IFrame has a single, long-lived onload handler that clears - * Browserbot.currentPage and sets the "newPageLoaded" flag. For separate - * windows, we need to attach a handler each time. This uses the - * "callOnWindowPageTransition" mechanism, which is implemented differently - * for different browsers. - */ -BrowserBot.prototype.modifySeparateTestWindowToDetectPageLoads = function(windowToModify) { - if (this.currentWindowName != null) { - this.callOnWindowPageTransition(this.recordPageLoad, windowToModify); - } -}; - -/** * Call the supplied function when a the current page unloads and a new one loads. * This is done by polling continuously until the document changes and is fully loaded. */ -BrowserBot.prototype.callOnWindowPageTransition = function(loadFunction, windowObject) { +BrowserBot.prototype.modifySeparateTestWindowToDetectPageLoads = function(windowObject) { // Since the unload event doesn't fire in Safari 1.3, we start polling immediately - if (windowObject && !windowObject.closed) { - LOG.debug("Starting pollForLoad: " + windowObject.document.location); - this.pollingForLoad = true; - this.pollForLoad(loadFunction, windowObject, windowObject.location, windowObject.location.href); + if (!windowObject) { + LOG.warn("modifySeparateTestWindowToDetectPageLoads: no windowObject!"); + return; + } + if (this._windowClosed(windowObject)) { + LOG.info("modifySeparateTestWindowToDetectPageLoads: windowObject was closed"); + return; + } + var oldMarker = this.isPollingForLoad(windowObject); + if (oldMarker) { + LOG.debug("modifySeparateTestWindowToDetectPageLoads: already polling this window: " + oldMarker); + return; + } + + var marker = 'selenium' + new Date().getTime(); + LOG.debug("Starting pollForLoad (" + marker + "): " + windowObject.document.location); + this.pollingForLoad[marker] = true; + // if this is a frame, add a load listener, otherwise, attach a poller + if (this._getFrameElement(windowObject)) { + LOG.debug("modifySeparateTestWindowToDetectPageLoads: this window is a frame; attaching a load listener"); + addLoadListener(windowObject.frameElement, this.recordPageLoad); + windowObject.frameElement[marker] = true; + windowObject.frameElement[this.uniqueId] = marker; + } else { + windowObject.document.location[marker] = true; + windowObject[this.uniqueId] = marker; + this.pollForLoad(this.recordPageLoad, windowObject, windowObject.document, windowObject.location, windowObject.location.href, marker); } }; +BrowserBot.prototype._getFrameElement = function(win) { + var frameElement = null; + try { + frameElement = win.frameElement; + } catch (e) { + } // on IE, checking frameElement on a pop-up results in a "No such interface supported" exception + return frameElement; +} + /** * Set up a polling timer that will keep checking the readyState of the document until it's complete. * Since we might call this before the original page is unloaded, we first check to see that the current location * or href is different from the original one. */ -BrowserBot.prototype.pollForLoad = function(loadFunction, windowObject, originalLocation, originalHref) { - var windowClosed = true; +BrowserBot.prototype.pollForLoad = function(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker) { + LOG.debug("pollForLoad original (" + marker + "): " + originalHref); + try { - windowClosed = windowObject.closed; + if (this._windowClosed(windowObject)) { + LOG.debug("pollForLoad WINDOW CLOSED (" + marker + ")"); + delete this.pollingForLoad[marker]; + return; + } + // todo: Change this!!! + // under multi-window layout, buttonWindow should be TestRunner window + // but only after the _windowClosed checking, we can ensure that this.topWindow exists + // then we can assign the TestRunner window to buttonWindow + this.buttonWindow = windowObject.opener; + + var isSamePage = this._isSamePage(windowObject, originalDocument, originalLocation, originalHref, marker); + var rs = this.getReadyState(windowObject, windowObject.document); + + if (!isSamePage && rs == 'complete') { + var currentHref = windowObject.location.href; + LOG.debug("pollForLoad FINISHED (" + marker + "): " + rs + " (" + currentHref + ")"); + delete this.pollingForLoad[marker]; + this._modifyWindow(windowObject); + var newMarker = this.isPollingForLoad(windowObject); + if (!newMarker) { + LOG.debug("modifyWindow didn't start new poller: " + newMarker); + this.modifySeparateTestWindowToDetectPageLoads(windowObject); + } + newMarker = this.isPollingForLoad(windowObject); + LOG.debug("pollForLoad (" + marker + ") restarting " + newMarker); + if (/(TestRunner-splash|Blank)\.html\?start=true$/.test(currentHref)) { + LOG.debug("pollForLoad Oh, it's just the starting page. Never mind!"); + } else if (this.currentWindow[this.uniqueId] == newMarker) { + loadFunction(); + } else { + LOG.debug("pollForLoad page load detected in non-current window; ignoring"); + } + return; + } + LOG.debug("pollForLoad continue (" + marker + "): " + currentHref); + this.reschedulePoller(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker); } catch (e) { - LOG.debug("exception detecting closed window (I guess it must be closed)"); - LOG.exception(e); - // swallow exceptions which may occur in HTA mode when the window is closed + LOG.error("Exception during pollForLoad; this should get noticed soon (" + e.message + ")!"); + LOG.exception(e); + this.pageLoadError = e; } - if (null == windowClosed) windowClosed = true; - if (windowClosed) { - this.pollingForLoad = false; - return; +}; + +BrowserBot.prototype._isSamePage = function(windowObject, originalDocument, originalLocation, originalHref, marker) { + var currentDocument = windowObject.document; + var currentLocation = windowObject.location; + var currentHref = currentLocation.href + + var sameDoc = this._isSameDocument(originalDocument, currentDocument); + + var sameLoc = (originalLocation === currentLocation); + var sameHref = (originalHref === currentHref); + var markedLoc = currentLocation[marker]; + + if (browserVersion.isKonqueror || browserVersion.isSafari) { + // the mark disappears too early on these browsers + markedLoc = true; } + return sameDoc && sameLoc && sameHref && markedLoc +}; - LOG.debug("pollForLoad original: " + originalHref); - try { +BrowserBot.prototype._isSameDocument = function(originalDocument, currentDocument) { + return originalDocument === currentDocument; +}; - var currentLocation = windowObject.location; - var currentHref = currentLocation.href - var sameLoc = (originalLocation === currentLocation); - var sameHref = (originalHref === currentHref); - var rs = windowObject.document.readyState; +BrowserBot.prototype.getReadyState = function(windowObject, currentDocument) { + var rs = currentDocument.readyState; + if (rs == null) { + if ((this.buttonWindow!=null && this.buttonWindow.document.readyState == null) // not proxy injection mode (and therefore buttonWindow isn't null) + || (top.document.readyState == null)) { // proxy injection mode (and therefore everything's in the top window, but buttonWindow doesn't exist) + // uh oh! we're probably on Firefox with no readyState extension installed! + // We'll have to just take a guess as to when the document is loaded; this guess + // will never be perfect. :-( + if (typeof currentDocument.getElementsByTagName != 'undefined' + && typeof currentDocument.getElementById != 'undefined' + && ( currentDocument.getElementsByTagName('body')[0] != null + || currentDocument.body != null )) { + if (windowObject.frameElement && windowObject.location.href == "about:blank" && windowObject.frameElement.src != "about:blank") { + LOG.info("getReadyState not loaded, frame location was about:blank, but frame src = " + windowObject.frameElement.src); + return null; + } + LOG.debug("getReadyState = windowObject.frames.length = " + windowObject.frames.length); + for (var i = 0; i < windowObject.frames.length; i++) { + LOG.debug("i = " + i); + if (this.getReadyState(windowObject.frames[i], windowObject.frames[i].document) != 'complete') { + LOG.debug("getReadyState aha! the nested frame " + windowObject.frames[i].name + " wasn't ready!"); + return null; + } + } - if (rs == null) rs = 'complete'; + rs = 'complete'; + } else { + LOG.debug("pollForLoad readyState was null and DOM appeared to not be ready yet"); + } + } + } + else if (rs == "loading" && browserVersion.isIE) { + LOG.debug("pageUnloading = true!!!!"); + this.pageUnloading = true; + } + LOG.debug("getReadyState returning " + rs); + return rs; +}; - if (!(sameLoc && sameHref) && rs == 'complete') { - LOG.debug("pollForLoad complete: " + rs + " (" + currentHref + ")"); - loadFunction(); - this.pollingForLoad = false; - return; - } - var self = this; - LOG.debug("pollForLoad continue: " + currentHref); - window.setTimeout(function() {self.pollForLoad(loadFunction, windowObject, originalLocation, originalHref);}, 500); - } catch (e) { - LOG.error("Exception during pollForLoad; this should get noticed soon!"); - LOG.exception(e); - this.pageLoadError = e; - } +/** This function isn't used normally, but was the way we used to schedule pollers: + asynchronously executed autonomous units. This is deprecated, but remains here + for future reference. + */ +BrowserBot.prototype.XXXreschedulePoller = function(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker) { + var self = this; + window.setTimeout(function() { + self.pollForLoad(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker); + }, 500); }; +/** This function isn't used normally, but is useful for debugging asynchronous pollers + * To enable it, rename it to "reschedulePoller", so it will override the + * existing reschedulePoller function + */ +BrowserBot.prototype.XXXreschedulePoller = function(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker) { + var doc = this.buttonWindow.document; + var button = doc.createElement("button"); + var buttonName = doc.createTextNode(marker + " - " + windowObject.name); + button.appendChild(buttonName); + var tools = doc.getElementById("tools"); + var self = this; + button.onclick = function() { + tools.removeChild(button); + self.pollForLoad(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker); + }; + tools.appendChild(button); + window.setTimeout(button.onclick, 500); +}; + +BrowserBot.prototype.reschedulePoller = function(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker) { + var self = this; + var pollerFunction = function() { + self.pollForLoad(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker); + }; + this.windowPollers.push(pollerFunction); +}; -BrowserBot.prototype.getContentWindow = function() { - return this.getFrame().contentWindow || frames[this.getFrame().id]; +BrowserBot.prototype.runScheduledPollers = function() { + var oldPollers = this.windowPollers; + this.windowPollers = new Array(); + for (var i = 0; i < oldPollers.length; i++) { + oldPollers[i].call(); + } +}; + +BrowserBot.prototype.isPollingForLoad = function(win) { + var marker; + if (this._getFrameElement(win)) { + marker = win.frameElement[this.uniqueId]; + } else { + marker = win[this.uniqueId]; + } + if (!marker) { + LOG.debug("isPollingForLoad false, missing uniqueId " + this.uniqueId + ": " + marker); + return false; + } + if (!this.pollingForLoad[marker]) { + LOG.debug("isPollingForLoad false, this.pollingForLoad[" + marker + "]: " + this.pollingForLoad[marker]); + return false; + } + return marker; }; -BrowserBot.prototype.getTargetWindow = function(windowName) { - LOG.debug("getTargetWindow(" + windowName + ")"); +BrowserBot.prototype.getWindowByName = function(windowName, doNotModify) { + LOG.debug("getWindowByName(" + windowName + ")"); // First look in the map of opened windows var targetWindow = this.openedWindows[windowName]; if (!targetWindow) { - var evalString = "this.getContentWindow().window." + windowName; - targetWindow = eval(evalString); + targetWindow = this.topWindow[windowName]; } if (!targetWindow) { throw new SeleniumError("Window does not exist"); } + if (!doNotModify) { + this._modifyWindow(targetWindow); + } return targetWindow; }; -BrowserBot.prototype.getCurrentWindow = function() { - var testWindow = this.getContentWindow().window; - if (this.currentWindowName != null) { - testWindow = this.getTargetWindow(this.currentWindowName); +BrowserBot.prototype.getCurrentWindow = function(doNotModify) { + var testWindow = this.currentWindow; + if (!doNotModify) { + this._modifyWindow(testWindow); } return testWindow; }; @@ -313,11 +555,27 @@ KonquerorBrowserBot.prototype.setIFrameLocation = function(iframe, location) { iframe.src = location; }; -KonquerorBrowserBot.prototype.setOpenLocation = function(location) { +KonquerorBrowserBot.prototype.setOpenLocation = function(win, loc) { // Window doesn't fire onload event when setting src to the current value, // so we set it to blank first. - this.getCurrentWindow().location.href = "about:blank"; - this.getCurrentWindow().location.href = location; + win.location.href = "about:blank"; + win.location.href = loc; + // force the current polling thread to detect a page load + var marker = this.isPollingForLoad(win); + if (marker) { + delete win.location[marker]; + } +}; + +KonquerorBrowserBot.prototype._isSameDocument = function(originalDocument, currentDocument) { + // under Konqueror, there may be this case: + // originalDocument and currentDocument are different objects + // while their location are same. + if (originalDocument) { + return originalDocument.location == currentDocument.location + } else { + return originalDocument === currentDocument; + } }; function SafariBrowserBot(frame) { @@ -326,6 +584,20 @@ function SafariBrowserBot(frame) { SafariBrowserBot.prototype = new BrowserBot; SafariBrowserBot.prototype.setIFrameLocation = KonquerorBrowserBot.prototype.setIFrameLocation; +SafariBrowserBot.prototype.setOpenLocation = KonquerorBrowserBot.prototype.setOpenLocation; + + +function OperaBrowserBot(frame) { + BrowserBot.call(this, frame); +} +OperaBrowserBot.prototype = new BrowserBot; +OperaBrowserBot.prototype.setIFrameLocation = function(iframe, location) { + if (iframe.src == location) { + iframe.src = location + '?reload'; + } else { + iframe.src = location; + } +} function IEBrowserBot(frame) { BrowserBot.call(this, frame); @@ -345,7 +617,7 @@ IEBrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToModif var end_of_base_ref = doc_location.indexOf('TestRunner.html'); var base_ref = doc_location.substring(0, end_of_base_ref); - var fullURL = base_ref + "TestRunner.html?singletest=" + escape(browserBot.modalDialogTest) + "&autoURL=" + escape(url) + "&runInterval=" + runInterval; + var fullURL = base_ref + "TestRunner.html?singletest=" + escape(browserBot.modalDialogTest) + "&autoURL=" + escape(url) + "&runInterval=" + runOptions.runInterval; browserBot.modalDialogTest = null; var returnValue = oldShowModalDialog(fullURL, args, features); @@ -353,6 +625,85 @@ IEBrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToModif }; }; +IEBrowserBot.prototype.modifySeparateTestWindowToDetectPageLoads = function(windowObject) { + this.pageUnloading = false; + this.permDeniedCount = 0; + var self = this; + var pageUnloadDetector = function() { + self.pageUnloading = true; + }; + windowObject.attachEvent("onbeforeunload", pageUnloadDetector); + BrowserBot.prototype.modifySeparateTestWindowToDetectPageLoads.call(this, windowObject); +}; + +IEBrowserBot.prototype.pollForLoad = function(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker) { + BrowserBot.prototype.pollForLoad.call(this, loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker); + if (this.pageLoadError) { + if (this.pageUnloading) { + var self = this; + LOG.warn("pollForLoad UNLOADING (" + marker + "): caught exception while firing events on unloading page: " + this.pageLoadError.message); + this.reschedulePoller(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker); + this.pageLoadError = null; + return; + } else if (((this.pageLoadError.message == "Permission denied") || (/^Access is denied/.test(this.pageLoadError.message))) + && this.permDeniedCount++ < 4) { + var self = this; + LOG.warn("pollForLoad (" + marker + "): " + this.pageLoadError.message + " (" + this.permDeniedCount + "), waiting to see if it goes away"); + this.reschedulePoller(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker); + this.pageLoadError = null; + return; + } + //handy for debugging! + //throw this.pageLoadError; + } +}; + +IEBrowserBot.prototype._windowClosed = function(win) { + try { + var c = win.closed; + // frame windows claim to be non-closed when their parents are closed + // but you can't access their document objects in that case + if (!c) { + try { + win.document; + } catch (de) { + if (de.message == "Permission denied") { + // the window is probably unloading, which means it's probably not closed yet + return false; + } + else if (/^Access is denied/.test(de.message)) { + // rare variation on "Permission denied"? + LOG.debug("IEBrowserBot.windowClosed: got " + de.message + " (this.pageUnloading=" + this.pageUnloading + "); assuming window is unloading, probably not closed yet"); + return false; + } else { + // this is probably one of those frame window situations + LOG.debug("IEBrowserBot.windowClosed: couldn't read win.document, assume closed: " + de.message + " (this.pageUnloading=" + this.pageUnloading + ")"); + return true; + } + } + } + if (c == null) { + LOG.debug("IEBrowserBot.windowClosed: win.closed was null, assuming closed"); + return true; + } + return c; + } catch (e) { + // Got an exception trying to read win.closed; we'll have to take a guess! + if (browserVersion.isHTA) { + if (e.message == "Permission denied") { + // the window is probably unloading, which means it's probably not closed yet + return false; + } else { + // there's a good chance that we've lost contact with the window object if it is closed + return true; + } + } else { + // the window is probably unloading, which means it's probably not closed yet + return false; + } + } +}; + SafariBrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToModify, browserBot) { BrowserBot.prototype.modifyWindowToRecordPopUpDialogs(windowToModify, browserBot); @@ -381,15 +732,12 @@ SafariBrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToM }; }; -var PageBot = function(pageWindow) { - if (pageWindow) { - this.currentWindow = pageWindow; - this.currentDocument = pageWindow.document; - this.location = pageWindow.location; - this.title = function() {return this.currentDocument.title;}; - } +var PageBot = function(browserbot) { + this.browserbot = browserbot; + this._registerAllLocatorFunctions(); +}; - // Register all locateElementBy* functions +PageBot.prototype._registerAllLocatorFunctions = function() { // TODO - don't do this in the constructor - only needed once ever this.locationStrategies = {}; for (var functionName in this) { @@ -409,71 +757,86 @@ var PageBot = function(pageWindow) { /** * Find a locator based on a prefix. */ - this.findElementBy = function(locatorType, locator, inDocument) { - var locatorFunction = this.locationStrategies[locatorType]; + this.findElementBy = function(locatorType, locator, inDocument, inWindow) { + var locatorFunction = this.locationStrategies[locatorType]; if (! locatorFunction) { throw new SeleniumError("Unrecognised locator type: '" + locatorType + "'"); } - return locatorFunction.call(this, locator, inDocument); + return locatorFunction.call(this, locator, inDocument, inWindow); }; /** * The implicit locator, that is used when no prefix is supplied. */ - this.locationStrategies['implicit'] = function(locator, inDocument) { + this.locationStrategies['implicit'] = function(locator, inDocument, inWindow) { if (locator.startsWith('//')) { - return this.locateElementByXPath(locator, inDocument); + return this.locateElementByXPath(locator, inDocument, inWindow); } if (locator.startsWith('document.')) { - return this.locateElementByDomTraversal(locator, inDocument); + return this.locateElementByDomTraversal(locator, inDocument, inWindow); } - return this.locateElementByIdentifier(locator, inDocument); + return this.locateElementByIdentifier(locator, inDocument, inWindow); }; +} -}; +PageBot.prototype.getDocument = function() { + return this.getCurrentWindow().document; +} + +PageBot.prototype.getCurrentWindow = function() { + return this.browserbot.getCurrentWindow(); +} -PageBot.createForWindow = function(windowObject) { +PageBot.prototype.getTitle = function() { + var t = this.getDocument().title; + if (typeof(t) == "string") { + t = t.trim(); + } + return t; +} + +// todo: this is a bad name ... we're not passing a window in +PageBot.createForWindow = function(browserbot) { if (browserVersion.isIE) { - return new IEPageBot(windowObject); + return new IEPageBot(browserbot); } else if (browserVersion.isKonqueror) { - return new KonquerorPageBot(windowObject); + return new KonquerorPageBot(browserbot); } else if (browserVersion.isSafari) { - return new SafariPageBot(windowObject); + return new SafariPageBot(browserbot); } else if (browserVersion.isOpera) { - return new OperaPageBot(windowObject); + return new OperaPageBot(browserbot); } else { - LOG.info("Using MozillaPageBot") // Use mozilla by default - return new MozillaPageBot(windowObject); + return new MozillaPageBot(browserbot); } }; -var MozillaPageBot = function(pageWindow) { - PageBot.call(this, pageWindow); +var MozillaPageBot = function(browserbot) { + PageBot.call(this, browserbot); }; MozillaPageBot.prototype = new PageBot(); -var KonquerorPageBot = function(pageWindow) { - PageBot.call(this, pageWindow); +var KonquerorPageBot = function(browserbot) { + PageBot.call(this, browserbot); }; KonquerorPageBot.prototype = new PageBot(); -var SafariPageBot = function(pageWindow) { - PageBot.call(this, pageWindow); +var SafariPageBot = function(browserbot) { + PageBot.call(this, browserbot); }; SafariPageBot.prototype = new PageBot(); -var IEPageBot = function(pageWindow) { - PageBot.call(this, pageWindow); +var IEPageBot = function(browserbot) { + PageBot.call(this, browserbot); }; IEPageBot.prototype = new PageBot(); -OperaPageBot = function(pageWindow) { - PageBot.call(this, pageWindow); +var OperaPageBot = function(browserbot) { + PageBot.call(this, browserbot); }; OperaPageBot.prototype = new PageBot(); @@ -491,14 +854,14 @@ PageBot.prototype.findElement = function(locator) { locatorString = result[2]; } - var element = this.findElementBy(locatorType, locatorString, this.currentDocument); + var element = this.findElementBy(locatorType, locatorString, this.getDocument(), this.getCurrentWindow()); if (element != null) { - return element; + return this.highlight(element); } - for (var i = 0; i < this.currentWindow.frames.length; i++) { - element = this.findElementBy(locatorType, locatorString, this.currentWindow.frames[i].document); + for (var i = 0; i < this.getCurrentWindow().frames.length; i++) { + element = this.findElementBy(locatorType, locatorString, this.getCurrentWindow().frames[i].document, this.getCurrentWindow().frames[i]); if (element != null) { - return element; + return this.highlight(element); } } @@ -506,27 +869,41 @@ PageBot.prototype.findElement = function(locator) { throw new SeleniumError("Element " + locator + " not found"); }; +PageBot.prototype.highlight = function (element) { + if (shouldHighlightLocatedElement) { + Effect.highlight(element); + } + return element; +} + +// as a static variable. +var shouldHighlightLocatedElement = false; + +PageBot.prototype.setHighlightElement = function (shouldHighlight) { + shouldHighlightLocatedElement = shouldHighlight; +} + /** * In non-IE browsers, getElementById() does not search by name. Instead, we * we search separately by id and name. */ -PageBot.prototype.locateElementByIdentifier = function(identifier, inDocument) { - return PageBot.prototype.locateElementById(identifier, inDocument) - || PageBot.prototype.locateElementByName(identifier, inDocument) +PageBot.prototype.locateElementByIdentifier = function(identifier, inDocument, inWindow) { + return PageBot.prototype.locateElementById(identifier, inDocument, inWindow) + || PageBot.prototype.locateElementByName(identifier, inDocument, inWindow) || null; }; /** * In IE, getElementById() also searches by name - this is an optimisation for IE. */ -IEPageBot.prototype.locateElementByIdentifer = function(identifier, inDocument) { +IEPageBot.prototype.locateElementByIdentifer = function(identifier, inDocument, inWindow) { return inDocument.getElementById(identifier); }; /** * Find the element with id - can't rely on getElementById, coz it returns by name as well in IE.. */ -PageBot.prototype.locateElementById = function(identifier, inDocument) { +PageBot.prototype.locateElementById = function(identifier, inDocument, inWindow) { var element = inDocument.getElementById(identifier); if (element && element.id === identifier) { return element; @@ -540,7 +917,7 @@ PageBot.prototype.locateElementById = function(identifier, inDocument) { * Find an element by name, refined by (optional) element-filter * expressions. */ -PageBot.prototype.locateElementByName = function(locator, document) { +PageBot.prototype.locateElementByName = function(locator, document, inWindow) { var elements = document.getElementsByTagName("*"); var filters = locator.split(' '); @@ -558,18 +935,21 @@ PageBot.prototype.locateElementByName = function(locator, document) { }; /** -* Finds an element using by evaluating the "document.*" string against the -* current document object. Dom expressions must begin with "document." -*/ -PageBot.prototype.locateElementByDomTraversal = function(domTraversal, inDocument) { - if (domTraversal.indexOf("document.") != 0) { - return null; - } + * Finds an element using by evaluating the specfied string. + */ +PageBot.prototype.locateElementByDomTraversal = function(domTraversal, inDocument, inWindow) { - // Trim the leading 'document' - domTraversal = domTraversal.substr(9); - var locatorScript = "inDocument." + domTraversal; - var element = eval(locatorScript); + var element = null; + try { + if (browserVersion.isOpera) { + element = inWindow.eval(domTraversal); + } else { + element = eval("inWindow." + domTraversal); + } + } catch (e) { + e.isSeleniumError = true; + throw e; + } if (!element) { return null; @@ -580,10 +960,10 @@ PageBot.prototype.locateElementByDomTraversal = function(domTraversal, inDocumen PageBot.prototype.locateElementByDomTraversal.prefix = "dom"; /** -* Finds an element identified by the xpath expression. Expressions _must_ -* begin with "//". -*/ -PageBot.prototype.locateElementByXPath = function(xpath, inDocument) { + * Finds an element identified by the xpath expression. Expressions _must_ + * begin with "//". + */ +PageBot.prototype.locateElementByXPath = function(xpath, inDocument, inWindow) { // Trim any trailing "/": not valid xpath, and remains from attribute // locator. @@ -602,30 +982,30 @@ PageBot.prototype.locateElementByXPath = function(xpath, inDocument) { // Handle //tag[@attr='value'] var match = xpath.match(/^\/\/(\w+|\*)\[@(\w+)=('([^\']+)'|"([^\"]+)")\]$/); if (match) { - return this.findElementByTagNameAndAttributeValue( - inDocument, - match[1].toUpperCase(), - match[2].toLowerCase(), - match[3].slice(1, -1) - ); + return this._findElementByTagNameAndAttributeValue( + inDocument, + match[1].toUpperCase(), + match[2].toLowerCase(), + match[3].slice(1, -1) + ); } // Handle //tag[text()='value'] var match = xpath.match(/^\/\/(\w+|\*)\[text\(\)=('([^\']+)'|"([^\"]+)")\]$/); if (match) { - return this.findElementByTagNameAndText( - inDocument, - match[1].toUpperCase(), - match[2].slice(1, -1) - ); + return this._findElementByTagNameAndText( + inDocument, + match[1].toUpperCase(), + match[2].slice(1, -1) + ); } - return this.findElementUsingFullXPath(xpath, inDocument); + return this._findElementUsingFullXPath(xpath, inDocument); }; -PageBot.prototype.findElementByTagNameAndAttributeValue = function( - inDocument, tagName, attributeName, attributeValue -) { +PageBot.prototype._findElementByTagNameAndAttributeValue = function( + inDocument, tagName, attributeName, attributeValue + ) { if (browserVersion.isIE && attributeName == "class") { attributeName = "className"; } @@ -639,9 +1019,9 @@ PageBot.prototype.findElementByTagNameAndAttributeValue = function( return null; }; -PageBot.prototype.findElementByTagNameAndText = function( - inDocument, tagName, text -) { +PageBot.prototype._findElementByTagNameAndText = function( + inDocument, tagName, text + ) { var elements = inDocument.getElementsByTagName(tagName); for (var i = 0; i < elements.length; i++) { if (getText(elements[i]) == text) { @@ -651,10 +1031,25 @@ PageBot.prototype.findElementByTagNameAndText = function( return null; }; -PageBot.prototype.findElementUsingFullXPath = function(xpath, inDocument) { +PageBot.prototype._namespaceResolver = function(prefix) { + if (prefix == 'html' || prefix == 'xhtml' || prefix == 'x') { + return 'http://www.w3.org/1999/xhtml'; + } else if (prefix == 'mathml') { + return 'http://www.w3.org/1998/Math/MathML'; + } else { + throw new Error("Unknown namespace: " + prefix + "."); + } +} + +PageBot.prototype._findElementUsingFullXPath = function(xpath, inDocument, inWindow) { + // HUGE hack - remove namespace from xpath for IE + if (browserVersion.isIE) { + xpath = xpath.replace(/x:/g, '') + } + // Use document.evaluate() if it's available if (inDocument.evaluate) { - return inDocument.evaluate(xpath, inDocument, null, 0, null).iterateNext(); + return inDocument.evaluate(xpath, inDocument, this._namespaceResolver, 0, null).iterateNext(); } // If not, fall back to slower JavaScript implementation @@ -668,10 +1063,10 @@ PageBot.prototype.findElementUsingFullXPath = function(xpath, inDocument) { }; /** -* Finds a link element with text matching the expression supplied. Expressions must -* begin with "link:". -*/ -PageBot.prototype.locateElementByLinkText = function(linkText, inDocument) { + * Finds a link element with text matching the expression supplied. Expressions must + * begin with "link:". + */ +PageBot.prototype.locateElementByLinkText = function(linkText, inDocument, inWindow) { var links = inDocument.getElementsByTagName('a'); for (var i = 0; i < links.length; i++) { var element = links[i]; @@ -684,9 +1079,9 @@ PageBot.prototype.locateElementByLinkText = function(linkText, inDocument) { PageBot.prototype.locateElementByLinkText.prefix = "link"; /** -* Returns an attribute based on an attribute locator. This is made up of an element locator -* suffixed with @attribute-name. -*/ + * Returns an attribute based on an attribute locator. This is made up of an element locator + * suffixed with @attribute-name. + */ PageBot.prototype.findAttribute = function(locator) { // Split into locator + attributeName var attributePos = locator.lastIndexOf("@"); @@ -728,7 +1123,6 @@ PageBot.prototype.selectOption = function(element, optionToSelect) { if (changed) { triggerEvent(element, 'change', true); } - triggerEvent(element, 'blur', false); }; /* @@ -741,7 +1135,6 @@ PageBot.prototype.addSelection = function(element, option) { option.selected = true; triggerEvent(element, 'change', true); } - triggerEvent(element, 'blur', false); }; /* @@ -754,7 +1147,6 @@ PageBot.prototype.removeSelection = function(element, option) { option.selected = false; triggerEvent(element, 'change', true); } - triggerEvent(element, 'blur', false); }; PageBot.prototype.checkMultiselect = function(element) { @@ -768,71 +1160,88 @@ PageBot.prototype.checkMultiselect = function(element) { PageBot.prototype.replaceText = function(element, stringValue) { triggerEvent(element, 'focus', false); triggerEvent(element, 'select', true); - element.value=stringValue; - if (!browserVersion.isChrome) { - // In chrome URL, The change event is already fired by setting the value. - triggerEvent(element, 'change', true); + var maxLengthAttr = element.getAttribute("maxLength"); + var actualValue = stringValue; + if (maxLengthAttr != null) { + var maxLength = parseInt(maxLengthAttr); + if (stringValue.length > maxLength) { + LOG.warn("BEFORE") + actualValue = stringValue.substr(0, maxLength); + LOG.warn("AFTER") + } } - triggerEvent(element, 'blur', false); + element.value = actualValue; + // DGF this used to be skipped in chrome URLs, but no longer. Is xpcnativewrappers to blame? + triggerEvent(element, 'change', true); }; -MozillaPageBot.prototype.clickElement = function(element) { +MozillaPageBot.prototype.clickElement = function(element, clientX, clientY) { triggerEvent(element, 'focus', false); // Add an event listener that detects if the default action has been prevented. // (This is caused by a javascript onclick handler returning false) var preventDefault = false; - - element.addEventListener("click", function(evt) {preventDefault = evt.getPreventDefault();}, false); - + + element.addEventListener("click", function(evt) { + preventDefault = evt.getPreventDefault(); + }, false); + // Trigger the click event. - triggerMouseEvent(element, 'click', true); + triggerMouseEvent(element, 'click', true, clientX, clientY); // Perform the link action if preventDefault was set. // In chrome URL, the link action is already executed by triggerMouseEvent. if (!browserVersion.isChrome && !preventDefault) { - // Try the element itself, as well as it's parent - this handles clicking images inside links. + var targetWindow = this.browserbot._getTargetWindow(element); if (element.href) { - this.currentWindow.location.href = element.href; - } - else if (element.parentNode && element.parentNode.href) { - this.currentWindow.location.href = element.parentNode.href; + targetWindow.location.href = element.href; + } else { + this.browserbot._handleClickingImagesInsideLinks(targetWindow, element); } } - if (this.windowClosed()) { + if (this._windowClosed()) { return; } - triggerEvent(element, 'blur', false); }; -OperaPageBot.prototype.clickElement = function(element) { +BrowserBot.prototype._handleClickingImagesInsideLinks = function(targetWindow, element) { + if (element.parentNode && element.parentNode.href) { + targetWindow.location.href = element.parentNode.href; + } +} + +BrowserBot.prototype._getTargetWindow = function(element) { + var targetWindow = this.getCurrentWindow(); + if (element.target) { + var frame = this._getFrameFromGlobal(element.target); + targetWindow = frame.contentWindow; + } + return targetWindow; +} + +BrowserBot.prototype._getFrameFromGlobal = function(target) { + pagebot = PageBot.createForWindow(this); + return pagebot.findElementBy("implicit", target, this.topWindow.document, this.topWindow); +} + +OperaPageBot.prototype.clickElement = function(element, clientX, clientY) { triggerEvent(element, 'focus', false); // Trigger the click event. - triggerMouseEvent(element, 'click', true); - - if (isDefined(element.checked)) { - // In Opera, clicking won't check/uncheck - if (element.type == "checkbox") { - element.checked = !element.checked; - } else { - element.checked = true; - } - } - - if (this.windowClosed()) { + triggerMouseEvent(element, 'click', true, clientX, clientY); + + if (this._windowClosed()) { return; } - triggerEvent(element, 'blur', false); }; -KonquerorPageBot.prototype.clickElement = function(element) { +KonquerorPageBot.prototype.clickElement = function(element, clientX, clientY) { triggerEvent(element, 'focus', false); @@ -840,20 +1249,17 @@ KonquerorPageBot.prototype.clickElement = function(element) { element.click(); } else { - triggerMouseEvent(element, 'click', true); + triggerMouseEvent(element, 'click', true, clientX, clientY); } - if (this.windowClosed()) { + if (this._windowClosed()) { return; } - triggerEvent(element, 'blur', false); }; -SafariPageBot.prototype.clickElement = function(element) { - +SafariPageBot.prototype.clickElement = function(element, clientX, clientY) { triggerEvent(element, 'focus', false); - var wasChecked = element.checked; // For form element it is simple. @@ -862,47 +1268,20 @@ SafariPageBot.prototype.clickElement = function(element) { } // For links and other elements, event emulation is required. else { - triggerMouseEvent(element, 'click', true); - - // Unfortunately, triggering the event doesn't seem to activate onclick handlers. - // We currently call onclick for the link, but I'm guessing that the onclick for containing - // elements is not being called. - var success = true; - if (element.onclick) { - var evt = document.createEvent('HTMLEvents'); - evt.initEvent('click', true, true); - var onclickResult = element.onclick(evt); - if (onclickResult === false) { - success = false; - } - } - - if (success) { - // Try the element itself, as well as it's parent - this handles clicking images inside links. - if (element.href) { - this.currentWindow.location.href = element.href; - } - else if (element.parentNode.href) { - this.currentWindow.location.href = element.parentNode.href; - } else { - // This is true for buttons outside of forms, and maybe others. - LOG.warn("Ignoring 'click' call for button outside form, or link without href." - + "Using buttons without an enclosing form can cause wierd problems with URL resolution in Safari." ); - // I implemented special handling for window.open, but unfortunately this behaviour is also displayed - // when we have a button without an enclosing form that sets document.location in the onclick handler. - // The solution is to always use an enclosing form for a button. - } + var targetWindow = this.browserbot._getTargetWindow(element); + // todo: what if the target anchor is on another page? + if (element.href && element.href.indexOf("#") != -1) { + var b = targetWindow.document.getElementById(element.href.split("#")[1]); + targetWindow.document.body.scrollTop = b.offsetTop; + } else { + triggerMouseEvent(element, 'click', true, clientX, clientY); } - } - if (this.windowClosed()) { - return; } - triggerEvent(element, 'blur', false); }; -IEPageBot.prototype.clickElement = function(element) { +IEPageBot.prototype.clickElement = function(element, clientX, clientY) { triggerEvent(element, 'focus', false); @@ -911,17 +1290,19 @@ IEPageBot.prototype.clickElement = function(element) { // Set a flag that records if the page will unload - this isn't always accurate, because // <a href="javascript:alert('foo'):"> triggers the onbeforeunload event, even thought the page won't unload var pageUnloading = false; - var pageUnloadDetector = function() {pageUnloading = true;}; - this.currentWindow.attachEvent("onbeforeunload", pageUnloadDetector); - + var pageUnloadDetector = function() { + pageUnloading = true; + }; + this.getCurrentWindow().attachEvent("onbeforeunload", pageUnloadDetector); element.click(); + // If the page is going to unload - still attempt to fire any subsequent events. // However, we can't guarantee that the page won't unload half way through, so we need to handle exceptions. try { - this.currentWindow.detachEvent("onbeforeunload", pageUnloadDetector); + this.getCurrentWindow().detachEvent("onbeforeunload", pageUnloadDetector); - if (this.windowClosed()) { + if (this._windowClosed()) { return; } @@ -930,12 +1311,13 @@ IEPageBot.prototype.clickElement = function(element) { triggerEvent(element, 'change', true); } - triggerEvent(element, 'blur', false); } catch (e) { // If the page is unloading, we may get a "Permission denied" or "Unspecified error". // Just ignore it, because the document may have unloaded. if (pageUnloading) { + LOG.logHook = function() { + }; LOG.warn("Caught exception when firing events on unloading page: " + e.message); return; } @@ -943,16 +1325,16 @@ IEPageBot.prototype.clickElement = function(element) { } }; -PageBot.prototype.windowClosed = function(element) { - return this.currentWindow.closed; +PageBot.prototype._windowClosed = function(element) { + return selenium.browserbot._windowClosed(this.getCurrentWindow()); }; PageBot.prototype.bodyText = function() { - return getText(this.currentDocument.body); + return getText(this.getDocument().body); }; PageBot.prototype.getAllButtons = function() { - var elements = this.currentDocument.getElementsByTagName('input'); + var elements = this.getDocument().getElementsByTagName('input'); var result = ''; for (var i = 0; i < elements.length; i++) { @@ -968,7 +1350,7 @@ PageBot.prototype.getAllButtons = function() { PageBot.prototype.getAllFields = function() { - var elements = this.currentDocument.getElementsByTagName('input'); + var elements = this.getDocument().getElementsByTagName('input'); var result = ''; for (var i = 0; i < elements.length; i++) { @@ -983,7 +1365,7 @@ PageBot.prototype.getAllFields = function() { }; PageBot.prototype.getAllLinks = function() { - var elements = this.currentDocument.getElementsByTagName('a'); + var elements = this.getDocument().getElementsByTagName('a'); var result = ''; for (var i = 0; i < elements.length; i++) { @@ -996,10 +1378,13 @@ PageBot.prototype.getAllLinks = function() { }; PageBot.prototype.setContext = function(strContext, logLevel) { - //set the current test title - document.getElementById("context").innerHTML=strContext; - if (logLevel!=null) { - LOG.setLogLevelThreshold(logLevel); + //set the current test title + var ctx = document.getElementById("context"); + if (ctx != null) { + ctx.innerHTML = strContext; + } + if (logLevel != null) { + LOG.setLogLevelThreshold(logLevel); } }; @@ -1008,27 +1393,23 @@ function isDefined(value) { } PageBot.prototype.goBack = function() { - this.currentWindow.history.back(); - if (browserVersion.isOpera && !selenium.browserbot.pollingForLoad) { - // DGF On Opera, goBack doesn't re-trigger a load event, so we have to poll for it - selenium.browserbot.callOnWindowPageTransition(selenium.browserbot.recordPageLoad, this.currentWindow); - } + this.getCurrentWindow().history.back(); }; PageBot.prototype.goForward = function() { - this.currentWindow.history.forward(); + this.getCurrentWindow().history.forward(); }; PageBot.prototype.close = function() { - if (browserVersion.isChrome) { - this.currentWindow.close(); - } else { - this.currentWindow.eval("window.close();"); - } + if (browserVersion.isChrome || browserVersion.isSafari) { + this.getCurrentWindow().close(); + } else { + this.getCurrentWindow().eval("window.close();"); + } }; PageBot.prototype.refresh = function() { - this.currentWindow.location.reload(true); + this.getCurrentWindow().location.reload(true); }; /** @@ -1043,7 +1424,7 @@ PageBot.prototype.selectElementsBy = function(filterType, filter, elements) { return filterFunction(filter, elements); }; -PageBot.filterFunctions = {}; +PageBot.filterFunctions = {}; PageBot.filterFunctions.name = function(name, elements) { var selectedElements = []; @@ -1076,10 +1457,10 @@ PageBot.filterFunctions.index = function(index, elements) { return [elements[index]]; }; -PageBot.prototype.selectElements = function(filterExpr, elements, defaultFilterType) { +PageBot.prototype.selectElements = function(filterExpr, elements, defaultFilterType) { var filterType = (defaultFilterType || 'value'); - + // If there is a filter prefix, use the specified strategy var result = filterExpr.match(/^([A-Za-z]+)=(.+)/); if (result) { @@ -1094,11 +1475,11 @@ PageBot.prototype.selectElements = function(filterExpr, elements, defaultFilterT * Find an element by class */ PageBot.prototype.locateElementByClass = function(locator, document) { - return Element.findFirstMatchingChild(document, - function(element) { - return element.className == locator - } - ); + return Element.findFirstMatchingChild(document, + function(element) { + return element.className == locator + } + ); } /** @@ -1106,9 +1487,18 @@ PageBot.prototype.locateElementByClass = function(locator, document) { */ PageBot.prototype.locateElementByAlt = function(locator, document) { return Element.findFirstMatchingChild(document, - function(element) { - return element.alt == locator - } - ); + function(element) { + return element.alt == locator + } + ); } +/** + * Find an element by css selector + */ +PageBot.prototype.locateElementByCss = function(locator, document) { + var elements = cssQuery(locator, document); + if (elements.length != 0) + return elements[0]; + return null; +} diff --git a/tests/test_tools/selenium/core/scripts/selenium-browserdetect.js b/tests/test_tools/selenium/core/scripts/selenium-browserdetect.js index 137a1518..d97e5a58 100644 --- a/tests/test_tools/selenium/core/scripts/selenium-browserdetect.js +++ b/tests/test_tools/selenium/core/scripts/selenium-browserdetect.js @@ -1,30 +1,30 @@ /* -* Copyright 2004 ThoughtWorks, Inc -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -* -*/ + * Copyright 2004 ThoughtWorks, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ -// Although it's generally better web development practice not to use browser-detection -// (feature detection is better), the subtle browser differences that Selenium has to -// work around seem to make it necessary. Maybe as we learn more about what we need, -// we can do this in a more "feature-centric" rather than "browser-centric" way. +// Although it's generally better web development practice not to use +// browser-detection (feature detection is better), the subtle browser +// differences that Selenium has to work around seem to make it +// necessary. Maybe as we learn more about what we need, we can do this in +// a more "feature-centric" rather than "browser-centric" way. -BrowserVersion = function() { +var BrowserVersion = function() { this.name = navigator.appName; - if (window.opera != null) - { + if (window.opera != null) { this.browser = BrowserVersion.OPERA; this.isOpera = true; return; @@ -33,66 +33,60 @@ BrowserVersion = function() { var self = this; var checkChrome = function() { - var loc = window.document.location.href; - try { - loc = window.top.document.location.href; - } catch (e) { - // can't see the top (that means we might be chrome, but it's impossible to be sure) - self.isChromeDetectable = "no, top location couldn't be read in this window"; - } - - if (/^chrome:\/\//.test(loc)) { - self.isChrome = true; - } else { - self.isChrome = false; - } + var loc = window.document.location.href; + try { + loc = window.top.document.location.href; + } catch (e) { + // can't see the top (that means we might be chrome, but it's impossible to be sure) + self.isChromeDetectable = "no, top location couldn't be read in this window"; + } + + if (/^chrome:\/\//.test(loc)) { + self.isChrome = true; + } else { + self.isChrome = false; + } } - if (this.name == "Microsoft Internet Explorer") - { + if (this.name == "Microsoft Internet Explorer") { this.browser = BrowserVersion.IE; this.isIE = true; if (window.top.SeleniumHTARunner && window.top.document.location.pathname.match(/.hta$/i)) { - this.isHTA = true; + this.isHTA = true; } if ("0" == navigator.appMinorVersion) { - this.preSV1 = true; + this.preSV1 = true; } return; } - if (navigator.userAgent.indexOf('Safari') != -1) - { + if (navigator.userAgent.indexOf('Safari') != -1) { this.browser = BrowserVersion.SAFARI; this.isSafari = true; this.khtml = true; return; } - if (navigator.userAgent.indexOf('Konqueror') != -1) - { + if (navigator.userAgent.indexOf('Konqueror') != -1) { this.browser = BrowserVersion.KONQUEROR; this.isKonqueror = true; this.khtml = true; return; } - if (navigator.userAgent.indexOf('Firefox') != -1) - { + if (navigator.userAgent.indexOf('Firefox') != -1) { this.browser = BrowserVersion.FIREFOX; this.isFirefox = true; this.isGecko = true; var result = /.*Firefox\/([\d\.]+).*/.exec(navigator.userAgent); - if (result) - { + if (result) { this.firefoxVersion = result[1]; } checkChrome(); return; } - if (navigator.userAgent.indexOf('Gecko') != -1) - { + if (navigator.userAgent.indexOf('Gecko') != -1) { this.browser = BrowserVersion.MOZILLA; this.isMozilla = true; this.isGecko = true; @@ -111,5 +105,4 @@ BrowserVersion.FIREFOX = "Firefox"; BrowserVersion.MOZILLA = "Mozilla"; BrowserVersion.UNKNOWN = "Unknown"; -browserVersion = new BrowserVersion(); - +var browserVersion = new BrowserVersion(); diff --git a/tests/test_tools/selenium/core/scripts/selenium-commandhandlers.js b/tests/test_tools/selenium/core/scripts/selenium-commandhandlers.js index ee01ea76..c11a80ad 100644 --- a/tests/test_tools/selenium/core/scripts/selenium-commandhandlers.js +++ b/tests/test_tools/selenium/core/scripts/selenium-commandhandlers.js @@ -15,38 +15,58 @@ * */ function CommandHandlerFactory() { - this.actions = {}; - this.asserts = {}; - this.accessors = {}; var self = this; + this.handlers = {}; + this.registerAction = function(name, action, wait, dontCheckAlertsAndConfirms) { var handler = new ActionHandler(action, wait, dontCheckAlertsAndConfirms); - this.actions[name] = handler; + this.handlers[name] = handler; }; this.registerAccessor = function(name, accessor) { var handler = new AccessorHandler(accessor); - this.accessors[name] = handler; + this.handlers[name] = handler; }; this.registerAssert = function(name, assertion, haltOnFailure) { var handler = new AssertHandler(assertion, haltOnFailure); - this.asserts[name] = handler; + this.handlers[name] = handler; }; - + this.getCommandHandler = function(name) { - return this.actions[name] || this.accessors[name] || this.asserts[name] || null; + return this.handlers[name] || null; // todo: why null, and not undefined? }; - this.registerAll = function(commandObject) { - registerAllAccessors(commandObject); - registerAllActions(commandObject); - registerAllAsserts(commandObject); + // Methods of the form getFoo(target) result in commands: + // getFoo, assertFoo, verifyFoo, assertNotFoo, verifyNotFoo + // storeFoo, waitForFoo, and waitForNotFoo. + var _registerAllAccessors = function(commandObject) { + for (var functionName in commandObject) { + var matchForGetter = /^get([A-Z].+)$/.exec(functionName); + if (matchForGetter != null) { + var accessor = commandObject[functionName]; + var baseName = matchForGetter[1]; + self.registerAccessor(functionName, accessor); + self.registerAssertionsBasedOnAccessor(accessor, baseName); + self.registerStoreCommandBasedOnAccessor(accessor, baseName); + self.registerWaitForCommandsBasedOnAccessor(accessor, baseName); + } + var matchForIs = /^is([A-Z].+)$/.exec(functionName); + if (matchForIs != null) { + var accessor = commandObject[functionName]; + var baseName = matchForIs[1]; + var predicate = self.createPredicateFromBooleanAccessor(accessor); + self.registerAccessor(functionName, accessor); + self.registerAssertionsBasedOnAccessor(accessor, baseName, predicate); + self.registerStoreCommandBasedOnAccessor(accessor, baseName); + self.registerWaitForCommandsBasedOnAccessor(accessor, baseName, predicate); + } + } }; - var registerAllActions = function(commandObject) { + var _registerAllActions = function(commandObject) { for (var functionName in commandObject) { var result = /^do([A-Z].+)$/.exec(functionName); if (result != null) { @@ -63,8 +83,7 @@ function CommandHandlerFactory() { } }; - - var registerAllAsserts = function(commandObject) { + var _registerAllAsserts = function(commandObject) { for (var functionName in commandObject) { var result = /^assert([A-Z].+)$/.exec(functionName); if (result != null) { @@ -81,7 +100,12 @@ function CommandHandlerFactory() { } }; - + this.registerAll = function(commandObject) { + _registerAllAccessors(commandObject); + _registerAllActions(commandObject); + _registerAllAsserts(commandObject); + }; + // Given an accessor function getBlah(target), // return a "predicate" equivalient to isBlah(target, value) that // is true when the value returned by the accessor matches the specified value. @@ -95,7 +119,7 @@ function CommandHandlerFactory() { } }; }; - + // Given a (no-arg) accessor function getBlah(), // return a "predicate" equivalient to isBlah(value) that // is true when the value returned by the accessor matches the specified value. @@ -109,20 +133,20 @@ function CommandHandlerFactory() { } }; }; - + // Given a boolean accessor function isBlah(), // return a "predicate" equivalient to isBlah() that // returns an appropriate PredicateResult value. this.createPredicateFromBooleanAccessor = function(accessor) { return function() { - var accessorResult; - if (arguments.length > 2) throw new SeleniumError("Too many arguments! " + arguments.length); - if (arguments.length == 2) { - accessorResult = accessor.call(this, arguments[0], arguments[1]); + var accessorResult; + if (arguments.length > 2) throw new SeleniumError("Too many arguments! " + arguments.length); + if (arguments.length == 2) { + accessorResult = accessor.call(this, arguments[0], arguments[1]); } else if (arguments.length == 1) { - accessorResult = accessor.call(this, arguments[0]); + accessorResult = accessor.call(this, arguments[0]); } else { - accessorResult = accessor.call(this); + accessorResult = accessor.call(this); } if (accessorResult) { return new PredicateResult(true, "true"); @@ -131,17 +155,17 @@ function CommandHandlerFactory() { } }; }; - + // Given an accessor fuction getBlah([target]) (target is optional) // return a predicate equivalent to isBlah([target,] value) that // is true when the value returned by the accessor matches the specified value. this.createPredicateFromAccessor = function(accessor) { - if (accessor.length == 0) { + if (accessor.length == 0) { return self.createPredicateFromNoArgAccessor(accessor); } return self.createPredicateFromSingleArgAccessor(accessor); }; - + // Given a predicate, return the negation of that predicate. // Leaves the message unchanged. // Used to create assertNot, verifyNot, and waitForNot commands. @@ -152,124 +176,90 @@ function CommandHandlerFactory() { return result; }; }; - + // Convert an isBlahBlah(target, value) function into an assertBlahBlah(target, value) function. this.createAssertionFromPredicate = function(predicate) { - return function(target, value) { - var result = predicate.call(this, target, value); - if (!result.isTrue) { - Assert.fail(result.message); - } - }; + return function(target, value) { + var result = predicate.call(this, target, value); + if (!result.isTrue) { + Assert.fail(result.message); + } + }; }; - + + + var _negtiveName = function(baseName) { + var matchResult = /^(.*)Present$/.exec(baseName); + if (matchResult != null) { + return matchResult[1] + "NotPresent"; + } + return "Not" + baseName; + }; + // Register an assertion, a verification, a negative assertion, // and a negative verification based on the specified accessor. this.registerAssertionsBasedOnAccessor = function(accessor, baseName, predicate) { - if (predicate==null) { + if (predicate == null) { predicate = self.createPredicateFromAccessor(accessor); } var assertion = self.createAssertionFromPredicate(predicate); - // Register an assert with the "assert" prefix, and halt on failure. self.registerAssert("assert" + baseName, assertion, true); - // Register a verify with the "verify" prefix, and do not halt on failure. self.registerAssert("verify" + baseName, assertion, false); - + var invertedPredicate = self.invertPredicate(predicate); var negativeAssertion = self.createAssertionFromPredicate(invertedPredicate); - - var result = /^(.*)Present$/.exec(baseName); - if (result==null) { - // Register an assertNot with the "assertNot" prefix, and halt on failure. - self.registerAssert("assertNot"+baseName, negativeAssertion, true); - // Register a verifyNot with the "verifyNot" prefix, and do not halt on failure. - self.registerAssert("verifyNot"+baseName, negativeAssertion, false); - } - else { - var invertedBaseName = result[1] + "NotPresent"; - - // Register an assertNot ending w/ "NotPresent", and halt on failure. - self.registerAssert("assert"+invertedBaseName, negativeAssertion, true); - // Register an assertNot ending w/ "NotPresent", and do not halt on failure. - self.registerAssert("verify"+invertedBaseName, negativeAssertion, false); - } + self.registerAssert("assert" + _negtiveName(baseName), negativeAssertion, true); + self.registerAssert("verify" + _negtiveName(baseName), negativeAssertion, false); }; - - + // Convert an isBlahBlah(target, value) function into a waitForBlahBlah(target, value) function. this.createWaitForActionFromPredicate = function(predicate) { - var action = function(target, value) { - var seleniumApi = this; - testLoop.waitForCondition = function () { - try { - return predicate.call(seleniumApi, target, value).isTrue; - } catch (e) { - // Treat exceptions as meaning the condition is not yet met. - // Useful, for example, for waitForValue when the element has - // not even been created yet. - // TODO: possibly should rethrow some types of exception. - return false; - } - }; - }; - return action; + return function(target, value) { + var seleniumApi = this; + return function () { + try { + return predicate.call(seleniumApi, target, value).isTrue; + } catch (e) { + // Treat exceptions as meaning the condition is not yet met. + // Useful, for example, for waitForValue when the element has + // not even been created yet. + // TODO: possibly should rethrow some types of exception. + return false; + } + }; + }; }; - + // Register a waitForBlahBlah and waitForNotBlahBlah based on the specified accessor. this.registerWaitForCommandsBasedOnAccessor = function(accessor, baseName, predicate) { if (predicate==null) { predicate = self.createPredicateFromAccessor(accessor); } var waitForAction = self.createWaitForActionFromPredicate(predicate); - self.registerAction("waitFor"+baseName, waitForAction, false, true); + self.registerAction("waitFor"+baseName, waitForAction, false, true); var invertedPredicate = self.invertPredicate(predicate); var waitForNotAction = self.createWaitForActionFromPredicate(invertedPredicate); - self.registerAction("waitForNot"+baseName, waitForNotAction, false, true); - } - - // Register a storeBlahBlah based on the specified accessor. + self.registerAction("waitFor"+_negtiveName(baseName), waitForNotAction, false, true); + //TODO decide remove "waitForNot.*Present" action name or not + //for the back compatiblity issues we still make waitForNot.*Present availble + self.registerAction("waitForNot"+baseName, waitForNotAction, false, true); + } + + // Register a storeBlahBlah based on the specified accessor. this.registerStoreCommandBasedOnAccessor = function(accessor, baseName) { var action; if (accessor.length == 1) { - action = function(target, varName) { - storedVars[varName] = accessor.call(this, target); - }; - } else { - action = function(varName) { - storedVars[varName] = accessor.call(this); - }; - } - self.registerAction("store"+baseName, action, false, accessor.dontCheckAlertsAndConfirms); - }; - - // Methods of the form getFoo(target) result in commands: - // getFoo, assertFoo, verifyFoo, assertNotFoo, verifyNotFoo - // storeFoo, waitForFoo, and waitForNotFoo. - var registerAllAccessors = function(commandObject) { - for (var functionName in commandObject) { - var match = /^get([A-Z].+)$/.exec(functionName); - if (match != null) { - var accessor = commandObject[functionName]; - var baseName = match[1]; - self.registerAccessor(functionName, accessor); - self.registerAssertionsBasedOnAccessor(accessor, baseName); - self.registerStoreCommandBasedOnAccessor(accessor, baseName); - self.registerWaitForCommandsBasedOnAccessor(accessor, baseName); - } - var match = /^is([A-Z].+)$/.exec(functionName); - if (match != null) { - var accessor = commandObject[functionName]; - var baseName = match[1]; - var predicate = self.createPredicateFromBooleanAccessor(accessor); - self.registerAccessor(functionName, accessor); - self.registerAssertionsBasedOnAccessor(accessor, baseName, predicate); - self.registerStoreCommandBasedOnAccessor(accessor, baseName); - self.registerWaitForCommandsBasedOnAccessor(accessor, baseName, predicate); - } + action = function(target, varName) { + storedVars[varName] = accessor.call(this, target); + }; + } else { + action = function(varName) { + storedVars[varName] = accessor.call(this); + }; } + self.registerAction("store"+baseName, action, false, accessor.dontCheckAlertsAndConfirms); }; - - + } function PredicateResult(isTrue, message) { @@ -301,36 +291,34 @@ function ActionHandler(action, wait, dontCheckAlerts) { ActionHandler.prototype = new CommandHandler; ActionHandler.prototype.execute = function(seleniumApi, command) { if (this.checkAlerts && (null==/(Alert|Confirmation)(Not)?Present/.exec(command.command))) { - this.checkForAlerts(seleniumApi); + seleniumApi.ensureNoUnhandledPopups(); } - var processState = this.executor.call(seleniumApi, command.target, command.value); + var terminationCondition = this.executor.call(seleniumApi, command.target, command.value); // If the handler didn't return a wait flag, check to see if the // handler was registered with the wait flag. - if (processState == undefined && this.wait) { - processState = SELENIUM_PROCESS_WAIT; - } - return new CommandResult(processState); -}; -ActionHandler.prototype.checkForAlerts = function(seleniumApi) { - if ( seleniumApi.browserbot.hasAlerts() ) { - throw new SeleniumError("There was an unexpected Alert! [" + seleniumApi.browserbot.getNextAlert() + "]"); - } - if ( seleniumApi.browserbot.hasConfirmations() ) { - throw new SeleniumError("There was an unexpected Confirmation! [" + seleniumApi.browserbot.getNextConfirmation() + "]"); + if (terminationCondition == undefined && this.wait) { + terminationCondition = seleniumApi.makePageLoadCondition(); } + return new ActionResult(terminationCondition); }; +function ActionResult(terminationCondition) { + this.terminationCondition = terminationCondition; +} + function AccessorHandler(accessor) { CommandHandler.call(this, "accessor", true, accessor); } AccessorHandler.prototype = new CommandHandler; AccessorHandler.prototype.execute = function(seleniumApi, command) { var returnValue = this.executor.call(seleniumApi, command.target, command.value); - var result = new CommandResult(); - result.result = returnValue; - return result; + return new AccessorResult(returnValue); }; +function AccessorResult(result) { + this.result = result; +} + /** * Handler for assertions and verifications. */ @@ -339,10 +327,9 @@ function AssertHandler(assertion, haltOnFailure) { } AssertHandler.prototype = new CommandHandler; AssertHandler.prototype.execute = function(seleniumApi, command) { - var result = new CommandResult(); + var result = new AssertResult(); try { this.executor.call(seleniumApi, command.target, command.value); - result.passed = true; } catch (e) { // If this is not a AssertionFailedError, or we should haltOnFailure, rethrow. if (!e.isAssertionFailedError) { @@ -352,20 +339,24 @@ AssertHandler.prototype.execute = function(seleniumApi, command) { var error = new SeleniumError(e.failureMessage); throw error; } - result.failed = true; - result.failureMessage = e.failureMessage; + result.setFailed(e.failureMessage); } return result; }; - -function CommandResult(processState) { - this.processState = processState; - this.result = null; +function AssertResult() { + this.passed = true; +} +AssertResult.prototype.setFailed = function(message) { + this.passed = null; + this.failed = true; + this.failureMessage = message; } -function SeleniumCommand(command, target, value) { +function SeleniumCommand(command, target, value, isBreakpoint) { this.command = command; this.target = target; this.value = value; + this.isBreakpoint = isBreakpoint; } + diff --git a/tests/test_tools/selenium/core/scripts/selenium-executionloop.js b/tests/test_tools/selenium/core/scripts/selenium-executionloop.js index 14c1a07a..d59fc148 100644 --- a/tests/test_tools/selenium/core/scripts/selenium-executionloop.js +++ b/tests/test_tools/selenium/core/scripts/selenium-executionloop.js @@ -14,253 +14,173 @@ * limitations under the License. */ -SELENIUM_PROCESS_WAIT = "wait"; - function TestLoop(commandFactory) { - this.commandFactory = commandFactory; - this.waitForConditionTimeout = 30 * 1000; // 30 seconds +} + +TestLoop.prototype = { - this.start = function() { + start : function() { selenium.reset(); - LOG.debug("testLoop.start()"); + LOG.debug("currentTest.start()"); this.continueTest(); - }; + }, - /** - * Select the next command and continue the test. - */ - this.continueTest = function() { - LOG.debug("testLoop.continueTest() - acquire the next command"); + continueTest : function() { + /** + * Select the next command and continue the test. + */ + LOG.debug("currentTest.continueTest() - acquire the next command"); if (! this.aborted) { this.currentCommand = this.nextCommand(); } if (! this.requiresCallBack) { - this.beginNextTest(); - } // otherwise, just finish and let the callback invoke beginNextTest() - }; - - this.beginNextTest = function() { - LOG.debug("testLoop.beginNextTest()"); - if (this.currentCommand) { + this.continueTestAtCurrentCommand(); + } // otherwise, just finish and let the callback invoke continueTestAtCurrentCommand() + }, + + continueTestAtCurrentCommand : function() { + LOG.debug("currentTest.continueTestAtCurrentCommand()"); + if (this.currentCommand) { // TODO: rename commandStarted to commandSelected, OR roll it into nextCommand this.commandStarted(this.currentCommand); - this.resumeAfterDelay(); + this._resumeAfterDelay(); } else { - this.testComplete(); + this._testComplete(); } - } - - /** - * Pause, then execute the current command. - */ - this.resumeAfterDelay = function() { + }, + + _resumeAfterDelay : function() { + /** + * Pause, then execute the current command. + */ // Get the command delay. If a pauseInterval is set, use it once // and reset it. Otherwise, use the defined command-interval. var delay = this.pauseInterval || this.getCommandInterval(); this.pauseInterval = undefined; - if (delay < 0) { + if (this.currentCommand.isBreakpoint || delay < 0) { // Pause: enable the "next/continue" button this.pause(); } else { - window.setTimeout("testLoop.resume()", delay); + window.setTimeout(this.resume.bind(this), delay); } - }; + }, - /** - * Select the next command and continue the test. - */ - this.resume = function() { - LOG.debug("testLoop.resume() - actually execute"); + resume: function() { + /** + * Select the next command and continue the test. + */ + LOG.debug("currentTest.resume() - actually execute"); try { - this.executeCurrentCommand(); - this.waitForConditionStart = new Date().getTime(); + selenium.browserbot.runScheduledPollers(); + this._executeCurrentCommand(); this.continueTestWhenConditionIsTrue(); } catch (e) { - this.handleCommandError(e); - this.testComplete(); + this._handleCommandError(e); + this._testComplete(); return; } - }; - - /** - * Execute the current command. - * - * The return value, if not null, should be a function which will be - * used to determine when execution can continue. - */ - this.executeCurrentCommand = function() { - + }, + + _testComplete : function() { + selenium.ensureNoUnhandledPopups(); + this.testComplete(); + }, + + _executeCurrentCommand : function() { + /** + * Execute the current command. + * + * @return a function which will be used to determine when + * execution can continue, or null if we can continue immediately + */ var command = this.currentCommand; LOG.info("Executing: |" + command.command + " | " + command.target + " | " + command.value + " |"); - + var handler = this.commandFactory.getCommandHandler(command.command); if (handler == null) { throw new SeleniumError("Unknown command: '" + command.command + "'"); } - + command.target = selenium.preprocessParameter(command.target); command.value = selenium.preprocessParameter(command.value); LOG.debug("Command found, going to execute " + command.command); var result = handler.execute(selenium, command); - LOG.debug("Command complete"); + LOG.debug("Command complete"); this.commandComplete(result); - if (result.processState == SELENIUM_PROCESS_WAIT) { - this.waitForCondition = function() { - LOG.debug("Checking condition: isNewPageLoaded?"); - return selenium.browserbot.isNewPageLoaded(); - }; - } - }; - - this.handleCommandError = function(e) { - if (!e.isSeleniumError) { + this.waitForCondition = result.terminationCondition; + + }, + + _handleCommandError : function(e) { + if (!e.isSeleniumError) { LOG.exception(e); var msg = "Selenium failure. Please report to selenium-dev@openqa.org, with error details from the log window."; if (e.message) { - msg += " The error message is: " + e.message; + msg += " The error message is: " + e.message; } this.commandError(msg); } else { LOG.error(e.message); this.commandError(e.message); } - }; - - /** - * Busy wait for waitForCondition() to become true, and then carry on - * with test. Fail the current test if there's a timeout or an exception. - */ - this.continueTestWhenConditionIsTrue = function () { - LOG.debug("testLoop.continueTestWhenConditionIsTrue()"); + }, + + continueTestWhenConditionIsTrue: function () { + /** + * Busy wait for waitForCondition() to become true, and then carry + * on with test. Fail the current test if there's a timeout or an + * exception. + */ + LOG.debug("currentTest.continueTestWhenConditionIsTrue()"); + selenium.browserbot.runScheduledPollers(); try { if (this.waitForCondition == null || this.waitForCondition()) { - LOG.debug("condition satisfied; let's continueTest()"); - this.waitForCondition = null; - this.waitForConditionStart = null; - this.continueTest(); - } else { - LOG.debug("waitForCondition was false; keep waiting!"); - if (this.waitForConditionTimeout != null) { - var now = new Date(); - if ((now - this.waitForConditionStart) > this.waitForConditionTimeout) { - throw new SeleniumError("Timed out after " + this.waitForConditionTimeout + "ms"); - } - } - window.setTimeout("testLoop.continueTestWhenConditionIsTrue()", 10); - } - } catch (e) { - var lastResult = new CommandResult(); - lastResult.failed = true; - lastResult.failureMessage = e.message; - this.commandComplete(lastResult); - this.testComplete(); - } - }; - -} - -/** The default is not to have any interval between commands. */ -TestLoop.prototype.getCommandInterval = function() { - return 0; -}; - -TestLoop.prototype.nextCommand = noop; - -TestLoop.prototype.commandStarted = noop; - -TestLoop.prototype.commandError = noop; - -TestLoop.prototype.commandComplete = noop; - -TestLoop.prototype.testComplete = noop; - -TestLoop.prototype.pause = noop; - -function noop() { - -}; - -/** - * Tell Selenium to expect a failure on the next command execution. This - * command temporarily installs a CommandFactory that generates - * CommandHandlers that expect a failure. - */ -Selenium.prototype.assertFailureOnNext = function(message) { - if (!message) { - throw new Error("Message must be provided"); - } + LOG.debug("condition satisfied; let's continueTest()"); + this.waitForCondition = null; + this.continueTest(); + } else { + LOG.debug("waitForCondition was false; keep waiting!"); + window.setTimeout(this.continueTestWhenConditionIsTrue.bind(this), 100); + } + } catch (e) { + var lastResult = {}; + lastResult.failed = true; + lastResult.failureMessage = e.message; + this.commandComplete(lastResult); + this.testComplete(); + } + }, - var expectFailureCommandFactory = - new ExpectFailureCommandFactory(testLoop.commandFactory, message, "failure"); - expectFailureCommandFactory.baseExecutor = executeCommandAndReturnFailureMessage; - testLoop.commandFactory = expectFailureCommandFactory; -}; + pause : function() {}, + nextCommand : function() {}, + commandStarted : function() {}, + commandComplete : function() {}, + commandError : function() {}, + testComplete : function() {}, -/** - * Tell Selenium to expect an error on the next command execution. This - * command temporarily installs a CommandFactory that generates - * CommandHandlers that expect a failure. - */ -Selenium.prototype.assertErrorOnNext = function(message) { - if (!message) { - throw new Error("Message must be provided"); + getCommandInterval : function() { + return 0; } - var expectFailureCommandFactory = - new ExpectFailureCommandFactory(testLoop.commandFactory, message, "error"); - expectFailureCommandFactory.baseExecutor = executeCommandAndReturnErrorMessage; - testLoop.commandFactory = expectFailureCommandFactory; -}; - -function ExpectFailureCommandFactory(originalCommandFactory, expectedErrorMessage, errorType) { - this.getCommandHandler = function(name) { - var baseHandler = originalCommandFactory.getCommandHandler(name); - var baseExecutor = this.baseExecutor; - var expectFailureCommand = {}; - expectFailureCommand.execute = function() { - var baseFailureMessage = baseExecutor(baseHandler, arguments); - var result = new CommandResult(); - if (!baseFailureMessage) { - result.failed = true; - result.failureMessage = "Expected " + errorType + " did not occur."; - } - else { - if (! PatternMatcher.matches(expectedErrorMessage, baseFailureMessage)) { - result.failed = true; - result.failureMessage = "Expected " + errorType + " message '" + expectedErrorMessage - + "' but was '" + baseFailureMessage + "'"; - } - else { - result.passed = true; - result.result = baseFailureMessage; - } - } - testLoop.commandFactory = originalCommandFactory; - return result; - }; - return expectFailureCommand; - }; -}; - -function executeCommandAndReturnFailureMessage(baseHandler, originalArguments) { - var baseResult = baseHandler.execute.apply(baseHandler, originalArguments); - if (baseResult.passed) { - return null; - } - return baseResult.failureMessage; -}; +} -function executeCommandAndReturnErrorMessage(baseHandler, originalArguments) { - try { - baseHandler.execute.apply(baseHandler, originalArguments); +function decorateFunctionWithTimeout(f, timeout) { + if (f == null) { return null; } - catch (expected) { - return expected.message; + if (isNaN(timeout)) { + throw new SeleniumError("Timeout is not a number: " + timeout); } -}; - + var now = new Date().getTime(); + var timeoutTime = now + timeout; + return function() { + if (new Date().getTime() > timeoutTime) { + throw new SeleniumError("Timed out after " + timeout + "ms"); + } + return f(); + }; +} diff --git a/tests/test_tools/selenium/core/scripts/selenium-logging.js b/tests/test_tools/selenium/core/scripts/selenium-logging.js index b0fc67e4..25e11463 100644 --- a/tests/test_tools/selenium/core/scripts/selenium-logging.js +++ b/tests/test_tools/selenium/core/scripts/selenium-logging.js @@ -1,33 +1,31 @@ /* -* Copyright 2004 ThoughtWorks, Inc -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ + * Copyright 2004 ThoughtWorks, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ var Logger = function() { this.logWindow = null; } Logger.prototype = { + pendingMessages: new Array(), + setLogLevelThreshold: function(logLevel) { - this.pendingLogLevelThreshold = logLevel; + this.pendingLogLevelThreshold = logLevel; this.show(); - // - // The following message does not show up in the log -- _unless_ I step along w/ the debugger - // down to the append call. I believe this is because the new log window has not yet loaded, - // and therefore the log msg is discarded; but if I step through the debugger, this changes - // the scheduling so as to load that window and make it ready. - // this.info("Log level programmatically set to " + logLevel + " (presumably by driven-mode test code)"); + // NOTE: log messages will be discarded until the log window is + // fully loaded. }, getLogWindow: function() { @@ -37,10 +35,12 @@ Logger.prototype = { if (this.logWindow && this.pendingLogLevelThreshold && this.logWindow.setThresholdLevel) { this.logWindow.setThresholdLevel(this.pendingLogLevelThreshold); - // can't just directly log because that action would loop back to this code infinitely - this.pendingInfoMessage = "Log level programmatically set to " + this.pendingLogLevelThreshold + " (presumably by driven-mode test code)"; + // can't just directly log because that action would loop back + // to this code infinitely + var pendingMessage = new LogMessage("info", "Log level programmatically set to " + this.pendingLogLevelThreshold + " (presumably by driven-mode test code)"); + this.pendingMessages.push(pendingMessage); - this.pendingLogLevelThreshold = null; // let's only go this way one time + this.pendingLogLevelThreshold = null; // let's only go this way one time } return this.logWindow; @@ -49,8 +49,9 @@ Logger.prototype = { openLogWindow: function() { this.logWindow = window.open( getDocumentBase(document) + "SeleniumLog.html", "SeleniumLog", - "width=600,height=250,bottom=0,right=0,status,scrollbars,resizable" + "width=600,height=1000,bottom=0,right=0,status,scrollbars,resizable" ); + this.logWindow.moveTo(window.screenX + 1210, window.screenY + window.outerHeight - 1400); return this.logWindow; }, @@ -60,28 +61,42 @@ Logger.prototype = { } }, + logHook: function(message, className) { + }, + log: function(message, className) { var logWindow = this.getLogWindow(); + this.logHook(message, className); if (logWindow) { if (logWindow.append) { - if (this.pendingInfoMessage) { - logWindow.append("info: " + this.pendingInfoMessage, "info"); - this.pendingInfoMessage = null; + if (this.pendingMessages.length > 0) { + logWindow.append("info: Appending missed logging messages", "info"); + while (this.pendingMessages.length > 0) { + var msg = this.pendingMessages.shift(); + logWindow.append(msg.type + ": " + msg.msg, msg.type); + } + logWindow.append("info: Done appending missed logging messages", "info"); } logWindow.append(className + ": " + message, className); } + } else { + // uncomment this to turn on background logging + /* these logging messages are never flushed, which creates + an enormous array of strings that never stops growing. Only + turn this on if you need it for debugging! */ + //this.pendingMessages.push(new LogMessage(message, className)); } }, close: function(message) { - if (this.logWindow != null) { - try { - this.logWindow.close(); - } catch (e) { - // swallow exception - // the window is probably closed if we get an exception here - } - this.logWindow = null; + if (this.logWindow != null) { + try { + this.logWindow.close(); + } catch (e) { + // swallow exception + // the window is probably closed if we get an exception here + } + this.logWindow = null; } }, @@ -110,3 +125,7 @@ Logger.prototype = { var LOG = new Logger(); +var LogMessage = function(msg, type) { + this.type = type; + this.msg = msg; +} diff --git a/tests/test_tools/selenium/core/scripts/selenium-seleneserunner.js b/tests/test_tools/selenium/core/scripts/selenium-seleneserunner.js index 041b3bf9..99c7efbc 100644 --- a/tests/test_tools/selenium/core/scripts/selenium-seleneserunner.js +++ b/tests/test_tools/selenium/core/scripts/selenium-seleneserunner.js @@ -23,278 +23,429 @@ doneColor = "#FFFFCC"; slowMode = false; +var injectedSessionId; var cmd1 = document.createElement("div"); var cmd2 = document.createElement("div"); var cmd3 = document.createElement("div"); var cmd4 = document.createElement("div"); var postResult = "START"; +var debugMode = false; +var relayToRC = null; +var proxyInjectionMode = false; +var uniqueId = 'sel_' + Math.round(100000 * Math.random()); + +var SeleneseRunnerOptions = Class.create(); +Object.extend(SeleneseRunnerOptions.prototype, URLConfiguration.prototype); +Object.extend(SeleneseRunnerOptions.prototype, { + initialize: function() { + this._acquireQueryString(); + }, + getDebugMode: function() { + return this._getQueryParameter("debugMode"); + }, + + getContinue: function() { + return this._getQueryParameter("continue"); + }, + + getBaseUrl: function() { + return this._getQueryParameter("baseUrl"); + }, + + getDriverHost: function() { + return this._getQueryParameter("driverhost"); + }, + + getDriverPort: function() { + return this._getQueryParameter("driverport"); + }, + + getSessionId: function() { + return this._getQueryParameter("sessionId"); + }, + + _acquireQueryString: function () { + if (this.queryString) return; + if (browserVersion.isHTA) { + var args = this._extractArgs(); + if (args.length < 2) return null; + this.queryString = args[1]; + } else if (proxyInjectionMode) { + this.queryString = selenium.browserbot.getCurrentWindow().location.search.substr(1); + } else { + this.queryString = top.location.search.substr(1); + } + } -queryString = null; +}); +var runOptions; -function runTest() { - var testAppFrame = document.getElementById('myiframe'); - selenium = Selenium.createForFrame(testAppFrame); +function runSeleniumTest() { + runOptions = new SeleneseRunnerOptions(); + var testAppWindow; + + if (runOptions.isMultiWindowMode()) { + testAppWindow = openSeparateApplicationWindow('Blank.html'); + } else if ($('myiframe') != null) { + testAppWindow = $('myiframe').contentWindow; + } + else { + proxyInjectionMode = true; + testAppWindow = window; + } + selenium = Selenium.createForWindow(testAppWindow); + if (!debugMode) { + debugMode = runOptions.getDebugMode(); + } + if (proxyInjectionMode) { + LOG.log = logToRc; + selenium.browserbot._modifyWindow(testAppWindow); + } + else if (debugMode) { + LOG.logHook = logToRc; + } + window.selenium = selenium; commandFactory = new CommandHandlerFactory(); commandFactory.registerAll(selenium); - testLoop = new TestLoop(commandFactory); - - testLoop.nextCommand = nextCommand; - testLoop.commandStarted = commandStarted; - testLoop.commandComplete = commandComplete; - testLoop.commandError = commandError; - testLoop.requiresCallBack = true; - testLoop.testComplete = function() { - window.status = "Selenium Tests Complete, for this Test" - // Continue checking for new results - testLoop.continueTest(); - postResult = "START"; - }; - - document.getElementById("commandList").appendChild(cmd4); - document.getElementById("commandList").appendChild(cmd3); - document.getElementById("commandList").appendChild(cmd2); - document.getElementById("commandList").appendChild(cmd1); - - var doContinue = getQueryVariable("continue"); - if (doContinue != null) postResult = "OK"; - - testLoop.start(); -} + currentTest = new SeleneseRunner(commandFactory); -function getQueryString() { - if (queryString != null) return queryString; - if (browserVersion.isHTA) { - var args = extractArgs(); - if (args.length < 2) return null; - queryString = args[1]; - return queryString; - } else { - return location.search.substr(1); - } + if (document.getElementById("commandList") != null) { + document.getElementById("commandList").appendChild(cmd4); + document.getElementById("commandList").appendChild(cmd3); + document.getElementById("commandList").appendChild(cmd2); + document.getElementById("commandList").appendChild(cmd1); + } + + var doContinue = runOptions.getContinue(); + if (doContinue != null) postResult = "OK"; + + currentTest.start(); } -function extractArgs() { - var str = SeleniumHTARunner.commandLine; - if (str == null || str == "") return new Array(); - var matches = str.match(/(?:"([^"]+)"|(?!"([^"]+)")(\S+))/g); - // We either want non quote stuff ([^"]+) surrounded by quotes - // or we want to look-ahead, see that the next character isn't - // a quoted argument, and then grab all the non-space stuff - // this will return for the line: "foo" bar - // the results "\"foo\"" and "bar" - - // So, let's unquote the quoted arguments: - var args = new Array; - for (var i = 0; i < matches.length; i++) { - args[i] = matches[i]; - args[i] = args[i].replace(/^"(.*)"$/, "$1"); +function buildBaseUrl() { + var baseUrl = runOptions.getBaseUrl(); + if (baseUrl != null) { + return baseUrl; } - return args; + var s = window.location.href + var slashPairOffset = s.indexOf("//") + "//".length + var pathSlashOffset = s.substring(slashPairOffset).indexOf("/") + return s.substring(0, slashPairOffset + pathSlashOffset) + "/selenium-server/core/"; } -function getQueryVariable(variable) { - var query = getQueryString(); - if (query == null) return null; - var vars = query.split("&"); - for (var i=0;i<vars.length;i++) { - var pair = vars[i].split("="); - if (pair[0] == variable) { - return pair[1]; - } +function logToRc(message, logLevel) { + if (logLevel == null) { + logLevel = "debug"; + } + if (debugMode) { + sendToRC("logLevel=" + logLevel + ":" + message.replace(/[\n\r\015]/g, " ") + "\n"); } } -function buildBaseUrl() { - var baseUrl = getQueryVariable("baseUrl"); - if (baseUrl != null) return baseUrl; - var lastSlash = window.location.href.lastIndexOf('/'); - baseUrl = window.location.href.substring(0, lastSlash+1); - return baseUrl; +function isArray(x) { + return ((typeof x) == "object") && (x["length"] != null); } -function buildDriverParams() { - var params = ""; +function serializeString(name, s) { + return name + "=unescape(\"" + escape(s) + "\");"; +} - var host = getQueryVariable("driverhost"); - var port = getQueryVariable("driverport"); - if (host != undefined && port != undefined) { - params = params + "&driverhost=" + host + "&driverport=" + port; +function serializeObject(name, x) +{ + var s = ''; + + if (isArray(x)) + { + s = name + "=new Array(); "; + var len = x["length"]; + for (var j = 0; j < len; j++) + { + s += serializeString(name + "[" + j + "]", x[j]); + } } - - var sessionId = getQueryVariable("sessionId"); - if (sessionId != undefined) { - params = params + "&sessionId=" + sessionId; + else if (typeof x == "string") + { + s = serializeString(name, x); + } + else + { + throw "unrecognized object not encoded: " + name + "(" + x + ")"; } + return s; +} - return params; +function relayBotToRC(s) { } -function preventBrowserCaching() { - var t = (new Date()).getTime(); - return "&counterToMakeURsUniqueAndSoStopPageCachingInTheBrowser=" + t; -} +function setSeleniumWindowName(seleniumWindowName) { + selenium.browserbot.getCurrentWindow()['seleniumWindowName'] = seleniumWindowName; +} -function nextCommand() { - xmlHttp = XmlHttp.create(); - try { - - var url = buildBaseUrl(); +function slowClicked() { + slowMode = !slowMode; +} + +SeleneseRunner = Class.create(); +Object.extend(SeleneseRunner.prototype, new TestLoop()); +Object.extend(SeleneseRunner.prototype, { + initialize : function(commandFactory) { + this.commandFactory = commandFactory; + this.requiresCallBack = true; + this.commandNode = null; + this.xmlHttpForCommandsAndResults = null; + }, + + nextCommand : function() { + var urlParms = ""; if (postResult == "START") { - url = url + "driver/?seleniumStart=true" + buildDriverParams() + preventBrowserCaching(); + urlParms += "seleniumStart=true"; + } + this.xmlHttpForCommandsAndResults = XmlHttp.create(); + sendToRC(postResult, urlParms, this._HandleHttpResponse.bind(this), this.xmlHttpForCommandsAndResults); + }, + + commandStarted : function(command) { + this.commandNode = document.createElement("div"); + var innerHTML = command.command + '('; + if (command.target != null && command.target != "") { + innerHTML += command.target; + if (command.value != null && command.value != "") { + innerHTML += ', ' + command.value; + } + } + innerHTML += ")"; + this.commandNode.innerHTML = innerHTML; + this.commandNode.style.backgroundColor = workingColor; + if (document.getElementById("commandList") != null) { + document.getElementById("commandList").removeChild(cmd1); + document.getElementById("commandList").removeChild(cmd2); + document.getElementById("commandList").removeChild(cmd3); + document.getElementById("commandList").removeChild(cmd4); + cmd4 = cmd3; + cmd3 = cmd2; + cmd2 = cmd1; + cmd1 = this.commandNode; + document.getElementById("commandList").appendChild(cmd4); + document.getElementById("commandList").appendChild(cmd3); + document.getElementById("commandList").appendChild(cmd2); + document.getElementById("commandList").appendChild(cmd1); + } + }, + + commandComplete : function(result) { + + if (result.failed) { + if (postResult == "CONTINUATION") { + currentTest.aborted = true; + } + postResult = result.failureMessage; + this.commandNode.title = result.failureMessage; + this.commandNode.style.backgroundColor = failColor; + } else if (result.passed) { + postResult = "OK"; + this.commandNode.style.backgroundColor = passColor; } else { - url = url + "driver/?" + buildDriverParams() + preventBrowserCaching(); + if (result.result == null) { + postResult = "OK"; + } else { + postResult = "OK," + result.result; + } + this.commandNode.style.backgroundColor = doneColor; } - LOG.debug("XMLHTTPRequesting " + url); - xmlHttp.open("POST", url, true); - xmlHttp.onreadystatechange=handleHttpResponse; - xmlHttp.send(postResult); - } catch(e) { - var s = 'xmlHttp returned:\n' - for (key in e) { - s += "\t" + key + " -> " + e[key] + "\n" + }, + + commandError : function(message) { + postResult = "ERROR: " + message; + this.commandNode.style.backgroundColor = errorColor; + this.commandNode.title = message; + }, + + testComplete : function() { + window.status = "Selenium Tests Complete, for this Test" + // Continue checking for new results + this.continueTest(); + postResult = "START"; + }, + + _HandleHttpResponse : function() { + if (this.xmlHttpForCommandsAndResults.readyState == 4) { + if (this.xmlHttpForCommandsAndResults.status == 200) { + var command = this._extractCommand(this.xmlHttpForCommandsAndResults); + this.currentCommand = command; + this.continueTestAtCurrentCommand(); + } else { + var s = 'xmlHttp returned: ' + this.xmlHttpForCommandsAndResults.status + ": " + this.xmlHttpForCommandsAndResults.statusText; + LOG.error(s); + this.currentCommand = null; + setTimeout(this.continueTestAtCurrentCommand.bind(this), 2000); + } + } - LOG.error(s); - return null; - } - return null; -} + }, - function handleHttpResponse() { - if (xmlHttp.readyState == 4) { - if (xmlHttp.status == 200) { - var command = extractCommand(xmlHttp); - testLoop.currentCommand = command; - testLoop.beginNextTest(); - } else { - var s = 'xmlHttp returned: ' + xmlHttp.status + ": " + xmlHttp.statusText; - LOG.error(s); - testLoop.currentCommand = null; - setTimeout("testLoop.beginNextTest();", 2000); - } - - } - } - - -function extractCommand(xmlHttp) { - if (slowMode) { - delay(2000); + _extractCommand : function(xmlHttp) { + if (slowMode) { + this._delay(2000); + } + + var command; + try { + var re = new RegExp("^(.*?)\n((.|[\r\n])*)"); + if (re.exec(xmlHttp.responseText)) { + command = RegExp.$1; + var rest = RegExp.$2; + rest = rest.trim(); + if (rest) { + eval(rest); + } + } + else { + command = xmlHttp.responseText; + } + } catch (e) { + alert('could not get responseText: ' + e.message); + } + if (command.substr(0, '|testComplete'.length) == '|testComplete') { + return null; + } + + return this._createCommandFromRequest(command); + }, + + + _delay : function(millis) { + var startMillis = new Date(); + while (true) { + milli = new Date(); + if (milli - startMillis > millis) { + break; + } + } + }, + +// Parses a URI query string into a SeleniumCommand object + _createCommandFromRequest : function(commandRequest) { + //decodeURIComponent doesn't strip plus signs + var processed = commandRequest.replace(/\+/g, "%20"); + // strip trailing spaces + var processed = processed.replace(/\s+$/, ""); + var vars = processed.split("&"); + var cmdArgs = new Object(); + for (var i = 0; i < vars.length; i++) { + var pair = vars[i].split("="); + cmdArgs[pair[0]] = pair[1]; + } + var cmd = cmdArgs['cmd']; + var arg1 = cmdArgs['1']; + if (null == arg1) arg1 = ""; + arg1 = decodeURIComponent(arg1); + var arg2 = cmdArgs['2']; + if (null == arg2) arg2 = ""; + arg2 = decodeURIComponent(arg2); + if (cmd == null) { + throw new Error("Bad command request: " + commandRequest); + } + return new SeleniumCommand(cmd, arg1, arg2); } - var command; - try { - command = xmlHttp.responseText; - } catch (e) { - alert('could not get responseText: ' + e.message); +}) + + +function sendToRC(dataToBePosted, urlParms, callback, xmlHttpObject, async) { + if (async == null) { + async = true; } - if (command.substr(0,'|testComplete'.length)=='|testComplete') { - return null; + if (xmlHttpObject == null) { + xmlHttpObject = XmlHttp.create(); } + var url = buildBaseUrl() + "driver/?" + if (urlParms) { + url += urlParms; + } + url += "&localFrameAddress=" + (proxyInjectionMode ? makeAddressToAUTFrame() : "top"); + url += "&seleniumWindowName=" + getSeleniumWindowName(); + url += "&uniqueId=" + uniqueId; - return createCommandFromRequest(command); -} - -function commandStarted(command) { - commandNode = document.createElement("div"); - innerHTML = command.command + '('; - if (command.target != null && command.target != "") { - innerHTML += command.target; - if (command.value != null && command.value != "") { - innerHTML += ', ' + command.value; - } + if (callback == null) { + callback = function() { + }; } - innerHTML += ")"; - commandNode.innerHTML = innerHTML; - commandNode.style.backgroundColor = workingColor; - document.getElementById("commandList").removeChild(cmd1); - document.getElementById("commandList").removeChild(cmd2); - document.getElementById("commandList").removeChild(cmd3); - document.getElementById("commandList").removeChild(cmd4); - cmd4 = cmd3; - cmd3 = cmd2; - cmd2 = cmd1; - cmd1 = commandNode; - document.getElementById("commandList").appendChild(cmd4); - document.getElementById("commandList").appendChild(cmd3); - document.getElementById("commandList").appendChild(cmd2); - document.getElementById("commandList").appendChild(cmd1); + url += buildDriverParams() + preventBrowserCaching(); + xmlHttpObject.open("POST", url, async); + xmlHttpObject.onreadystatechange = callback; + xmlHttpObject.send(dataToBePosted); + return null; } -function commandComplete(result) { - if (result.failed) { - if (postResult == "CONTINUATION") { - testLoop.aborted = true; - } - postResult = result.failureMessage; - commandNode.title = result.failureMessage; - commandNode.style.backgroundColor = failColor; - } else if (result.passed) { - postResult = "OK"; - commandNode.style.backgroundColor = passColor; - } else { - if (result.result == null) { - postResult = "OK"; - } else { - postResult = "OK," + result.result; - } - commandNode.style.backgroundColor = doneColor; +function buildDriverParams() { + var params = ""; + + var host = runOptions.getDriverHost(); + var port = runOptions.getDriverPort(); + if (host != undefined && port != undefined) { + params = params + "&driverhost=" + host + "&driverport=" + port; } -} -function commandError(message) { - postResult = "ERROR: " + message; - commandNode.style.backgroundColor = errorColor; - commandNode.title = message; + var sessionId = runOptions.getSessionId(); + if (sessionId == undefined) { + sessionId = injectedSessionId; + } + if (sessionId != undefined) { + params = params + "&sessionId=" + sessionId; + } + return params; } -function slowClicked() { - slowMode = !slowMode; +function preventBrowserCaching() { + var t = (new Date()).getTime(); + return "&counterToMakeURsUniqueAndSoStopPageCachingInTheBrowser=" + t; } -function delay(millis) { - startMillis = new Date(); - while (true) { - milli = new Date(); - if (milli-startMillis > millis) { - break; - } +// Return the name of the current window in the selenium recordkeeping. +// +// In selenium, the additional widow has no name. +// +// Additional pop-ups are associated with names given by the argument to the routine waitForPopUp. +// +// I try to arrange for widows which are opened in such manner to track their own names using the top-level property +// seleniumWindowName, but it is possible that this property will not be available (if the widow has just reloaded +// itself). In this case, return "?". +// +function getSeleniumWindowName() { + var w = (proxyInjectionMode ? selenium.browserbot.getCurrentWindow() : window); + if (w.opener == null) { + return ""; } + if (w["seleniumWindowName"] == null) { + return "?"; + } + return w["seleniumWindowName"]; } -function getIframeDocument(iframe) { - if (iframe.contentDocument) { - return iframe.contentDocument; - } - else { - return iframe.contentWindow.document; +// construct a JavaScript expression which leads to my frame (i.e., the frame containing the window +// in which this code is operating) +function makeAddressToAUTFrame(w, frameNavigationalJSexpression) +{ + if (w == null) + { + w = top; + frameNavigationalJSexpression = "top"; } -} -// Parses a URI query string into a SeleniumCommand object -function createCommandFromRequest(commandRequest) { - //decodeURIComponent doesn't strip plus signs - var processed = commandRequest.replace(/\+/g, "%20"); - // strip trailing spaces - var processed = processed.replace(/\s+$/, ""); - var vars = processed.split("&"); - var cmdArgs = new Object(); - for (var i=0;i<vars.length;i++) { - var pair = vars[i].split("="); - cmdArgs[pair[0]] = pair[1]; + if (w == selenium.browserbot.getCurrentWindow()) + { + return frameNavigationalJSexpression; } - var cmd = cmdArgs['cmd']; - var arg1 = cmdArgs['1']; - if (null == arg1) arg1 = ""; - arg1 = decodeURIComponent(arg1); - var arg2 = cmdArgs['2']; - if (null == arg2) arg2 = ""; - arg2 = decodeURIComponent(arg2); - if (cmd == null) { - throw new Error("Bad command request: " + commandRequest); + for (var j = 0; j < w.frames.length; j++) + { + var t = makeAddressToAUTFrame(w.frames[j], frameNavigationalJSexpression + ".frames[" + j + "]"); + if (t != null) + { + return t; + } } - return new SeleniumCommand(cmd, arg1, arg2); + return null; } - diff --git a/tests/test_tools/selenium/core/scripts/selenium-testrunner.js b/tests/test_tools/selenium/core/scripts/selenium-testrunner.js index 1ced0a11..b5104d39 100644 --- a/tests/test_tools/selenium/core/scripts/selenium-testrunner.js +++ b/tests/test_tools/selenium/core/scripts/selenium-testrunner.js @@ -15,433 +15,584 @@ * */ -// The current row in the list of tests (test suite) -currentRowInSuite = 0; +// An object representing the current test, used external +var currentTest = null; // TODO: get rid of this global, which mirrors the htmlTestRunner.currentTest +var selenium = null; + +var htmlTestRunner; +var HtmlTestRunner = Class.create(); +Object.extend(HtmlTestRunner.prototype, { + initialize: function() { + this.metrics = new Metrics(); + this.controlPanel = new HtmlTestRunnerControlPanel(); + this.htmlTestSuite = null; + this.testFailed = false; + this.currentTest = null; + this.runAllTests = false; + this.appWindow = null; + // we use a timeout here to make sure the LOG has loaded first, so we can see _every_ error + setTimeout(function() { + this.loadSuiteFrame(); + }.bind(this), 500); + }, -// An object representing the current test -currentTest = null; + markFailed: function() { + this.testFailed = true; + this.htmlTestSuite.markFailed(); + }, -// Whether or not the jsFT should run all tests in the suite -runAllTests = false; + loadSuiteFrame: function() { + if (selenium == null) { + selenium = Selenium.createForWindow(this._getApplicationWindow()); + this._registerCommandHandlers(); + } + this.controlPanel.setHighlightOption(); + var testSuiteName = this.controlPanel.getTestSuiteName(); + if (testSuiteName) { + suiteFrame.load(testSuiteName, this._onloadTestSuite.bind(this)); + } + }, -// Whether or not the current test has any errors; -testFailed = false; -suiteFailed = false; + _getApplicationWindow: function () { + if (this.controlPanel.isMultiWindowMode()) { + return this._getSeparateApplicationWindow(); + } + return $('myiframe').contentWindow; + }, -// Colors used to provide feedback -passColor = "#ccffcc"; -doneColor = "#eeffee"; -failColor = "#ffcccc"; -workingColor = "#ffffcc"; + _getSeparateApplicationWindow: function () { + if (this.appWindow == null) { + this.appWindow = openSeparateApplicationWindow('TestRunner-splash.html'); + } + return this.appWindow; + }, -// Holds the handlers for each command. -commandHandlers = null; + _onloadTestSuite:function () { + this.htmlTestSuite = new HtmlTestSuite(suiteFrame.getDocument()); + if (! this.htmlTestSuite.isAvailable()) { + return; + } + if (this.controlPanel.isAutomatedRun()) { + htmlTestRunner.startTestSuite(); + } else if (this.controlPanel.getAutoUrl()) { + //todo what is the autourl doing, left to check it out + addLoadListener(this._getApplicationWindow(), this._startSingleTest.bind(this)); + this._getApplicationWindow().src = this.controlPanel.getAutoUrl(); + } else { + this.htmlTestSuite.getSuiteRows()[0].loadTestCase(); + } + }, -// The number of tests run -numTestPasses = 0; + _startSingleTest:function () { + removeLoadListener(getApplicationWindow(), this._startSingleTest.bind(this)); + var singleTestName = this.controlPanel.getSingleTestName(); + testFrame.load(singleTestName, this.startTest.bind(this)); + }, -// The number of tests that have failed -numTestFailures = 0; + _registerCommandHandlers: function () { + this.commandFactory = new CommandHandlerFactory(); + this.commandFactory.registerAll(selenium); + }, -// The number of commands which have passed -numCommandPasses = 0; + startTestSuite: function() { + this.controlPanel.reset(); + this.metrics.resetMetrics(); + this.htmlTestSuite.reset(); + this.runAllTests = true; + this.runNextTest(); + }, -// The number of commands which have failed -numCommandFailures = 0; + runNextTest: function () { + if (!this.runAllTests) { + return; + } + this.htmlTestSuite.runNextTestInSuite(); + }, -// The number of commands which have caused errors (element not found) -numCommandErrors = 0; + startTest: function () { + this.controlPanel.reset(); + testFrame.scrollToTop(); + //todo: move testFailed and storedVars to TestCase + this.testFailed = false; + storedVars = new Object(); + this.currentTest = new HtmlRunnerTestLoop(testFrame.getCurrentTestCase(), this.metrics, this.commandFactory); + currentTest = this.currentTest; + this.currentTest.start(); + }, -// The time that the test was started. -startTime = null; + runSingleTest:function() { + this.runAllTests = false; + this.metrics.resetMetrics(); + this.startTest(); + } +}); -// The current time. -currentTime = null; +var FeedbackColors = Class.create(); +Object.extend(FeedbackColors, { + passColor : "#ccffcc", + doneColor : "#eeffee", + failColor : "#ffcccc", + workingColor : "#ffffcc", + breakpointColor : "#cccccc" +}); -// An simple enum for failureType -ERROR = 0; -FAILURE = 1; -runInterval = 0; +var runInterval = 0; -queryString = null; -function setRunInterval() { - // Get the value of the checked runMode option. - // There should be a way of getting the value of the "group", but I don't know how. - var runModeOptions = document.forms['controlPanel'].runMode; - for (var i = 0; i < runModeOptions.length; i++) { - if (runModeOptions[i].checked) { - runInterval = runModeOptions[i].value; - break; - } - } -} +/** SeleniumFrame encapsulates an iframe element */ +var SeleniumFrame = Class.create(); +Object.extend(SeleniumFrame.prototype, { -function continueCurrentTest() { - document.getElementById('continueTest').disabled = true; - testLoop.resume(); -} + initialize : function(frame) { + this.frame = frame; + addLoadListener(this.frame, this._handleLoad.bind(this)); + }, -function getApplicationFrame() { - return document.getElementById('myiframe'); -} + getDocument : function() { + return this.frame.contentWindow.document; + }, -function getSuiteFrame() { - return document.getElementById('testSuiteFrame'); -} + _handleLoad: function() { + this._onLoad(); + if (this.loadCallback) { + this.loadCallback(); + this.loadCallback = null; + } + }, -function getTestFrame(){ - return document.getElementById('testFrame'); -} + _onLoad: function() { + }, -function loadAndRunIfAuto() { - loadSuiteFrame(); -} + scrollToTop : function() { + this.frame.contentWindow.scrollTo(0, 0); + }, -function start() { - queryString = null; - setRunInterval(); - loadSuiteFrame(); -} + _setLocation: function(location) { + if (browserVersion.isSafari) { + // safari doesn't reload the page when the location equals to current location. + // hence, set the location to blank so that the page will reload automatically. + this.frame.src = "about:blank"; + this.frame.src = location; + } else { + this.frame.contentWindow.location.replace(location); + } + }, -function loadSuiteFrame() { - var testAppFrame = document.getElementById('myiframe'); - selenium = Selenium.createForFrame(testAppFrame); - registerCommandHandlers(); + load: function(/* url, [callback] */) { + if (arguments.length > 1) { + this.loadCallback = arguments[1]; - //set the runInterval if there is a queryParameter for it - var tempRunInterval = getQueryParameter("runInterval"); - if (tempRunInterval) { - runInterval = tempRunInterval; + } + this._setLocation(arguments[0]); } - document.getElementById("modeRun").onclick = setRunInterval; - document.getElementById('modeWalk').onclick = setRunInterval; - document.getElementById('modeStep').onclick = setRunInterval; - document.getElementById('continueTest').onclick = continueCurrentTest; +}); + +/** HtmlTestFrame - encapsulates the test-case iframe element */ +var HtmlTestFrame = Class.create(); +Object.extend(HtmlTestFrame.prototype, SeleniumFrame.prototype); +Object.extend(HtmlTestFrame.prototype, { - var testSuiteName = getQueryParameter("test"); + _onLoad: function() { + this.setCurrentTestCase(); + }, + + setCurrentTestCase: function() { + //todo: this is not good looking + this.currentTestCase = new HtmlTestCase(this.getDocument(), htmlTestRunner.htmlTestSuite.getCurrentRow()); + }, - if (testSuiteName) { - addLoadListener(getSuiteFrame(), onloadTestSuite); - getSuiteFrame().src = testSuiteName; - } else { - onloadTestSuite(); + getCurrentTestCase: function() { + return this.currentTestCase; } -} -function startSingleTest() { - removeLoadListener(getApplicationFrame(), startSingleTest); - var singleTestName = getQueryParameter("singletest"); - addLoadListener(getTestFrame(), startTest); - getTestFrame().src = singleTestName; +}); + +function onSeleniumLoad() { + suiteFrame = new SeleniumFrame(getSuiteFrame()); + testFrame = new HtmlTestFrame(getTestFrame()); + htmlTestRunner = new HtmlTestRunner(); } -function getIframeDocument(iframe) -{ - if (iframe.contentDocument) { - return iframe.contentDocument; - } - else { - return iframe.contentWindow.document; + +var suiteFrame; +var testFrame; +function getSuiteFrame() { + var f = $('testSuiteFrame'); + if (f == null) { + f = top; + // proxyInjection mode does not set myiframe } + return f; } -function onloadTestSuite() { - removeLoadListener(getSuiteFrame(), onloadTestSuite); - - // Add an onclick function to each link in all suite tables - var allTables = getIframeDocument(getSuiteFrame()).getElementsByTagName("table"); - for (var tableNum = 0; tableNum < allTables.length; tableNum++) - { - var skippedTable = allTables[tableNum]; - for(rowNum = 1;rowNum < skippedTable.rows.length; rowNum++) { - addOnclick(skippedTable, rowNum); - } +function getTestFrame() { + var f = $('testFrame'); + if (f == null) { + f = top; + // proxyInjection mode does not set myiframe } + return f; +} + +var HtmlTestRunnerControlPanel = Class.create(); +Object.extend(HtmlTestRunnerControlPanel.prototype, URLConfiguration.prototype); +Object.extend(HtmlTestRunnerControlPanel.prototype, { + initialize: function() { + this._acquireQueryString(); + + this.runInterval = 0; - suiteTable = getIframeDocument(getSuiteFrame()).getElementsByTagName("table")[0]; - if (suiteTable!=null) { + this.highlightOption = $('highlightOption'); + this.pauseButton = $('pauseTest'); + this.stepButton = $('stepTest'); - if (isAutomatedRun()) { - startTestSuite(); - } else if (getQueryParameter("autoURL")) { + this.highlightOption.onclick = (function() { + this.setHighlightOption(); + }).bindAsEventListener(this); + this.pauseButton.onclick = this.pauseCurrentTest.bindAsEventListener(this); + this.stepButton.onclick = this.stepCurrentTest.bindAsEventListener(this); - addLoadListener(getApplicationFrame(), startSingleTest); + this.speedController = new Control.Slider('speedHandle', 'speedTrack', { + range: $R(0, 1000), + onSlide: this.setRunInterval.bindAsEventListener(this), + onChange: this.setRunInterval.bindAsEventListener(this) + }); - getApplicationFrame().src = getQueryParameter("autoURL"); + this._parseQueryParameter(); + }, + + setHighlightOption: function () { + var isHighlight = this.highlightOption.checked; + selenium.browserbot.getCurrentPage().setHighlightElement(isHighlight); + }, - } else { - testLink = suiteTable.rows[currentRowInSuite+1].cells[0].getElementsByTagName("a")[0]; - getTestFrame().src = testLink.href; + _parseQueryParameter: function() { + var tempRunInterval = this._getQueryParameter("runInterval"); + if (tempRunInterval) { + this.setRunInterval(tempRunInterval); } - } -} + this.highlightOption.checked = this._getQueryParameter("highlight"); + }, -// Adds an onclick function to the link in the given row in suite table. -// This function checks whether the test has already been run and the data is -// stored. If the data is stored, it sets the test frame to be the stored data. -// Otherwise, it loads the fresh page. -function addOnclick(suiteTable, rowNum) { - aLink = suiteTable.rows[rowNum].cells[0].getElementsByTagName("a")[0]; - aLink.onclick = function(eventObj) { - srcObj = null; + setRunInterval: function(runInterval) { + this.runInterval = runInterval; + }, - // For mozilla-like browsers - if(eventObj) - srcObj = eventObj.target; + setToPauseAtNextCommand: function() { + this.runInterval = -1; + }, - // For IE-like browsers - else if (getSuiteFrame().contentWindow.event) - srcObj = getSuiteFrame().contentWindow.event.srcElement; + pauseCurrentTest: function () { + this.setToPauseAtNextCommand(); + this._switchPauseButtonToContinue(); + }, - // The target row (the event source is not consistently reported by browsers) - row = srcObj.parentNode.parentNode.rowIndex || srcObj.parentNode.parentNode.parentNode.rowIndex; + continueCurrentTest: function () { + this.reset(); + currentTest.resume(); + }, - // If the row has a stored results table, use that - if(suiteTable.rows[row].cells[1]) { - var bodyElement = getIframeDocument(getTestFrame()).body; - - // Create a div element to hold the results table. - var tableNode = getIframeDocument(getTestFrame()).createElement("div"); - var resultsCell = suiteTable.rows[row].cells[1]; - tableNode.innerHTML = resultsCell.innerHTML; - - // Append this text node, and remove all the preceding nodes. - bodyElement.appendChild(tableNode); - while (bodyElement.firstChild != bodyElement.lastChild) { - bodyElement.removeChild(bodyElement.firstChild); - } - } - // Otherwise, just open up the fresh page. - else { - getTestFrame().src = suiteTable.rows[row].cells[0].getElementsByTagName("a")[0].href; - } + reset: function() { + this.runInterval = this.speedController.value; + this._switchContinueButtonToPause(); + }, - return false; - }; -} + _switchContinueButtonToPause: function() { + this.pauseButton.innerHTML = "Pause"; + this.pauseButton.onclick = this.pauseCurrentTest.bindAsEventListener(this); + }, -function isQueryParameterTrue(name) { - parameterValue = getQueryParameter(name); - return (parameterValue != null && parameterValue.toLowerCase() == "true"); -} + _switchPauseButtonToContinue: function() { + $('stepTest').disabled = false; + this.pauseButton.innerHTML = "Continue"; + this.pauseButton.onclick = this.continueCurrentTest.bindAsEventListener(this); + }, -function getQueryString() { - if (queryString != null) return queryString; - if (browserVersion.isHTA) { - var args = extractArgs(); - if (args.length < 2) return null; - queryString = args[1]; - return queryString; - } else { - return location.search.substr(1); - } -} + stepCurrentTest: function () { + this.setToPauseAtNextCommand(); + currentTest.resume(); + }, -function extractArgs() { - var str = SeleniumHTARunner.commandLine; - if (str == null || str == "") return new Array(); - var matches = str.match(/(?:"([^"]+)"|(?!"([^"]+)")(\S+))/g); - // We either want non quote stuff ([^"]+) surrounded by quotes - // or we want to look-ahead, see that the next character isn't - // a quoted argument, and then grab all the non-space stuff - // this will return for the line: "foo" bar - // the results "\"foo\"" and "bar" - - // So, let's unquote the quoted arguments: - var args = new Array; - for (var i = 0; i < matches.length; i++) { - args[i] = matches[i]; - args[i] = args[i].replace(/^"(.*)"$/, "$1"); - } - return args; -} + isAutomatedRun: function() { + return this._isQueryParameterTrue("auto"); + }, + + shouldSaveResultsToFile: function() { + return this._isQueryParameterTrue("save"); + }, + + closeAfterTests: function() { + return this._isQueryParameterTrue("close"); + }, -function getQueryParameter(searchKey) { - var str = getQueryString(); - if (str == null) return null; - var clauses = str.split('&'); - for (var i = 0; i < clauses.length; i++) { - var keyValuePair = clauses[i].split('=',2); - var key = unescape(keyValuePair[0]); - if (key == searchKey) { - return unescape(keyValuePair[1]); + getTestSuiteName: function() { + return this._getQueryParameter("test"); + }, + + getSingleTestName: function() { + return this._getQueryParameter("singletest"); + }, + + getAutoUrl: function() { + return this._getQueryParameter("autoURL"); + }, + + getResultsUrl: function() { + return this._getQueryParameter("resultsUrl"); + }, + + _acquireQueryString: function() { + if (this.queryString) return; + if (browserVersion.isHTA) { + var args = this._extractArgs(); + if (args.length < 2) return null; + this.queryString = args[1]; + } else { + this.queryString = location.search.substr(1); } } - return null; -} -function isNewWindow() { - return isQueryParameterTrue("newWindow"); -} +}); -function isAutomatedRun() { - return isQueryParameterTrue("auto"); -} -function resetMetrics() { - numTestPasses = 0; - numTestFailures = 0; - numCommandPasses = 0; - numCommandFailures = 0; - numCommandErrors = 0; - startTime = new Date().getTime(); -} +var AbstractResultAwareRow = Class.create(); +Object.extend(AbstractResultAwareRow.prototype, { -function runSingleTest() { - runAllTests = false; - resetMetrics(); - startTest(); -} + initialize: function(trElement) { + this.trElement = trElement; + }, -function startTest() { - removeLoadListener(getTestFrame(), startTest); + markWorking: function() { + this.trElement.bgColor = FeedbackColors.workingColor; + safeScrollIntoView(this.trElement); + }, - // Scroll to the top of the test frame - if (getTestFrame().contentWindow) { - getTestFrame().contentWindow.scrollTo(0,0); - } - else { - frames['testFrame'].scrollTo(0,0); + markPassed: function() { + this.trElement.bgColor = FeedbackColors.passColor; + }, + + markDone: function() { + this.trElement.bgColor = FeedbackColors.doneColor; + }, + + markFailed: function() { + this.trElement.bgColor = FeedbackColors.failColor; } - currentTest = new HtmlTest(getIframeDocument(getTestFrame())); +}); - testFailed = false; - storedVars = new Object(); +var HtmlTestCaseRow = Class.create(); +Object.extend(HtmlTestCaseRow.prototype, AbstractResultAwareRow.prototype); +Object.extend(HtmlTestCaseRow.prototype, { - testLoop = initialiseTestLoop(); - testLoop.start(); -} + getCommand: function () { + return new SeleniumCommand(getText(this.trElement.cells[0]), + getText(this.trElement.cells[1]), + getText(this.trElement.cells[2]), + this.isBreakpoint()); + }, -function HtmlTest(testDocument) { - this.init(testDocument); -} + markFailed: function(errorMsg) { + this.trElement.bgColor = FeedbackColors.failColor; + this.setMessage(errorMsg); + }, -HtmlTest.prototype = { + setMessage: function(message) { + this.trElement.cells[2].innerHTML = message; + }, - init: function(testDocument) { - this.document = testDocument; - this.document.bgColor = ""; - this.currentRow = null; - this.commandRows = new Array(); - this.headerRow = null; - var tables = this.document.getElementsByTagName("table"); - for (var i = 0; i < tables.length; i++) { - var candidateRows = tables[i].rows; - for (var j = 0; j < candidateRows.length; j++) { - if (!this.headerRow) { - this.headerRow = candidateRows[j]; - } - if (candidateRows[j].cells.length >= 3) { - this.addCommandRow(candidateRows[j]); - } + reset: function() { + this.trElement.bgColor = ''; + var thirdCell = this.trElement.cells[2]; + if (thirdCell) { + if (thirdCell.originalHTML) { + thirdCell.innerHTML = thirdCell.originalHTML; + } else { + thirdCell.originalHTML = thirdCell.innerHTML; } } }, - addCommandRow: function(row) { - if (row.cells[2] && row.cells[2].originalHTML) { - row.cells[2].innerHTML = row.cells[2].originalHTML; + onClick: function() { + if (this.trElement.isBreakpoint == undefined) { + this.trElement.isBreakpoint = true; + this.trElement.beforeBackgroundColor = Element.getStyle(this.trElement, "backgroundColor"); + Element.setStyle(this.trElement, {"background-color" : FeedbackColors.breakpointColor}); + } else { + this.trElement.isBreakpoint = undefined; + Element.setStyle(this.trElement, {"background-color" : this.trElement.beforeBackgroundColor}); } - row.bgColor = ""; - this.commandRows.push(row); }, - nextCommand: function() { - if (this.commandRows.length > 0) { - this.currentRow = this.commandRows.shift(); - } else { - this.currentRow = null; + addBreakpointSupport: function() { + Element.setStyle(this.trElement, {"cursor" : "pointer"}); + this.trElement.onclick = function() { + this.onClick(); + }.bindAsEventListener(this); + }, + + isBreakpoint: function() { + if (this.trElement.isBreakpoint == undefined || this.trElement.isBreakpoint == null) { + return false } - return this.currentRow; + return this.trElement.isBreakpoint; } +}); + +var HtmlTestSuiteRow = Class.create(); +Object.extend(HtmlTestSuiteRow.prototype, AbstractResultAwareRow.prototype); +Object.extend(HtmlTestSuiteRow.prototype, { + + initialize: function(trElement, testFrame, htmlTestSuite) { + this.trElement = trElement; + this.testFrame = testFrame; + this.htmlTestSuite = htmlTestSuite; + this.link = trElement.getElementsByTagName("a")[0]; + this.link.onclick = this._onClick.bindAsEventListener(this); + }, -}; + reset: function() { + this.trElement.bgColor = ''; + }, -function startTestSuite() { - resetMetrics(); - currentRowInSuite = 0; - runAllTests = true; - suiteFailed = false; + _onClick: function() { + // todo: just send a message to the testSuite + this.loadTestCase(null); + return false; + }, - runNextTest(); -} + loadTestCase: function(onloadFunction) { + this.htmlTestSuite.currentRowInSuite = this.trElement.rowIndex - 1; + // If the row has a stored results table, use that + var resultsFromPreviousRun = this.trElement.cells[1]; + if (resultsFromPreviousRun) { + // this.testFrame.restoreTestCase(resultsFromPreviousRun.innerHTML); + var testBody = this.testFrame.getDocument().body; + testBody.innerHTML = resultsFromPreviousRun.innerHTML; + testFrame.setCurrentTestCase(); + if (onloadFunction) { + onloadFunction(); + } + } else { + this.testFrame.load(this.link.href, onloadFunction); + } + }, -function runNextTest() { - if (!runAllTests) - return; + saveTestResults: function() { + // todo: GLOBAL ACCESS! + var resultHTML = this.testFrame.getDocument().body.innerHTML; + if (!resultHTML) return; - suiteTable = getIframeDocument(getSuiteFrame()).getElementsByTagName("table")[0]; + // todo: why create this div? + var divElement = this.trElement.ownerDocument.createElement("div"); + divElement.innerHTML = resultHTML; - // Do not change the row color of the first row - if (currentRowInSuite > 0) { - // Provide test-status feedback - if (testFailed) { - setCellColor(suiteTable.rows, currentRowInSuite, 0, failColor); - } else { - setCellColor(suiteTable.rows, currentRowInSuite, 0, passColor); - } + var hiddenCell = this.trElement.ownerDocument.createElement("td"); + hiddenCell.appendChild(divElement); + hiddenCell.style.display = "none"; - // Set the results from the previous test run - setResultsData(suiteTable, currentRowInSuite); + this.trElement.appendChild(hiddenCell); } - currentRowInSuite++; +}); - // If we are done with all of the tests, set the title bar as pass or fail - if (currentRowInSuite >= suiteTable.rows.length) { - if (suiteFailed) { - setCellColor(suiteTable.rows, 0, 0, failColor); - } else { - setCellColor(suiteTable.rows, 0, 0, passColor); +var HtmlTestSuite = Class.create(); +Object.extend(HtmlTestSuite.prototype, { + + initialize: function(suiteDocument) { + this.suiteDocument = suiteDocument; + this.suiteRows = this._collectSuiteRows(); + this.titleRow = this.getTestTable().rows[0]; + this.title = this.titleRow.cells[0].innerHTML; + this.reset(); + }, + + reset: function() { + this.failed = false; + this.currentRowInSuite = -1; + this.titleRow.bgColor = ''; + this.suiteRows.each(function(row) { + row.reset(); + }); + }, + + getTitle: function() { + return this.title; + }, + + getSuiteRows: function() { + return this.suiteRows; + }, + + getTestTable: function() { + var tables = $A(this.suiteDocument.getElementsByTagName("table")); + return tables[0]; + }, + + isAvailable: function() { + return this.getTestTable() != null; + }, + + _collectSuiteRows: function () { + var result = []; + for (rowNum = 1; rowNum < this.getTestTable().rows.length; rowNum++) { + var rowElement = this.getTestTable().rows[rowNum]; + result.push(new HtmlTestSuiteRow(rowElement, testFrame, this)); } + return result; + }, - // If this is an automated run (i.e., build script), then submit - // the test results by posting to a form - if (isAutomatedRun()) - postTestResults(suiteFailed, suiteTable); - } + getCurrentRow: function() { + return this.suiteRows[this.currentRowInSuite]; + }, - else { - // Make the current row blue - setCellColor(suiteTable.rows, currentRowInSuite, 0, workingColor); + markFailed: function() { + this.failed = true; + this.titleRow.bgColor = FeedbackColors.failColor; + }, - testLink = suiteTable.rows[currentRowInSuite].cells[0].getElementsByTagName("a")[0]; - testLink.focus(); + markDone: function() { + if (!this.failed) { + this.titleRow.bgColor = FeedbackColors.passColor; + } + }, - var testFrame = getTestFrame(); - addLoadListener(testFrame, startTest); + _startCurrentTestCase: function() { + this.getCurrentRow().markWorking(); + this.getCurrentRow().loadTestCase(htmlTestRunner.startTest.bind(htmlTestRunner)); + }, - selenium.browserbot.setIFrameLocation(testFrame, testLink.href); - } -} + _onTestSuiteComplete: function() { + this.markDone(); + new TestResult(this.failed, this.getTestTable()).post(); + }, -function setCellColor(tableRows, row, col, colorStr) { - tableRows[row].cells[col].bgColor = colorStr; -} + _updateSuiteWithResultOfPreviousTest: function() { + if (this.currentRowInSuite >= 0) { + this.getCurrentRow().saveTestResults(); + } + }, -// Sets the results from a test into a hidden column on the suite table. So, -// for each tests, the second column is set to the HTML from the test table. -function setResultsData(suiteTable, row) { - // Create a text node of the test table - var resultTable = getIframeDocument(getTestFrame()).body.innerHTML; - if (!resultTable) return; + runNextTestInSuite: function() { + this._updateSuiteWithResultOfPreviousTest(); + this.currentRowInSuite++; - var tableNode = suiteTable.ownerDocument.createElement("div"); - tableNode.innerHTML = resultTable; + // If we are done with all of the tests, set the title bar as pass or fail + if (this.currentRowInSuite >= this.suiteRows.length) { + this._onTestSuiteComplete(); + } else { + this._startCurrentTestCase(); + } + } - var new_column = suiteTable.ownerDocument.createElement("td"); - new_column.appendChild(tableNode); - // Set the column to be invisible - new_column.style.display = "none"; - // Add the invisible column - suiteTable.rows[row].appendChild(new_column); -} +}); + +var TestResult = Class.create(); +Object.extend(TestResult.prototype, { // Post the results to a servlet, CGI-script, etc. The URL of the // results-handler defaults to "/postResults", but an alternative location @@ -461,288 +612,584 @@ function setResultsData(suiteTable, row) { // suite: the suite table, including the hidden column of test results // testTable.1 to testTable.N: the individual test tables // -function postTestResults(suiteFailed, suiteTable) { + initialize: function (suiteFailed, suiteTable) { + this.controlPanel = htmlTestRunner.controlPanel; + this.metrics = htmlTestRunner.metrics; + this.suiteFailed = suiteFailed; + this.suiteTable = suiteTable; + }, - form = document.createElement("form"); - document.body.appendChild(form); + post: function () { + if (!this.controlPanel.isAutomatedRun()) { + return; + } + var form = document.createElement("form"); + document.body.appendChild(form); - form.id = "resultsForm"; - form.method="post"; - form.target="myiframe"; + form.id = "resultsForm"; + form.method = "post"; + form.target = "myiframe"; - var resultsUrl = getQueryParameter("resultsUrl"); - if (!resultsUrl) { - resultsUrl = "./postResults"; - } + var resultsUrl = this.controlPanel.getResultsUrl(); + if (!resultsUrl) { + resultsUrl = "./postResults"; + } - var actionAndParameters = resultsUrl.split('?',2); - form.action = actionAndParameters[0]; - var resultsUrlQueryString = actionAndParameters[1]; + var actionAndParameters = resultsUrl.split('?', 2); + form.action = actionAndParameters[0]; + var resultsUrlQueryString = actionAndParameters[1]; + + form.createHiddenField = function(name, value) { + input = document.createElement("input"); + input.type = "hidden"; + input.name = name; + input.value = value; + this.appendChild(input); + }; + + if (resultsUrlQueryString) { + var clauses = resultsUrlQueryString.split('&'); + for (var i = 0; i < clauses.length; i++) { + var keyValuePair = clauses[i].split('=', 2); + var key = unescape(keyValuePair[0]); + var value = unescape(keyValuePair[1]); + form.createHiddenField(key, value); + } + } - form.createHiddenField = function(name, value) { - input = document.createElement("input"); - input.type = "hidden"; - input.name = name; - input.value = value; - this.appendChild(input); - }; + form.createHiddenField("selenium.version", Selenium.version); + form.createHiddenField("selenium.revision", Selenium.revision); + + form.createHiddenField("result", this.suiteFailed ? "failed" : "passed"); + + form.createHiddenField("totalTime", Math.floor((this.metrics.currentTime - this.metrics.startTime) / 1000)); + form.createHiddenField("numTestPasses", this.metrics.numTestPasses); + form.createHiddenField("numTestFailures", this.metrics.numTestFailures); + form.createHiddenField("numCommandPasses", this.metrics.numCommandPasses); + form.createHiddenField("numCommandFailures", this.metrics.numCommandFailures); + form.createHiddenField("numCommandErrors", this.metrics.numCommandErrors); + + // Create an input for each test table. The inputs are named + // testTable.1, testTable.2, etc. + for (rowNum = 1; rowNum < this.suiteTable.rows.length; rowNum++) { + // If there is a second column, then add a new input + if (this.suiteTable.rows[rowNum].cells.length > 1) { + var resultCell = this.suiteTable.rows[rowNum].cells[1]; + form.createHiddenField("testTable." + rowNum, resultCell.innerHTML); + // remove the resultCell, so it's not included in the suite HTML + resultCell.parentNode.removeChild(resultCell); + } + } + + form.createHiddenField("numTestTotal", rowNum); - if (resultsUrlQueryString) { - var clauses = resultsUrlQueryString.split('&'); - for (var i = 0; i < clauses.length; i++) { - var keyValuePair = clauses[i].split('=',2); - var key = unescape(keyValuePair[0]); - var value = unescape(keyValuePair[1]); - form.createHiddenField(key, value); + // Add HTML for the suite itself + form.createHiddenField("suite", this.suiteTable.parentNode.innerHTML); + + if (this.controlPanel.shouldSaveResultsToFile()) { + this._saveToFile(resultsUrl, form); + } else { + form.submit(); } - } + document.body.removeChild(form); + if (this.controlPanel.closeAfterTests()) { + window.top.close(); + } + }, - form.createHiddenField("selenium.version", Selenium.version); - form.createHiddenField("selenium.revision", Selenium.revision); - - form.createHiddenField("result", suiteFailed == true ? "failed" : "passed"); - - form.createHiddenField("totalTime", Math.floor((currentTime - startTime) / 1000)); - form.createHiddenField("numTestPasses", numTestPasses); - form.createHiddenField("numTestFailures", numTestFailures); - form.createHiddenField("numCommandPasses", numCommandPasses); - form.createHiddenField("numCommandFailures", numCommandFailures); - form.createHiddenField("numCommandErrors", numCommandErrors); - - // Create an input for each test table. The inputs are named - // testTable.1, testTable.2, etc. - for (rowNum = 1; rowNum < suiteTable.rows.length;rowNum++) { - // If there is a second column, then add a new input - if (suiteTable.rows[rowNum].cells.length > 1) { - var resultCell = suiteTable.rows[rowNum].cells[1]; - form.createHiddenField("testTable." + rowNum, resultCell.innerHTML); - // remove the resultCell, so it's not included in the suite HTML - resultCell.parentNode.removeChild(resultCell); + _saveToFile: function (fileName, form) { + // This only works when run as an IE HTA + var inputs = new Object(); + for (var i = 0; i < form.elements.length; i++) { + inputs[form.elements[i].name] = form.elements[i].value; } + var objFSO = new ActiveXObject("Scripting.FileSystemObject") + var scriptFile = objFSO.CreateTextFile(fileName); + scriptFile.WriteLine("<html><body>\n<h1>Test suite results </h1>" + + "\n\n<table>\n<tr>\n<td>result:</td>\n<td>" + inputs["result"] + "</td>\n" + + "</tr>\n<tr>\n<td>totalTime:</td>\n<td>" + inputs["totalTime"] + "</td>\n</tr>\n" + + "<tr>\n<td>numTestPasses:</td>\n<td>" + inputs["numTestPasses"] + "</td>\n</tr>\n" + + "<tr>\n<td>numTestFailures:</td>\n<td>" + inputs["numTestFailures"] + "</td>\n</tr>\n" + + "<tr>\n<td>numCommandPasses:</td>\n<td>" + inputs["numCommandPasses"] + "</td>\n</tr>\n" + + "<tr>\n<td>numCommandFailures:</td>\n<td>" + inputs["numCommandFailures"] + "</td>\n</tr>\n" + + "<tr>\n<td>numCommandErrors:</td>\n<td>" + inputs["numCommandErrors"] + "</td>\n</tr>\n" + + "<tr>\n<td>" + inputs["suite"] + "</td>\n<td> </td>\n</tr>"); + var testNum = inputs["numTestTotal"]; + for (var rowNum = 1; rowNum < testNum; rowNum++) { + scriptFile.WriteLine("<tr>\n<td>" + inputs["testTable." + rowNum] + "</td>\n<td> </td>\n</tr>"); + } + scriptFile.WriteLine("</table></body></html>"); + scriptFile.Close(); } - - form.createHiddenField("numTestTotal", rowNum); +}); - // Add HTML for the suite itself - form.createHiddenField("suite", suiteTable.parentNode.innerHTML); +/** HtmlTestCase encapsulates an HTML test document */ +var HtmlTestCase = Class.create(); +Object.extend(HtmlTestCase.prototype, { - if (isQueryParameterTrue("save")) { - saveToFile(resultsUrl, form); - } else { - form.submit(); - } - document.body.removeChild(form); - if (isQueryParameterTrue("close")) { - window.top.close(); - } -} + initialize: function(testDocument, htmlTestSuiteRow) { + if (testDocument == null) { + throw "testDocument should not be null"; + } + if (htmlTestSuiteRow == null) { + throw "htmlTestSuiteRow should not be null"; + } + this.testDocument = testDocument; + this.htmlTestSuiteRow = htmlTestSuiteRow; + this.commandRows = this._collectCommandRows(); + this.nextCommandRowIndex = 0; + this._addBreakpointSupport(); + }, -function saveToFile(fileName, form) { - // This only works when run as an IE HTA - var inputs = new Object(); - for (var i = 0; i < form.elements.length; i++) { - inputs[form.elements[i].name] = form.elements[i].value; - } - var objFSO = new ActiveXObject("Scripting.FileSystemObject") - var scriptFile = objFSO.CreateTextFile(fileName); - scriptFile.WriteLine("<html><body>\n<h1>Test suite results </h1>" + - "\n\n<table>\n<tr>\n<td>result:</td>\n<td>" + inputs["result"] + "</td>\n" + - "</tr>\n<tr>\n<td>totalTime:</td>\n<td>" + inputs["totalTime"] + "</td>\n</tr>\n" + - "<tr>\n<td>numTestPasses:</td>\n<td>" + inputs["numTestPasses"] + "</td>\n</tr>\n" + - "<tr>\n<td>numTestFailures:</td>\n<td>" + inputs["numTestFailures"] + "</td>\n</tr>\n" + - "<tr>\n<td>numCommandPasses:</td>\n<td>" + inputs["numCommandPasses"] + "</td>\n</tr>\n" + - "<tr>\n<td>numCommandFailures:</td>\n<td>" + inputs["numCommandFailures"] + "</td>\n</tr>\n" + - "<tr>\n<td>numCommandErrors:</td>\n<td>" + inputs["numCommandErrors"] + "</td>\n</tr>\n" + - "<tr>\n<td>" + inputs["suite"] + "</td>\n<td> </td>\n</tr>"); - var testNum = inputs["numTestTotal"]; - for (var rowNum = 1; rowNum < testNum; rowNum++) { - scriptFile.WriteLine("<tr>\n<td>" + inputs["testTable." + rowNum] + "</td>\n<td> </td>\n</tr>"); - } - scriptFile.WriteLine("</table></body></html>"); - scriptFile.Close(); -} + _collectCommandRows: function () { + var commandRows = []; + var tables = $A(this.testDocument.getElementsByTagName("table")); + var self = this; + tables.each(function (table) { + $A(table.rows).each(function(candidateRow) { + if (self.isCommandRow(candidateRow)) { + commandRows.push(new HtmlTestCaseRow(candidateRow)); + } + }.bind(this)); + }); + return commandRows; + }, -function printMetrics() { - setText(document.getElementById("commandPasses"), numCommandPasses); - setText(document.getElementById("commandFailures"), numCommandFailures); - setText(document.getElementById("commandErrors"), numCommandErrors); - setText(document.getElementById("testRuns"), numTestPasses + numTestFailures); - setText(document.getElementById("testFailures"), numTestFailures); + isCommandRow: function (row) { + return row.cells.length >= 3; + }, - currentTime = new Date().getTime(); + reset: function() { + /** + * reset the test to runnable state + */ + this.nextCommandRowIndex = 0; + + this._setTitleColor(''); + this.commandRows.each(function(row) { + row.reset(); + }); + + // remove any additional fake "error" row added to the end of the document + var errorElement = this.testDocument.getElementById('error'); + if (errorElement) { + Element.remove(errorElement); + } + }, - timeDiff = currentTime - startTime; - totalSecs = Math.floor(timeDiff / 1000); + getCommandRows: function () { + return this.commandRows; + }, - minutes = Math.floor(totalSecs / 60); - seconds = totalSecs % 60; + _setTitleColor: function(color) { + var headerRow = this.testDocument.getElementsByTagName("tr")[0]; + if (headerRow) { + headerRow.bgColor = color; + } + }, - setText(document.getElementById("elapsedTime"), pad(minutes)+":"+pad(seconds)); -} + markFailed: function() { + this._setTitleColor(FeedbackColors.failColor); + this.htmlTestSuiteRow.markFailed(); + }, -// Puts a leading 0 on num if it is less than 10 -function pad (num) { - return (num > 9) ? num : "0" + num; -} + markPassed: function() { + this._setTitleColor(FeedbackColors.passColor); + this.htmlTestSuiteRow.markPassed(); + }, -/* - * Register all of the built-in command handlers with the CommandHandlerFactory. - * TODO work out an easy way for people to register handlers without modifying the Selenium sources. - */ -function registerCommandHandlers() { - commandFactory = new CommandHandlerFactory(); - commandFactory.registerAll(selenium); + addErrorMessage: function(errorMsg, currentRow) { + if (currentRow) { + currentRow.markFailed(errorMsg); + } else { + var errorElement = this.testDocument.createElement("p"); + errorElement.id = "error"; + errorElement.innerHTML = errorMsg; + this.testDocument.body.appendChild(errorElement); + Element.setStyle(errorElement, {'backgroundColor': FeedbackColors.failColor}); + } + }, -} + _addBreakpointSupport: function() { + this.commandRows.each(function(row) { + row.addBreakpointSupport(); + }); + }, -function initialiseTestLoop() { - testLoop = new TestLoop(commandFactory); - - testLoop.getCommandInterval = function() { return runInterval; }; - testLoop.nextCommand = nextCommand; - testLoop.commandStarted = commandStarted; - testLoop.commandComplete = commandComplete; - testLoop.commandError = commandError; - testLoop.testComplete = testComplete; - testLoop.pause = function() { - document.getElementById('continueTest').disabled = false; - }; - return testLoop; -} + hasMoreCommandRows: function() { + return this.nextCommandRowIndex < this.commandRows.length; + }, -function nextCommand() { - var row = currentTest.nextCommand(); - if (row == null) { + getNextCommandRow: function() { + if (this.hasMoreCommandRows()) { + return this.commandRows[this.nextCommandRowIndex++]; + } return null; } - row.cells[2].originalHTML = row.cells[2].innerHTML; - return new SeleniumCommand(getText(row.cells[0]), - getText(row.cells[1]), - getText(row.cells[2])); -} -function removeNbsp(value) { - return value.replace(/\240/g, ""); -} +}); -function scrollIntoView(element) { - if (element.scrollIntoView) { - element.scrollIntoView(false); - return; - } - // TODO: work out how to scroll browsers that don't support - // scrollIntoView (like Konqueror) -} -function commandStarted() { - currentTest.currentRow.bgColor = workingColor; - scrollIntoView(currentTest.currentRow.cells[0]); - printMetrics(); -} +// TODO: split out an JavascriptTestCase class to handle the "sejs" stuff + +var get_new_rows = function() { + var row_array = new Array(); + for (var i = 0; i < new_block.length; i++) { + + var new_source = (new_block[i][0].tokenizer.source.slice(new_block[i][0].start, + new_block[i][0].end)); -function commandComplete(result) { - if (result.failed) { - numCommandFailures += 1; - recordFailure(result.failureMessage); - } else if (result.passed) { - numCommandPasses += 1; - currentTest.currentRow.bgColor = passColor; - } else { - currentTest.currentRow.bgColor = doneColor; + var row = '<td style="display:none;" class="js">getEval</td>' + + '<td style="display:none;">currentTest.doNextCommand()</td>' + + '<td style="white-space: pre;">' + new_source + '</td>' + + '<td></td>' + + row_array.push(row); } -} + return row_array; +}; -function commandError(errorMessage) { - numCommandErrors += 1; - recordFailure(errorMessage); -} -function recordFailure(errorMsg) { - LOG.warn("recordFailure: " + errorMsg); - // Set cell background to red - currentTest.currentRow.bgColor = failColor; +var Metrics = Class.create(); +Object.extend(Metrics.prototype, { + initialize: function() { + // The number of tests run + this.numTestPasses = 0; + // The number of tests that have failed + this.numTestFailures = 0; + // The number of commands which have passed + this.numCommandPasses = 0; + // The number of commands which have failed + this.numCommandFailures = 0; + // The number of commands which have caused errors (element not found) + this.numCommandErrors = 0; + // The time that the test was started. + this.startTime = null; + // The current time. + this.currentTime = null; + }, + + printMetrics: function() { + setText($('commandPasses'), this.numCommandPasses); + setText($('commandFailures'), this.numCommandFailures); + setText($('commandErrors'), this.numCommandErrors); + setText($('testRuns'), this.numTestPasses + this.numTestFailures); + setText($('testFailures'), this.numTestFailures); - // Set error message - currentTest.currentRow.cells[2].innerHTML = errorMsg; - currentTest.currentRow.title = errorMsg; - testFailed = true; - suiteFailed = true; -} + this.currentTime = new Date().getTime(); + + var timeDiff = this.currentTime - this.startTime; + var totalSecs = Math.floor(timeDiff / 1000); + + var minutes = Math.floor(totalSecs / 60); + var seconds = totalSecs % 60; + + setText($('elapsedTime'), this._pad(minutes) + ":" + this._pad(seconds)); + }, -function testComplete() { - var resultColor = passColor; - if (testFailed) { - resultColor = failColor; - numTestFailures += 1; - } else { - numTestPasses += 1; +// Puts a leading 0 on num if it is less than 10 + _pad: function(num) { + return (num > 9) ? num : "0" + num; + }, + + resetMetrics: function() { + this.numTestPasses = 0; + this.numTestFailures = 0; + this.numCommandPasses = 0; + this.numCommandFailures = 0; + this.numCommandErrors = 0; + this.startTime = new Date().getTime(); } - if (currentTest.headerRow) { - currentTest.headerRow.bgColor = resultColor; +}); + +var HtmlRunnerCommandFactory = Class.create(); +Object.extend(HtmlRunnerCommandFactory.prototype, { + + initialize: function(seleniumCommandFactory, testLoop) { + this.seleniumCommandFactory = seleniumCommandFactory; + this.testLoop = testLoop; + this.handlers = { + pause: { + execute: function(selenium, command) { + testLoop.pauseInterval = command.target; + return {}; + } + } + }; + //todo: register commands + }, + + getCommandHandler: function(command) { + if (this.handlers[command]) { + return this.handlers[command]; + } + return this.seleniumCommandFactory.getCommandHandler(command); } - - printMetrics(); - window.setTimeout("runNextTest()", 1); -} +}); -Selenium.prototype.doPause = function(waitTime) { - testLoop.pauseInterval = waitTime; -}; +var HtmlRunnerTestLoop = Class.create(); +Object.extend(HtmlRunnerTestLoop.prototype, new TestLoop()); +Object.extend(HtmlRunnerTestLoop.prototype, { + initialize: function(htmlTestCase, metrics, seleniumCommandFactory) { + + this.commandFactory = new HtmlRunnerCommandFactory(seleniumCommandFactory, this); + this.metrics = metrics; + + this.htmlTestCase = htmlTestCase; + + se = selenium; + global.se = selenium; + + this.currentRow = null; + this.currentRowIndex = 0; + + // used for selenium tests in javascript + this.currentItem = null; + this.commandAgenda = new Array(); + + this.htmlTestCase.reset(); + + this.sejsElement = this.htmlTestCase.testDocument.getElementById('sejs'); + if (this.sejsElement) { + var fname = 'Selenium JavaScript'; + parse_result = parse(this.sejsElement.innerHTML, fname, 0); + + var x2 = new ExecutionContext(GLOBAL_CODE); + ExecutionContext.current = x2; + + execute(parse_result, x2) + } + }, + + _advanceToNextRow: function() { + if (this.htmlTestCase.hasMoreCommandRows()) { + this.currentRow = this.htmlTestCase.getNextCommandRow(); + if (this.sejsElement) { + this.currentItem = agenda.pop(); + this.currentRowIndex++; + } + } else { + this.currentRow = null; + this.currentItem = null; + } + }, + + nextCommand : function() { + this._advanceToNextRow(); + if (this.currentRow == null) { + return null; + } + return this.currentRow.getCommand(); + }, + + commandStarted : function() { + $('pauseTest').disabled = false; + this.currentRow.markWorking(); + this.metrics.printMetrics(); + }, + + commandComplete : function(result) { + if (result.failed) { + this.metrics.numCommandFailures += 1; + this._recordFailure(result.failureMessage); + } else if (result.passed) { + this.metrics.numCommandPasses += 1; + this.currentRow.markPassed(); + } else { + this.currentRow.markDone(); + } + }, + + commandError : function(errorMessage) { + this.metrics.numCommandErrors += 1; + this._recordFailure(errorMessage); + }, + + _recordFailure : function(errorMsg) { + LOG.warn("currentTest.recordFailure: " + errorMsg); + htmlTestRunner.markFailed(); + this.htmlTestCase.addErrorMessage(errorMsg, this.currentRow); + }, + + testComplete : function() { + $('pauseTest').disabled = true; + $('stepTest').disabled = true; + if (htmlTestRunner.testFailed) { + this.htmlTestCase.markFailed(); + this.metrics.numTestFailures += 1; + } else { + this.htmlTestCase.markPassed(); + this.metrics.numTestPasses += 1; + } + + this.metrics.printMetrics(); + + window.setTimeout(function() { + htmlTestRunner.runNextTest(); + }, 1); + }, + + getCommandInterval : function() { + return htmlTestRunner.controlPanel.runInterval; + }, + + pause : function() { + htmlTestRunner.controlPanel.pauseCurrentTest(); + }, + + doNextCommand: function() { + var _n = this.currentItem[0]; + var _x = this.currentItem[1]; + + new_block = new Array() + execute(_n, _x); + if (new_block.length > 0) { + var the_table = this.htmlTestCase.testDocument.getElementById("se-js-table") + var loc = this.currentRowIndex + var new_rows = get_new_rows() + + // make the new statements visible on screen... + for (var i = 0; i < new_rows.length; i++) { + the_table.insertRow(loc + 1); + the_table.rows[loc + 1].innerHTML = new_rows[i]; + this.commandRows.unshift(the_table.rows[loc + 1]) + } + + } + } + +}); -Selenium.prototype.doPause.dontCheckAlertsAndConfirms = true; Selenium.prototype.doBreak = function() { - document.getElementById('modeStep').checked = true; - runInterval = -1; + /** Halt the currently running test, and wait for the user to press the Continue button. + * This command is useful for debugging, but be careful when using it, because it will + * force automated tests to hang until a user intervenes manually. + */ + // todo: should not refer to controlPanel directly + htmlTestRunner.controlPanel.setToPauseAtNextCommand(); }; +Selenium.prototype.doStore = function(expression, variableName) { + /** This command is a synonym for storeExpression. + * @param expression the value to store + * @param variableName the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + */ + storedVars[variableName] = expression; +} + /* * Click on the located element, and attach a callback to notify * when the page is reloaded. */ -Selenium.prototype.doModalDialogTest = function(returnValue) { +// DGF TODO this code has been broken for some time... what is it trying to accomplish? +Selenium.prototype.XXXdoModalDialogTest = function(returnValue) { this.browserbot.doModalDialogTest(returnValue); }; -/* - * Store the value of a form input in a variable - */ -Selenium.prototype.doStoreValue = function(target, varName) { - if (!varName) { - // Backward compatibility mode: read the ENTIRE text of the page - // and stores it in a variable with the name of the target - value = this.page().bodyText(); - storedVars[target] = value; - return; +Selenium.prototype.doEcho = function(message) { + /** Prints the specified message into the third table cell in your Selenese tables. + * Useful for debugging. + * @param message the message to print + */ + currentTest.currentRow.setMessage(message); +} + +Selenium.prototype.assertSelected = function(selectLocator, optionLocator) { + /** + * Verifies that the selected option of a drop-down satisfies the optionSpecifier. <i>Note that this command is deprecated; you should use assertSelectedLabel, assertSelectedValue, assertSelectedIndex, or assertSelectedId instead.</i> + * + * <p>See the select command for more information about option locators.</p> + * + * @param selectLocator an <a href="#locators">element locator</a> identifying a drop-down menu + * @param optionLocator an option locator, typically just an option label (e.g. "John Smith") + */ + var element = this.page().findElement(selectLocator); + var locator = this.optionLocatorFactory.fromLocatorString(optionLocator); + if (element.selectedIndex == -1) + { + Assert.fail("No option selected"); } - var element = this.page().findElement(target); - storedVars[varName] = getInputValue(element); + locator.assertSelected(element); }; -/* - * Store the text of an element in a variable +/** + * Tell Selenium to expect a failure on the next command execution. This + * command temporarily installs a CommandFactory that generates + * CommandHandlers that expect a failure. */ -Selenium.prototype.doStoreText = function(target, varName) { - var element = this.page().findElement(target); - storedVars[varName] = getText(element); +Selenium.prototype.assertFailureOnNext = function(message) { + if (!message) { + throw new Error("Message must be provided"); + } + + var expectFailureCommandFactory = + new ExpectFailureCommandFactory(currentTest.commandFactory, message, "failure", executeCommandAndReturnFailureMessage); + currentTest.commandFactory = expectFailureCommandFactory; }; -/* - * Store the value of an element attribute in a variable +/** + * Tell Selenium to expect an error on the next command execution. This + * command temporarily installs a CommandFactory that generates + * CommandHandlers that expect a failure. */ -Selenium.prototype.doStoreAttribute = function(target, varName) { - storedVars[varName] = this.page().findAttribute(target); +Selenium.prototype.assertErrorOnNext = function(message) { + if (!message) { + throw new Error("Message must be provided"); + } + + var expectFailureCommandFactory = + new ExpectFailureCommandFactory(currentTest.commandFactory, message, "error", executeCommandAndReturnErrorMessage); + currentTest.commandFactory = expectFailureCommandFactory; }; -/* - * Store the result of a literal value - */ -Selenium.prototype.doStore = function(value, varName) { - storedVars[varName] = value; +function executeCommandAndReturnFailureMessage(baseHandler, originalArguments) { + var baseResult = baseHandler.execute.apply(baseHandler, originalArguments); + if (baseResult.passed) { + return null; + } + return baseResult.failureMessage; +}; + +function executeCommandAndReturnErrorMessage(baseHandler, originalArguments) { + try { + baseHandler.execute.apply(baseHandler, originalArguments); + return null; + } + catch (expected) { + return expected.message; + } }; -Selenium.prototype.doEcho = function(msg) { - currentTest.currentRow.cells[2].innerHTML = msg; +function ExpectFailureCommandHandler(baseHandler, originalCommandFactory, expectedErrorMessage, errorType, decoratedExecutor) { + this.execute = function() { + var baseFailureMessage = decoratedExecutor(baseHandler, arguments); + var result = {}; + if (!baseFailureMessage) { + result.failed = true; + result.failureMessage = "Expected " + errorType + " did not occur."; + } + else { + if (! PatternMatcher.matches(expectedErrorMessage, baseFailureMessage)) { + result.failed = true; + result.failureMessage = "Expected " + errorType + " message '" + expectedErrorMessage + + "' but was '" + baseFailureMessage + "'"; + } + else { + result.passed = true; + result.result = baseFailureMessage; + } + } + currentTest.commandFactory = originalCommandFactory; + return result; + }; } + +function ExpectFailureCommandFactory(originalCommandFactory, expectedErrorMessage, errorType, decoratedExecutor) { + this.getCommandHandler = function(name) { + var baseHandler = originalCommandFactory.getCommandHandler(name); + return new ExpectFailureCommandHandler(baseHandler, originalCommandFactory, expectedErrorMessage, errorType, decoratedExecutor); + }; +}; diff --git a/tests/test_tools/selenium/core/scripts/selenium-version.js b/tests/test_tools/selenium/core/scripts/selenium-version.js index 1fee837b..c4a5508c 100644 --- a/tests/test_tools/selenium/core/scripts/selenium-version.js +++ b/tests/test_tools/selenium/core/scripts/selenium-version.js @@ -1,5 +1,5 @@ -Selenium.version = "@VERSION@"; -Selenium.revision = "@REVISION@"; +Selenium.version = "0.8.0"; +Selenium.revision = "1472:1473"; window.top.document.title += " v" + Selenium.version + " [" + Selenium.revision + "]"; diff --git a/tests/test_tools/selenium/core/selenium.css b/tests/test_tools/selenium/core/selenium.css index 6e9f3f30..963c63ad 100644 --- a/tests/test_tools/selenium/core/selenium.css +++ b/tests/test_tools/selenium/core/selenium.css @@ -16,9 +16,12 @@ /*---( Layout )---*/ +* { + margin: 0px; + padding: 0px; +} + body { - margin: 0; - padding: 0; overflow: auto; } @@ -37,15 +40,13 @@ tr { } .layout td { - margin: 0; - padding: 0; border: 0; } iframe { + border: 0px; width: 100%; height: 100%; - border: 0; background: white; overflow: auto; } @@ -158,11 +159,6 @@ button, label { color: #f90; } -.splash { - border: 1px solid black; - padding: 20px; - background: #ccc; -} /*---( Logging Console )---*/ @@ -209,3 +205,83 @@ button, label { #logging-console li.debug { color: green; } + +table.selenium { + font-family: Verdana, Arial, sans-serif; + font-size: 12; + border-width: 1px 1px 1px 1px; + border-spacing: 2px; + border-style: solid none solid none; + border-color: gray gray gray gray; + border-collapse: separate; + background-color: white; +} + +table.selenium th { + border-width: 1px 1px 1px 1px; + padding: 1px 1px 1px 1px; + border-style: none none none none; + border-color: gray gray gray gray; + -moz-border-radius: 0px 0px 0px 0px; +} + +table.selenium td { + border-width: 1px 1px 1px 1px; + padding: 1px 1px 1px 1px; + border-style: none none none none; + border-color: gray gray gray gray; + -moz-border-radius: 0px 0px 0px 0px; +} + +div.executionOptions { + padding-left: 5em; +} + +div.executionOptions label, div.executionOptions input { + display: block; + float: left; +} + +div.executionOptions br { + clear: left; +} + +#speedSlider { + text-align: left; + margin: 0px auto; + width: 260px; + line-height: 0px; + font-size: 0px; + padding: 0px; +} + +#speedSlider #speedTrack { + background-color: #333; + width: 260px; + height: 2px; + <!--[if IE]> + height: 2px; + line-height: 2px; + <![endif]--> + z-index: 1; + border: 1px solid; + border-color: #999 #ddd #ddd #999; + cursor: pointer; +} + +#speedSlider #speedHandle { + width: 12px; + top: -8px; + background-color: #666; + position: relative; + margin: 0px; + height: 8px; + <!--[if IE]> + height: 8px; + line-height: 8px; + <![endif]--> + z-index: 1; + border: 1px solid; + border-color: #999 #333 #333 #999; + cursor: pointer; +} diff --git a/tests/test_tools/selenium/php/TestRunner.php b/tests/test_tools/selenium/php/TestRunner.php index a82ce0dd..54e46647 100644 --- a/tests/test_tools/selenium/php/TestRunner.php +++ b/tests/test_tools/selenium/php/TestRunner.php @@ -1,169 +1,161 @@ - <html> <head> -<HTA:APPLICATION ID="SeleniumHTARunner" APPLICATIONNAME="Selenium" > -<!-- the previous line is only relevant if you rename this + <HTA:APPLICATION ID="SeleniumHTARunner" APPLICATIONNAME="Selenium"> + <!-- the previous line is only relevant if you rename this file to "TestRunner.hta" --> -<!-- The copyright notice and other comments have been moved to after the HTA declaration, - to work-around a bug in IE on Win2K whereby the HTA application doesn't function correctly --> -<!-- -Copyright 2004 ThoughtWorks, Inc - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<meta content="text/html; charset=ISO-8859-1" http-equiv="content-type" /> - -<title>Selenium Functional Test Runner</title> -<link rel="stylesheet" type="text/css" href="<?php echo $base_dir; ?>core/selenium.css" /> -<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-browserdetect.js"></script> -<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-browserbot.js"></script> -<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/prototype-1.4.0.js"></script> -<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/find_matching_child.js"></script> -<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-api.js"></script> -<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-commandhandlers.js"></script> -<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-executionloop.js"></script> -<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-testrunner.js"></script> -<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-logging.js"></script> -<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-version.js"></script> -<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/htmlutils.js"></script> -<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/xpath/misc.js"></script> -<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/xpath/dom.js"></script> -<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/xpath/xpath.js"></script> -<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>prado-functional-test.js"></script> -<script language="JavaScript" type="text/javascript"> - function openDomViewer() { - var autFrame = document.getElementById('myiframe'); - var autFrameDocument = getIframeDocument(autFrame); - this.rootDocument = autFrameDocument; - var domViewer = window.open('<?php echo $base_dir; ?>core/domviewer/domviewer.html'); - return false; - } - - Logger.prototype.openLogWindow = function() - { - this.logWindow = window.open( - "<?php echo $base_dir; ?>core/SeleniumLog.html", "SeleniumLog", - "width=600,height=250,bottom=0,right=0,status,scrollbars,resizable" - ); - return this.logWindow; - } - - var post_results_to = "<?php echo $driver; ?>"; - -</script> + <!-- The copyright notice and other comments have been moved to after the HTA declaration, +to work-around a bug in IE on Win2K whereby the HTA application doesn't function correctly --> + <!-- + Copyright 2004 ThoughtWorks, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + <meta content="text/html; charset=ISO-8859-1" http-equiv="content-type"/> + + <title>Selenium Functional Test Runner</title> + <link rel="stylesheet" type="text/css" href="<?php echo $base_dir; ?>core/selenium.css"/> + <script type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/narcissus-defs.js"></script> + <script type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/narcissus-parse.js"></script> + <script type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/narcissus-exec.js"></script> + <script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/lib/prototype.js"></script> + <script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/lib/scriptaculous/scriptaculous.js"></script> + <script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/lib/cssQuery/cssQuery-p.js"></script> + <script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-browserdetect.js"></script> + <script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-browserbot.js"></script> + <script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/find_matching_child.js"></script> + <script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/htmlutils.js"></script> + <script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-api.js"></script> + <script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-commandhandlers.js"></script> + <script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-executionloop.js"></script> + <script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-testrunner.js"></script> + <script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-logging.js"></script> + <script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-version.js"></script> + <script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/xpath/misc.js"></script> + <script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/xpath/dom.js"></script> + <script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/xpath/xpath.js"></script> + <script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>prado-functional-test.js"></script> + + <script language="JavaScript" type="text/javascript"> + var post_results_to = "<?php echo $driver; ?>"; + </script> </head> -<body onLoad="start();"> +<body onLoad="onSeleniumLoad();"> +<table class="layout"> +<form action="" name="controlPanel"> + +<!-- Suite, Test, Control Panel --> +<tr class="selenium"> +<td width="25%" height="15%"> + <iframe name="testSuiteFrame" id="testSuiteFrame" src="<?php echo $driver; ?>?testSuites" application="yes"></iframe> +</td> +<td width="50%" height="15%"> + <iframe name="testFrame" id="testFrame" application="yes"></iframe> +</td> + +<td width="25%"> <table class="layout"> - <form action="" name="controlPanel"> - - <!-- Suite, Test, Control Panel --> - - <tr class="selenium"> - <td width="25%" height="30%" ><iframe name="testSuiteFrame" id="testSuiteFrame" src="<?php echo $driver; ?>?testSuites" application="yes"></iframe></td> - <td width="50%" height="30%" ><iframe name="testFrame" id="testFrame" application="yes"></iframe></td> - - <td width="25%"> - <table class="layout"> - <tr class="selenium"> - <th width="25%" height="1" class="header"> - <h3><a href="http://selenium.thoughtworks.com" title="The Selenium Project">Selenium</a></h3> - </th> - </tr> - <tr> - <td width="25%" height="30%" id="controlPanel"> - - <fieldset> - <legend>Execute Tests</legend> - - <div> - <input id="modeRun" type="radio" name="runMode" value="0" checked="checked"/><label for="modeRun">Run</label> - <input id="modeWalk" type="radio" name="runMode" value="500" /><label for="modeWalk">Walk</label> - <input id="modeStep" type="radio" name="runMode" value="-1" /><label for="modeStep">Step</label> - </div> - - <div> - <button type="button" id="runSuite" onClick="startTestSuite();" - title="Run the entire Test-Suite"> - <strong>All</strong> - </button> - <button type="button" id="runTest" onClick="runSingleTest();" - title="Run the current Test"> - <em>Selected</em> - </button> - <button type="button" id="continueTest" disabled="disabled" - title="Continue the Test"> - Continue - </button> - </div> - - </fieldset> - - <table id="stats" align="center"> - <tr> - <td colspan="2" align="right">Elapsed:</td> - <td id="elapsedTime" colspan="2">00.00</td> - </tr> - <tr> - <th colspan="2">Tests</th> - <th colspan="2">Commands</th> - </tr> - <tr> - <td class="count" id="testRuns">0</td> - <td>run</td> - <td class="count" id="commandPasses">0</td> - <td>passed</td> - </tr> - <tr> - <td class="count" id="testFailures">0</td> - <td>failed</td> - <td class="count" id="commandFailures">0</td> - <td>failed</td> - </tr> - <tr> - <td colspan="2"></td> - <td class="count" id="commandErrors">0</td> - <td>incomplete</td> - </tr> - </table> - <!-- - <fieldset> - <legend>Tools</legend> - <button type="button" id="domViewer1" onClick="openDomViewer();"> - View DOM - </button> - <button type="button" onClick="LOG.show();"> - Show Log - </button> - - </fieldset> - --> - </td> - </tr> - </table> - </td> - </tr> - - <!-- AUT --> - - <tr> - <td colspan="3" height="70%"><iframe name="myiframe" id="myiframe" src="<?php echo $base_dir; ?>core/TestRunner-splash.html"></iframe></td> - </tr> + <tr class="selenium"> + <th width="25%" height="1" class="header"> + <h1><a href="http://selenium.thoughtworks.com" title="The Selenium Project">Selenium</a> TestRunner + </h1> + </th> + </tr> + <tr> + <td width="25%" height="15%" id="controlPanel"> + <fieldset> + <legend>Execute Tests</legend> + + <div style="float:left">Fast</div> + <div style="float:right">Slow</div> + <br> + <div id="speedSlider"> + <div id="speedTrack"> </div> + <div id="speedHandle"> </div> + </div> + + <div> + <button type="button" id="runSuite" onClick="htmlTestRunner.startTestSuite();" + title="Run the entire Test-Suite"> + <strong>All</strong> + </button> + <button type="button" id="runSeleniumTest" onClick="htmlTestRunner.runSingleTest();" + title="Run the current Test"> + <em>Selected</em> + </button> + <button type="button" id="pauseTest" disabled="disabled" + title="Pause the current Test"> + Pause + </button> + <button type="button" id="stepTest" disabled="disabled" + title="Step the Test"> + Step + </button> + </div> + + <div class="executionOptions"> + <input id="highlightOption" type="checkbox" name="highlightOption" value="0" checked="checked"/> + <label for="highlightOption">Highlight elements</label> + </div> + + </fieldset> + + <table id="stats" align="center"> + <tr> + <td colspan="2" align="right">Elapsed:</td> + <td id="elapsedTime" colspan="2">00.00</td> + </tr> + <tr> + <th colspan="2">Tests</th> + <th colspan="2">Commands</th> + </tr> + <tr> + <td class="count" id="testRuns">0</td> + <td>run</td> + <td class="count" id="commandPasses">0</td> + <td>passed</td> + </tr> + <tr> + <td class="count" id="testFailures">0</td> + <td>failed</td> + <td class="count" id="commandFailures">0</td> + <td>failed</td> + </tr> + <tr> + <td colspan="2"></td> + <td class="count" id="commandErrors">0</td> + <td>incomplete</td> + </tr> + </table> + </td> + </tr> + </table> +</td> +</tr> + +<!-- AUT --> + +<tr> + <td colspan="3" height="70%"> + <iframe name="myiframe" id="myiframe" src="<?php echo $base_dir; ?>core/TestRunner-splash.html"></iframe> + </td> +</tr> + </form> </table> </body> -</html>
\ No newline at end of file +</html> diff --git a/tests/test_tools/selenium/php/TestSuiteHeader.php b/tests/test_tools/selenium/php/TestSuiteHeader.php new file mode 100644 index 00000000..6fe2f6b6 --- /dev/null +++ b/tests/test_tools/selenium/php/TestSuiteHeader.php @@ -0,0 +1,54 @@ +<!--
+Copyright 2004 ThoughtWorks, Inc
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<html>
+<head>
+<meta content="text/html; charset=ISO-8859-1"
+http-equiv="content-type">
+<title>Test Suite</title>
+<link rel="stylesheet" type="text/css" href="<?php echo $base_dir; ?>core/selenium.css" />
+<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-browserdetect.js"></script>
+<script language="JavaScript" type="text/javascript">
+ var DISABLED = true; // used to flag failing tests
+
+ function filterTestsForBrowser() {
+ var suiteTable = document.getElementById("suiteTable");
+ var skippedTests = document.getElementById("skippedTests");
+
+ for(rowNum = suiteTable.rows.length - 1; rowNum >= 0; rowNum--)
+ {
+ var row = suiteTable.rows[rowNum];
+ var filterString = row.getAttribute("unless");
+ if (filterString && eval(filterString))
+ {
+ var cellHTML = row.cells[0].innerHTML;
+ suiteTable.deleteRow(rowNum);
+
+ var newRow = skippedTests.insertRow(1);
+ var newCell = newRow.insertCell(0)
+ newCell.innerHTML = cellHTML;
+ }
+ }
+ }
+</script>
+</head>
+
+<body onload="filterTestsForBrowser()">
+
+ <table id="suiteTable" cellpadding="1"
+ cellspacing="1"
+ border="1"
+ class="selenium">
+ <tbody>
diff --git a/tests/test_tools/selenium/php/selenium.php b/tests/test_tools/selenium/php/selenium.php index 683bcaa2..fcbbe562 100644 --- a/tests/test_tools/selenium/php/selenium.php +++ b/tests/test_tools/selenium/php/selenium.php @@ -65,12 +65,23 @@ class SeleniumTestStorage { protected $outputs = array(); protected $tests = array(); + protected $options=array(); public function getTests() { return $this->tests; } + public function getOptions() + { + return $this->options; + } + + public function addOption($test_name, $option) + { + $this->options[$test_name] = $option; + } + public function addCommand($test_case_id, $command) { $data = array($test_case_id, $command); @@ -105,6 +116,11 @@ class SeleneseInterpreter return $this->storage->getTests(); } + public function getOptions() + { + return $this->storage->getOptions(); + } + public function getCommand() { $command = $this->storage->getCommand(); @@ -115,12 +131,15 @@ class SeleneseInterpreter { if($func{0} == '_') return; + $trace = debug_backtrace(); + if($this->isTestOptionFunction($func,$args,$trace)) + return; + $ID = isset($args[0]) ? $args[0] : ""; $value = isset($args[1]) ? $args[1] : ""; if(strpos(strtolower($func),'htmlpresent') || strpos(strtolower($func),'htmlnotpresent')) $ID = htmlspecialchars($ID); $command = array($func, $ID, $value); - $trace = debug_backtrace(); if(is_int(strpos(strtolower($func), 'visible'))) $this->addCommand(array('pause','500',''),$trace); @@ -128,6 +147,17 @@ class SeleneseInterpreter return $this->addCommand($command, $trace); } + protected function isTestOptionFunction($func,$args,$trace) + { + if(strtolower($func)==='skipcondition') + { + list($trace, $test, $suite) = $this->tracer->getTrace($trace); + $this->storage->addOption($test,$args[0]); + return true; + } + return false; + } + protected function addCommand($command, $trace) { list($trace, $test, $suite) = $this->tracer->getTrace($trace); @@ -236,34 +266,26 @@ class SeleniumTestSuiteWriter protected function renderHeader() { - $contents = <<<EOD -<html> -<head> -<meta content="text/html; charset=UTF-8" http-equiv="content-type"> -<title>Test Suite</title> + $base_dir = $this->runner->getDriver().'?sr='; -</head> + include(dirname(__FILE__).'/TestSuiteHeader.php'); -<body> - -<table cellpadding="1" - cellspacing="1" - border="1"> - <tbody> + $contents = <<<EOD <tr><td><b>{$this->name}</b></td></tr> EOD; - return $contents; + echo $contents; } public function render() { - echo $this->renderHeader(); + $this->renderHeader(); foreach($this->suites as $name => $suite) { $file = $suite[0]['trace']['file']; $file = strtr($file,'\\','/'); + $option = $suite[0]['option']===null?'':' unless="'.$suite[0]['option'].'" '; $url = $this->runner->getDriver()."?case={$name}&file={$file}"; - echo "<tr>\n"; + echo "<tr{$option}>\n"; echo "<td><a href=\"{$url}\">{$name}</a></td>\n"; echo "</tr>\n"; } @@ -289,7 +311,18 @@ EOD; { $trace = '';//$this->getJsTraceInfo(); $contents = <<<EOD - </tbody> + </tbody> + </table> + + <br /> + <em>Not supported in this browser</em> + <table id="skippedTests" cellpadding="1" + cellspacing="1" + border="1" + class="selenium"> + <tbody> + <tr><td><b>Skipped Tests</b></td></tr> + </tbody> </table> <script type="text/javascript"> /*<![CDATA[*/ @@ -377,11 +410,14 @@ class SeleniumTestRunnerServer protected function initialize($int) { + $options = $int->getOptions(); foreach($int->getTests() as $command) { $case = $command[5]; + $option=isset($options[$case])?$options[$case]:null; $this->cases[$case][] = - array('test' => $command[2], 'trace' => $command[4]); + array('test' => $command[2], + 'trace' => $command[4], 'option'=>$option); if(is_null($this->name)) $this->name = $command[6]; } @@ -449,6 +485,18 @@ class SeleniumTestCase extends UnitTestCase protected $selenium; protected $Page; + const KONQUEROR='browserVersion.isKonqueror'; + const OPERA='browserVersion.isOpera'; + const CHROME='browserVersion.isChrome'; + const INTERNET_EXPLORER='browserVersion.isIE'; + const SAFARI='browserVersion.isSafari'; + const KHTML='browserVersion.khtml'; + const FIREFOX='browserVersion.isFirefox'; + const MOZILLA='browserVersion.isMozilla'; + const GECKO='browserVersion.isGecko'; + + protected $options=array(); + function __construct() { $server = SimpleSeleniumProxyServer::getInstance(); @@ -472,6 +520,25 @@ class SeleniumTestCase extends UnitTestCase else if (count($args) == 2) return $this->selenium->{$func}($args[0], $args[1]); } + + function disabled() + { + $this->selenium->skipCondition('DISABLED'); + } + + function skipBrowsers() + { + $arg_list = func_get_args(); + $browsers=array(); + foreach($arg_list as $arg) + { + if(is_array($arg)) + $browsers[] = '('.implode(' && ', $arg).')'; + else + $browsers[] = $arg; + } + $this->selenium->skipCondition(implode(' || ', $browsers)); + } } ?>
\ No newline at end of file diff --git a/tests/test_tools/selenium/prado-functional-test.js b/tests/test_tools/selenium/prado-functional-test.js index 306f6a74..4cda378c 100644 --- a/tests/test_tools/selenium/prado-functional-test.js +++ b/tests/test_tools/selenium/prado-functional-test.js @@ -1,3 +1,28 @@ +Object.extend(HtmlTestRunner.prototype, { + loadSuiteFrame: function() { + if (selenium == null) { + selenium = Selenium.createForWindow(this._getApplicationWindow()); + this._registerCommandHandlers(); + } + this.controlPanel.setHighlightOption(); + //var testSuiteName = this.controlPanel.getTestSuiteName(); + var testSuiteName = document.location+'?testSuites'; + if (testSuiteName) { + suiteFrame.load(testSuiteName, this._onloadTestSuite.bind(this)); + } + } +}); + +Object.extend(HtmlTestRunnerControlPanel.prototype, { + _parseQueryParameter: function() { + var tempRunInterval = this._getQueryParameter("runInterval"); + if (tempRunInterval) { + this.setRunInterval(tempRunInterval); + } + } +}); + + /** * Override selenium implementation. @@ -54,55 +79,17 @@ Selenium.prototype.assertEmptySelection = function(selectLocator, optionLocator) return element.selectedIndex == -1; } -function runNextTest() { - if (!runAllTests) - return; - - suiteTable = getIframeDocument(getSuiteFrame()).getElementsByTagName("table")[0]; - - // Do not change the row color of the first row - if (currentRowInSuite > 0) { - // Provide test-status feedback - if (testFailed) { - setCellColor(suiteTable.rows, currentRowInSuite, 0, failColor); - } else { - setCellColor(suiteTable.rows, currentRowInSuite, 0, passColor); - } - // Set the results from the previous test run - setResultsData(suiteTable, currentRowInSuite); - } +Object.extend(HtmlTestSuite.prototype, { + _onTestSuiteComplete: function() { + this.markDone(); + var result = new TestResult(this.failed, this.getTestTable()); + postTestResults(this.failed, this.getTestTable(), result); + } +}); - currentRowInSuite++; - // If we are done with all of the tests, set the title bar as pass or fail - if (currentRowInSuite >= suiteTable.rows.length) { - if (suiteFailed) { - setCellColor(suiteTable.rows, 0, 0, failColor); - } else { - setCellColor(suiteTable.rows, 0, 0, passColor); - } - - LOG.warn("next? ", "warn"); - // If this is an automated run (i.e., build script), then submit - // the test results by posting to a form - - postTestResults(suiteFailed, suiteTable); - } - - else { - // Make the current row blue - setCellColor(suiteTable.rows, currentRowInSuite, 0, workingColor); - - testLink = suiteTable.rows[currentRowInSuite].cells[0].getElementsByTagName("a")[0]; - testLink.focus(); - - var testFrame = getTestFrame(); - addLoadListener(testFrame, startTest); - - selenium.browserbot.setIFrameLocation(testFrame, testLink.href); - } -} + // Post the results to a servlet, CGI-script, etc. The URL of the // results-handler defaults to "/postResults", but an alternative location @@ -122,7 +109,7 @@ function runNextTest() { // suite: the suite table, including the hidden column of test results // testTable.1 to testTable.N: the individual test tables // -function postTestResults(suiteFailed, suiteTable) { +function postTestResults(suiteFailed, suiteTable, result) { form = document.createElement("form"); document.body.appendChild(form); @@ -160,13 +147,12 @@ function postTestResults(suiteFailed, suiteTable) { } form.createHiddenField("result", suiteFailed == true ? "failed" : "passed"); - - form.createHiddenField("totalTime", Math.floor((currentTime - startTime) / 1000)); - form.createHiddenField("numTestPasses", numTestPasses); - form.createHiddenField("numTestFailures", numTestFailures); - form.createHiddenField("numCommandPasses", numCommandPasses); - form.createHiddenField("numCommandFailures", numCommandFailures); - form.createHiddenField("numCommandErrors", numCommandErrors); + form.createHiddenField("totalTime", Math.floor((result.metrics.currentTime - result.metrics.startTime) / 1000)); + form.createHiddenField("numTestPasses", result.metrics.numTestPasses); + form.createHiddenField("numTestFailures", result.metrics.numTestFailures); + form.createHiddenField("numCommandPasses", result.metrics.numCommandPasses); + form.createHiddenField("numCommandFailures", result.metrics.numCommandFailures); + form.createHiddenField("numCommandErrors", result.metrics.numCommandErrors); // Create an input for each test table. The inputs are named // testTable.1, testTable.2, etc. @@ -223,9 +209,9 @@ function parse_resultCell(resultCell,rowNum,form) function get_color_status(element) { var color = element.getAttribute("bgcolor"); - if(color == passColor) return "passed"; - if(color == failColor) return "failed"; - if(color == doneColor) return "done"; + if(color == FeedbackColors.passColor) return "passed"; + if(color == FeedbackColors.failColor) return "failed"; + if(color == FeedbackColors.doneColor) return "done"; return ""; } diff --git a/tests/test_tools/selenium/reference.html b/tests/test_tools/selenium/reference.html new file mode 100644 index 00000000..5533764a --- /dev/null +++ b/tests/test_tools/selenium/reference.html @@ -0,0 +1,3858 @@ +<html> +<head> +<META http-equiv="Content-Type" content="text/html; charset=UTF-8"> +<title>Selenium Reference</title> +</head> +<body> +<h1>Selenium Reference</h1> +<h2>Concepts</h2> +<p>A <strong>command</strong> is what tells Selenium what to do. Selenium commands come in three 'flavors': <strong>Actions</strong>, <strong>Accessors</strong> and <strong>Assertions</strong>. + Each command call is one line in the test table of the form:</p> +<blockquote> +<table border="1" class="table"> +<colgroup> +<col width="39%"> +<col width="33%"> +<col width="28%"> +</colgroup> +<tbody valign="top"> +<tr> +<td>command</td><td>target</td><td>value</td> +</tr> +</tbody> +</table> +</blockquote> +<p> +<strong>Actions</strong> are commands that generally manipulate the state of the application. They do things like "click this link" and "select that option". If an Action fails, or has an error, the execution of the current test is stopped.</p> +<p>Many Actions can be called with the "AndWait" suffix, e.g. "clickAndWait". + This suffix tells Selenium that the action will cause the browser to make a call to the server, + and that Selenium should wait for a new page to load.</p> +<p> +<strong>Accessors</strong> examine the state of the application and store the results in variables, e.g. "storeTitle". They are also used to automatically generate Assertions.</p> +<p> +<strong>Assertions</strong> are like Accessors, but they verify that the state of the application conforms to what is expected. Examples include "make sure the page title is X" and "verify that this checkbox is checked".</p> +<p>All Selenium Assertions can be used in 3 modes: "assert", "verify", and "waitFor". For example, you can "assertText", "verifyText" and "waitForText". When an "assert" fails, the test is aborted. When a "verify" fails, the test will continue execution, logging the failure. This allows a single "assert" to ensure that the application is on the correct page, followed by a bunch of "verify" assertions to test form field values, labels, etc.</p> +<p>"waitFor" commands wait for some condition to become true (which can be useful for testing Ajax applications). + They will succeed immediately if the condition is already true. + However, they will fail and halt the test if the condition does not become true within the current timeout setting + (see the <strong>setTimeout</strong> action below). + </p> +<p> +<strong>Element Locators</strong> tell Selenium which HTML element a command refers to. Many commands require an Element Locator as the "target" attribute. Examples of Element Locators include "elementId" and "document.forms[0].element". These are described more clearly in the next section.</p> +<p> +<strong>Patterns</strong> are used for various reasons, e.g. to specify the expected value of an input field, or identify a select option. Selenium supports various types of pattern, including regular-expressions, all of which are described in more detail below.</p>Defines an object that runs Selenium commands. + +<h3> +<a name="locators"></a>Element Locators</h3> +<p> +Element Locators tell Selenium which HTML element a command refers to. +The format of a locator is:</p> +<blockquote> +<em>locatorType</em><strong>=</strong><em>argument</em> +</blockquote> +<p> +We support the following strategies for locating elements: +</p> +<blockquote> +<dl> +<dt> +<strong>identifier</strong>=<em>id</em> +</dt> +<dd>Select the element with the specified @id attribute. If no match is +found, select the first element whose @name attribute is <em>id</em>. +(This is normally the default; see below.)</dd> +<dt> +<strong>id</strong>=<em>id</em> +</dt> +<dd>Select the element with the specified @id attribute.</dd> +<dt> +<strong>name</strong>=<em>name</em> +</dt> +<dd>Select the first element with the specified @name attribute.</dd> +<dd> +<ul class="first last simple"> +<li>username</li> +<li>name=username</li> +</ul> +</dd> +<dd>The name may optionally be followed by one or more <em>element-filters</em>, separated from the name by whitespace. If the <em>filterType</em> is not specified, <strong>value</strong> is assumed.</dd> +<dd> +<ul class="first last simple"> +<li>name=flavour value=chocolate</li> +</ul> +</dd> +<dt> +<strong>dom</strong>=<em>javascriptExpression</em> +</dt> +<dd> +<dd>Find an element using JavaScript traversal of the HTML Document Object +Model. DOM locators <em>must</em> begin with "document.". +<ul class="first last simple"> +<li>dom=document.forms['myForm'].myDropdown</li> +<li>dom=document.images[56]</li> +</ul> +</dd> +</dd> +<dt> +<strong>xpath</strong>=<em>xpathExpression</em> +</dt> +<dd>Locate an element using an XPath expression. +<ul class="first last simple"> +<li>xpath=//img[@alt='The image alt text']</li> +<li>xpath=//table[@id='table1']//tr[4]/td[2]</li> +</ul> +</dd> +<dt> +<strong>link</strong>=<em>textPattern</em> +</dt> +<dd>Select the link (anchor) element which contains text matching the +specified <em>pattern</em>. +<ul class="first last simple"> +<li>link=The link text</li> +</ul> +</dd> +<dt> +<strong>css</strong>=<em>cssSelectorSyntax</em> +</dt> +<dd>Select the element using css selectors. Please refer to <a href="http://www.w3.org/TR/REC-CSS2/selector.html">CSS2 selectors</a>, <a href="http://www.w3.org/TR/2001/CR-css3-selectors-20011113/">CSS3 selectors</a> for more information. You can also check the TestCssLocators test in the selenium test suite for an example of usage, which is included in the downloaded selenium core package. +<ul class="first last simple"> +<li>css=a[href="#id3"]</li> +<li>css=span#firstChild + span</li> +</ul> +</dd> +<dd>Currently the css selector locator supports all css1, css2 and css3 selectors except namespace in css3, some pseudo classes(:nth-of-type, :nth-last-of-type, :first-of-type, :last-of-type, :only-of-type, :visited, :hover, :active, :focus, :indeterminate) and pseudo elements(::first-line, ::first-letter, ::selection, ::before, ::after). </dd> +</dl> +</blockquote> +<p> +Without an explicit locator prefix, Selenium uses the following default +strategies: +</p> +<ul class="simple"> +<li> +<strong>dom</strong>, for locators starting with "document."</li> +<li> +<strong>xpath</strong>, for locators starting with "//"</li> +<li> +<strong>identifier</strong>, otherwise</li> +</ul> +<h3> +<a name="element-filters">Element Filters</a> +</h3> +<blockquote> +<p>Element filters can be used with a locator to refine a list of candidate elements. They are currently used only in the 'name' element-locator.</p> +<p>Filters look much like locators, ie.</p> +<blockquote> +<em>filterType</em><strong>=</strong><em>argument</em> +</blockquote> +<p>Supported element-filters are:</p> +<p> +<strong>value=</strong><em>valuePattern</em> +</p> +<blockquote> +Matches elements based on their values. This is particularly useful for refining a list of similarly-named toggle-buttons.</blockquote> +<p> +<strong>index=</strong><em>index</em> +</p> +<blockquote> +Selects a single element based on its position in the list (offset from zero).</blockquote> +</blockquote> +<h3> +<a name="patterns"></a>String-match Patterns</h3> +<p> +Various Pattern syntaxes are available for matching string values: +</p> +<blockquote> +<dl> +<dt> +<strong>glob:</strong><em>pattern</em> +</dt> +<dd>Match a string against a "glob" (aka "wildmat") pattern. "Glob" is a +kind of limited regular-expression syntax typically used in command-line +shells. In a glob pattern, "*" represents any sequence of characters, and "?" +represents any single character. Glob patterns match against the entire +string.</dd> +<dt> +<strong>regexp:</strong><em>regexp</em> +</dt> +<dd>Match a string using a regular-expression. The full power of JavaScript +regular-expressions is available.</dd> +<dt> +<strong>exact:</strong><em>string</em> +</dt> +<dd>Match a string exactly, verbatim, without any of that fancy wildcard +stuff.</dd> +</dl> +</blockquote> +<p> +If no pattern prefix is specified, Selenium assumes that it's a "glob" +pattern. +</p> +<h2>Selenium Actions</h2> +<dl> +<dt> +<strong><a name="addSelection"></a>addSelection + ( + locator,optionLocator + ) + </strong> +</dt> +<dd>Add a selection to the set of selected options in a multi-select element using an option locator. + +@see #doSelect for details of option locators<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> identifying a multi-select box</li> +<li>optionLocator - an option locator (a label by default)</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="answerOnNextPrompt"></a>answerOnNextPrompt + ( + answer + ) + </strong> +</dt> +<dd>Instructs Selenium to return the specified answer string in response to +the next JavaScript prompt [window.prompt()].<p>Arguments:</p> +<ul> +<li>answer - the answer to give in response to the prompt pop-up</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="check"></a>check + ( + locator + ) + </strong> +</dt> +<dd>Check a toggle-button (checkbox/radio)<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="chooseCancelOnNextConfirmation"></a>chooseCancelOnNextConfirmation + ( + + ) + </strong> +</dt> +<dd>By default, Selenium's overridden window.confirm() function will +return true, as if the user had manually clicked OK. After running +this command, the next call to confirm() will return false, as if +the user had clicked Cancel.</dd> +<br> +<dt> +<strong><a name="click"></a>click + ( + locator + ) + </strong> +</dt> +<dd>Clicks on a link, button, checkbox or radio button. If the click action +causes a new page to load (like a link usually does), call +waitForPageToLoad.<p>Arguments:</p> +<ul> +<li>locator - an element locator</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="clickAt"></a>clickAt + ( + locator,coordString + ) + </strong> +</dt> +<dd>Clicks on a link, button, checkbox or radio button. If the click action +causes a new page to load (like a link usually does), call +waitForPageToLoad. + +Beware of http://jira.openqa.org/browse/SEL-280, which will lead some event handlers to +get null event arguments. Read the bug for more details, including a workaround.<p>Arguments:</p> +<ul> +<li>locator - an element locator</li> +<li>coordString - specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator.</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="close"></a>close + ( + + ) + </strong> +</dt> +<dd>Simulates the user clicking the "close" button in the titlebar of a popup +window or tab.</dd> +<br> +<dt> +<strong><a name="createCookie"></a>createCookie + ( + nameValuePair,optionsString + ) + </strong> +</dt> +<dd>Create a new cookie whose path and domain are same with those of current page +under test, unless you specified a path for this cookie explicitly.<p>Arguments:</p> +<ul> +<li>nameValuePair - name and value of the cookie in a format "name=value"</li> +<li>optionsString - options for the cookie. Currently supported options include 'path' and 'max_age'. the optionsString's format is "path=/path/, max_age=60". The order of options are irrelevant, the unit of the value of 'max_age' is second.</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="deleteCookie"></a>deleteCookie + ( + name,path + ) + </strong> +</dt> +<dd>Delete a named cookie with specified path.<p>Arguments:</p> +<ul> +<li>name - the name of the cookie to be deleted</li> +<li>path - the path property of the cookie to be deleted</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="dragdrop"></a>dragdrop + ( + locator,movementsString + ) + </strong> +</dt> +<dd>Drags an element a certain distance and then drops it +Beware of http://jira.openqa.org/browse/SEL-280, which will lead some event handlers to +get null event arguments. Read the bug for more details, including a workaround.<p>Arguments:</p> +<ul> +<li>locator - an element locator</li> +<li>movementsString - offset in pixels from the current location to which the element should be moved, e.g., "+70,-300"</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="fireEvent"></a>fireEvent + ( + locator,eventName + ) + </strong> +</dt> +<dd>Explicitly simulate an event, to trigger the corresponding "on<em>event</em>" +handler.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +<li>eventName - the event name, e.g. "focus" or "blur"</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="goBack"></a>goBack + ( + + ) + </strong> +</dt> +<dd>Simulates the user clicking the "back" button on their browser.</dd> +<br> +<dt> +<strong><a name="keyDown"></a>keyDown + ( + locator,keySequence + ) + </strong> +</dt> +<dd>Simulates a user pressing a key (without releasing it yet).<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +<li>keySequence - Either be a string("\" followed by the numeric keycode of the key to be pressed, normally the ASCII value of that key), or a single character. For example: "w", "\119".</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="keyPress"></a>keyPress + ( + locator,keySequence + ) + </strong> +</dt> +<dd>Simulates a user pressing and releasing a key.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +<li>keySequence - Either be a string("\" followed by the numeric keycode of the key to be pressed, normally the ASCII value of that key), or a single character. For example: "w", "\119".</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="keyUp"></a>keyUp + ( + locator,keySequence + ) + </strong> +</dt> +<dd>Simulates a user releasing a key.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +<li>keySequence - Either be a string("\" followed by the numeric keycode of the key to be pressed, normally the ASCII value of that key), or a single character. For example: "w", "\119".</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="mouseDown"></a>mouseDown + ( + locator + ) + </strong> +</dt> +<dd>Simulates a user pressing the mouse button (without releasing it yet) on +the specified element.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="mouseDownAt"></a>mouseDownAt + ( + locator,coordString + ) + </strong> +</dt> +<dd>Simulates a user pressing the mouse button (without releasing it yet) on +the specified element. + +Beware of http://jira.openqa.org/browse/SEL-280, which will lead some event handlers to +get null event arguments. Read the bug for more details, including a workaround.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +<li>coordString - specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator.</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="mouseMove"></a>mouseMove + ( + locator + ) + </strong> +</dt> +<dd>Simulates a user pressing the mouse button (without releasing it yet) on +the specified element.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="mouseMoveAt"></a>mouseMoveAt + ( + locator,coordString + ) + </strong> +</dt> +<dd>Simulates a user pressing the mouse button (without releasing it yet) on +the specified element. + +Beware of http://jira.openqa.org/browse/SEL-280, which will lead some event handlers to +get null event arguments. Read the bug for more details, including a workaround.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +<li>coordString - specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator.</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="mouseOut"></a>mouseOut + ( + locator + ) + </strong> +</dt> +<dd>Simulates a user moving the mouse pointer away from the specified element.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="mouseOver"></a>mouseOver + ( + locator + ) + </strong> +</dt> +<dd>Simulates a user hovering a mouse over the specified element.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="mouseUp"></a>mouseUp + ( + locator + ) + </strong> +</dt> +<dd>Simulates a user pressing the mouse button (without releasing it yet) on +the specified element.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="mouseUpAt"></a>mouseUpAt + ( + locator,coordString + ) + </strong> +</dt> +<dd>Simulates a user pressing the mouse button (without releasing it yet) on +the specified element. + +Beware of http://jira.openqa.org/browse/SEL-280, which will lead some event handlers to +get null event arguments. Read the bug for more details, including a workaround.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +<li>coordString - specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator.</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="open"></a>open + ( + url + ) + </strong> +</dt> +<dd>Opens an URL in the test frame. This accepts both relative and absolute +URLs. + +The "open" command waits for the page to load before proceeding, +ie. the "AndWait" suffix is implicit. + +<em>Note</em>: The URL must be on the same domain as the runner HTML +due to security restrictions in the browser (Same Origin Policy). If you +need to open an URL on another domain, use the Selenium Server to start a +new browser session on that domain.<p>Arguments:</p> +<ul> +<li>url - the URL to open; may be relative or absolute</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="refresh"></a>refresh + ( + + ) + </strong> +</dt> +<dd>Simulates the user clicking the "Refresh" button on their browser.</dd> +<br> +<dt> +<strong><a name="removeSelection"></a>removeSelection + ( + locator,optionLocator + ) + </strong> +</dt> +<dd>Remove a selection from the set of selected options in a multi-select element using an option locator. + +@see #doSelect for details of option locators<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> identifying a multi-select box</li> +<li>optionLocator - an option locator (a label by default)</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="select"></a>select + ( + selectLocator,optionLocator + ) + </strong> +</dt> +<dd>Select an option from a drop-down using an option locator. + +<p> +Option locators provide different ways of specifying options of an HTML +Select element (e.g. for selecting a specific option, or for asserting +that the selected option satisfies a specification). There are several +forms of Select Option Locator. +</p> +<dl> +<dt> +<strong>label</strong>=<em>labelPattern</em> +</dt> +<dd>matches options based on their labels, i.e. the visible text. (This +is the default.) +<ul class="first last simple"> +<li>label=regexp:^[Oo]ther</li> +</ul> +</dd> +<dt> +<strong>value</strong>=<em>valuePattern</em> +</dt> +<dd>matches options based on their values. +<ul class="first last simple"> +<li>value=other</li> +</ul> +</dd> +<dt> +<strong>id</strong>=<em>id</em> +</dt> +<dd>matches options based on their ids. +<ul class="first last simple"> +<li>id=option1</li> +</ul> +</dd> +<dt> +<strong>index</strong>=<em>index</em> +</dt> +<dd>matches an option based on its index (offset from zero). +<ul class="first last simple"> +<li>index=2</li> +</ul> +</dd> +</dl> +<p> +If no option locator prefix is provided, the default behaviour is to match on <strong>label</strong>. +</p> +<p>Arguments:</p> +<ul> +<li>selectLocator - an <a href="#locators">element locator</a> identifying a drop-down menu</li> +<li>optionLocator - an option locator (a label by default)</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="selectFrame"></a>selectFrame + ( + locator + ) + </strong> +</dt> +<dd>Selects a frame within the current window. (You may invoke this command +multiple times to select nested frames.) To select the parent frame, use +"relative=parent" as a locator; to select the top frame, use "relative=top". + +<p>You may also use a DOM expression to identify the frame you want directly, +like this: <code>dom=frames["main"].frames["subframe"]</code> +</p> +<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> identifying a frame or iframe</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="selectWindow"></a>selectWindow + ( + windowID + ) + </strong> +</dt> +<dd>Selects a popup window; once a popup window has been selected, all +commands go to that window. To select the main window again, use "null" +as the target.<p>Arguments:</p> +<ul> +<li>windowID - the JavaScript window ID of the window to select</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="setContext"></a>setContext + ( + context,logLevelThreshold + ) + </strong> +</dt> +<dd>Writes a message to the status bar and adds a note to the browser-side +log. + +<p>If logLevelThreshold is specified, set the threshold for logging +to that level (debug, info, warn, error).</p> +<p>(Note that the browser-side logs will <i>not</i> be sent back to the +server, and are invisible to the Client Driver.)</p> +<p>Arguments:</p> +<ul> +<li>context - the message to be sent to the browser</li> +<li>logLevelThreshold - one of "debug", "info", "warn", "error", sets the threshold for browser-side logging</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="setCursorPosition"></a>setCursorPosition + ( + locator,position + ) + </strong> +</dt> +<dd>Moves the text cursor to the specified position in the given input element or textarea. +This method will fail if the specified element isn't an input element or textarea.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> pointing to an input element or textarea</li> +<li>position - the numerical position of the cursor in the field; position should be 0 to move the position to the beginning of the field. You can also set the cursor to -1 to move it to the end of the field.</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="setTimeout"></a>setTimeout + ( + timeout + ) + </strong> +</dt> +<dd>Specifies the amount of time that Selenium will wait for actions to complete. + +<p>Actions that require waiting include "open" and the "waitFor*" actions.</p> +The default timeout is 30 seconds.<p>Arguments:</p> +<ul> +<li>timeout - a timeout in milliseconds, after which the action will return with an error</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="submit"></a>submit + ( + formLocator + ) + </strong> +</dt> +<dd>Submit the specified form. This is particularly useful for forms without +submit buttons, e.g. single-input "Search" forms.<p>Arguments:</p> +<ul> +<li>formLocator - an <a href="#locators">element locator</a> for the form you want to submit</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="type"></a>type + ( + locator,value + ) + </strong> +</dt> +<dd>Sets the value of an input field, as though you typed it in. + +<p>Can also be used to set the value of combo boxes, check boxes, etc. In these cases, +value should be the value of the option selected, not the visible text.</p> +<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +<li>value - the value to type</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="uncheck"></a>uncheck + ( + locator + ) + </strong> +</dt> +<dd>Uncheck a toggle-button (checkbox/radio)<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="waitForCondition"></a>waitForCondition + ( + script,timeout + ) + </strong> +</dt> +<dd>Runs the specified JavaScript snippet repeatedly until it evaluates to "true". +The snippet may have multiple lines, but only the result of the last line +will be considered. + +<p>Note that, by default, the snippet will be run in the runner's test window, not in the window +of your application. To get the window of your application, you can use +the JavaScript snippet <code>selenium.browserbot.getCurrentWindow()</code>, and then +run your JavaScript in there</p> +<p>Arguments:</p> +<ul> +<li>script - the JavaScript snippet to run</li> +<li>timeout - a timeout in milliseconds, after which this command will return with an error</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="waitForPageToLoad"></a>waitForPageToLoad + ( + timeout + ) + </strong> +</dt> +<dd>Waits for a new page to load. + +<p>You can use this command instead of the "AndWait" suffixes, "clickAndWait", "selectAndWait", "typeAndWait" etc. +(which are only available in the JS API).</p> +<p>Selenium constantly keeps track of new pages loading, and sets a "newPageLoaded" +flag when it first notices a page load. Running any other Selenium command after +turns the flag to false. Hence, if you want to wait for a page to load, you must +wait immediately after a Selenium command that caused a page-load.</p> +<p>Arguments:</p> +<ul> +<li>timeout - a timeout in milliseconds, after which this command will return with an error</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="waitForPopUp"></a>waitForPopUp + ( + windowID,timeout + ) + </strong> +</dt> +<dd>Waits for a popup window to appear and load up.<p>Arguments:</p> +<ul> +<li>windowID - the JavaScript window ID of the window that will appear</li> +<li>timeout - a timeout in milliseconds, after which the action will return with an error</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="windowFocus"></a>windowFocus + ( + windowName + ) + </strong> +</dt> +<dd>Gives focus to a window<p>Arguments:</p> +<ul> +<li>windowName - name of the window to be given focus</li> +</ul> +</dd> +<br> +<dt> +<strong><a name="windowMaximize"></a>windowMaximize + ( + windowName + ) + </strong> +</dt> +<dd>Resize window to take up the entire screen<p>Arguments:</p> +<ul> +<li>windowName - name of the window to be enlarged</li> +</ul> +</dd> +<br> +</dl> +<h2>Selenium Accessors</h2> +<dl> +<dt> +<strong><a name="storeAlert"></a>storeAlert + + ( + + variableName + ) + + </strong> +</dt> +<dd>Retrieves the message of a JavaScript alert generated during the previous action, or fail if there were no alerts. + +<p>Getting an alert has the same effect as manually clicking OK. If an +alert is generated but you do not get/verify it, the next Selenium action +will fail.</p> +<p>NOTE: under Selenium, JavaScript alerts will NOT pop up a visible alert +dialog.</p> +<p>NOTE: Selenium does NOT support JavaScript alerts that are generated in a +page's onload() event handler. In this case a visible dialog WILL be +generated and Selenium will hang until someone manually clicks OK.</p> +<p> +<dl> +<dt>Returns: </dt> +<dd>The message of the most recent JavaScript alert</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertAlert + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotAlert + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyAlert + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotAlert + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForAlert + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotAlert + ( + <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeAllButtons"></a>storeAllButtons + + ( + + variableName + ) + + </strong> +</dt> +<dd>Returns the IDs of all buttons on the page. + +<p>If a given button has no ID, it will appear as "" in this array.</p> +<p> +<dl> +<dt>Returns: </dt> +<dd>the IDs of all buttons on the page</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertAllButtons + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotAllButtons + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyAllButtons + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotAllButtons + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForAllButtons + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotAllButtons + ( + <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeAllFields"></a>storeAllFields + + ( + + variableName + ) + + </strong> +</dt> +<dd>Returns the IDs of all input fields on the page. + +<p>If a given field has no ID, it will appear as "" in this array.</p> +<p> +<dl> +<dt>Returns: </dt> +<dd>the IDs of all field on the page</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertAllFields + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotAllFields + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyAllFields + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotAllFields + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForAllFields + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotAllFields + ( + <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeAllLinks"></a>storeAllLinks + + ( + + variableName + ) + + </strong> +</dt> +<dd>Returns the IDs of all links on the page. + +<p>If a given link has no ID, it will appear as "" in this array.</p> +<p> +<dl> +<dt>Returns: </dt> +<dd>the IDs of all links on the page</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertAllLinks + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotAllLinks + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyAllLinks + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotAllLinks + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForAllLinks + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotAllLinks + ( + <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeAllWindowIds"></a>storeAllWindowIds + + ( + + variableName + ) + + </strong> +</dt> +<dd>Returns the IDs of all windows that the browser knows about.<p> +<dl> +<dt>Returns: </dt> +<dd>the IDs of all windows that the browser knows about.</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertAllWindowIds + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotAllWindowIds + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyAllWindowIds + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotAllWindowIds + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForAllWindowIds + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotAllWindowIds + ( + <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeAllWindowNames"></a>storeAllWindowNames + + ( + + variableName + ) + + </strong> +</dt> +<dd>Returns the names of all windows that the browser knows about.<p> +<dl> +<dt>Returns: </dt> +<dd>the names of all windows that the browser knows about.</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertAllWindowNames + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotAllWindowNames + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyAllWindowNames + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotAllWindowNames + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForAllWindowNames + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotAllWindowNames + ( + <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeAllWindowTitles"></a>storeAllWindowTitles + + ( + + variableName + ) + + </strong> +</dt> +<dd>Returns the titles of all windows that the browser knows about.<p> +<dl> +<dt>Returns: </dt> +<dd>the titles of all windows that the browser knows about.</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertAllWindowTitles + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotAllWindowTitles + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyAllWindowTitles + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotAllWindowTitles + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForAllWindowTitles + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotAllWindowTitles + ( + <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeAttribute"></a>storeAttribute + + ( + attributeLocator, + + variableName + ) + + </strong> +</dt> +<dd>Gets the value of an element attribute. + +Beware of http://jira.openqa.org/browse/SEL-280, which will lead some event handlers to +get null event arguments. Read the bug for more details, including a workaround.<p>Arguments:</p> +<ul> +<li>attributeLocator - an element locator followed by an</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>the value of the specified attribute</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertAttribute + ( + attributeLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotAttribute + ( + attributeLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyAttribute + ( + attributeLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotAttribute + ( + attributeLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForAttribute + ( + attributeLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotAttribute + ( + attributeLocator, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeAttributeFromAllWindows"></a>storeAttributeFromAllWindows + + ( + attributeName, + + variableName + ) + + </strong> +</dt> +<dd>Returns every instance of some attribute from all known windows.<p>Arguments:</p> +<ul> +<li>attributeName - name of an attribute on the windows</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>the set of values of this attribute from all known windows.</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertAttributeFromAllWindows + ( + attributeName, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotAttributeFromAllWindows + ( + attributeName, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyAttributeFromAllWindows + ( + attributeName, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotAttributeFromAllWindows + ( + attributeName, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForAttributeFromAllWindows + ( + attributeName, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotAttributeFromAllWindows + ( + attributeName, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeBodyText"></a>storeBodyText + + ( + + variableName + ) + + </strong> +</dt> +<dd>Gets the entire text of the page.<p> +<dl> +<dt>Returns: </dt> +<dd>the entire text of the page</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertBodyText + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotBodyText + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyBodyText + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotBodyText + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForBodyText + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotBodyText + ( + <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeConfirmation"></a>storeConfirmation + + ( + + variableName + ) + + </strong> +</dt> +<dd>Retrieves the message of a JavaScript confirmation dialog generated during +the previous action. + +<p> +By default, the confirm function will return true, having the same effect +as manually clicking OK. This can be changed by prior execution of the +chooseCancelOnNextConfirmation command. If an confirmation is generated +but you do not get/verify it, the next Selenium action will fail. +</p> +<p> +NOTE: under Selenium, JavaScript confirmations will NOT pop up a visible +dialog. +</p> +<p> +NOTE: Selenium does NOT support JavaScript confirmations that are +generated in a page's onload() event handler. In this case a visible +dialog WILL be generated and Selenium will hang until you manually click +OK. +</p> +<p> +<dl> +<dt>Returns: </dt> +<dd>the message of the most recent JavaScript confirmation dialog</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertConfirmation + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotConfirmation + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyConfirmation + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotConfirmation + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForConfirmation + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotConfirmation + ( + <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeCookie"></a>storeCookie + + ( + + variableName + ) + + </strong> +</dt> +<dd>Return all cookies of the current page under test.<p> +<dl> +<dt>Returns: </dt> +<dd>all cookies of the current page under test</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertCookie + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotCookie + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyCookie + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotCookie + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForCookie + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotCookie + ( + <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeCursorPosition"></a>storeCursorPosition + + ( + locator, + + variableName + ) + + </strong> +</dt> +<dd>Retrieves the text cursor position in the given input element or textarea; beware, this may not work perfectly on all browsers. + +<p>Specifically, if the cursor/selection has been cleared by JavaScript, this command will tend to +return the position of the last location of the cursor, even though the cursor is now gone from the page. This is filed as <a href="http://jira.openqa.org/browse/SEL-243">SEL-243</a>.</p> +This method will fail if the specified element isn't an input element or textarea, or there is no cursor in the element.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> pointing to an input element or textarea</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>the numerical position of the cursor in the field</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertCursorPosition + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotCursorPosition + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyCursorPosition + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotCursorPosition + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForCursorPosition + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotCursorPosition + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeElementHeight"></a>storeElementHeight + + ( + locator, + + variableName + ) + + </strong> +</dt> +<dd>Retrieves the height of an element<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> pointing to an element</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>height of an element in pixels</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertElementHeight + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotElementHeight + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyElementHeight + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotElementHeight + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForElementHeight + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotElementHeight + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeElementIndex"></a>storeElementIndex + + ( + locator, + + variableName + ) + + </strong> +</dt> +<dd>Get the relative index of an element to its parent (starting from 0). The comment node and empty text node +will be ignored.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> pointing to an element</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>of relative index of the element to its parent (starting from 0)</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertElementIndex + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotElementIndex + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyElementIndex + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotElementIndex + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForElementIndex + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotElementIndex + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeElementPositionLeft"></a>storeElementPositionLeft + + ( + locator, + + variableName + ) + + </strong> +</dt> +<dd>Retrieves the horizontal position of an element<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> pointing to an element OR an element itself</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>of pixels from the edge of the frame.</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertElementPositionLeft + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotElementPositionLeft + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyElementPositionLeft + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotElementPositionLeft + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForElementPositionLeft + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotElementPositionLeft + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeElementPositionTop"></a>storeElementPositionTop + + ( + locator, + + variableName + ) + + </strong> +</dt> +<dd>Retrieves the vertical position of an element<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> pointing to an element OR an element itself</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>of pixels from the edge of the frame.</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertElementPositionTop + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotElementPositionTop + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyElementPositionTop + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotElementPositionTop + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForElementPositionTop + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotElementPositionTop + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeElementWidth"></a>storeElementWidth + + ( + locator, + + variableName + ) + + </strong> +</dt> +<dd>Retrieves the width of an element<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> pointing to an element</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>width of an element in pixels</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertElementWidth + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotElementWidth + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyElementWidth + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotElementWidth + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForElementWidth + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotElementWidth + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeEval"></a>storeEval + + ( + script, + + variableName + ) + + </strong> +</dt> +<dd>Gets the result of evaluating the specified JavaScript snippet. The snippet may +have multiple lines, but only the result of the last line will be returned. + +<p>Note that, by default, the snippet will run in the context of the "selenium" +object itself, so <code>this</code> will refer to the Selenium object, and <code>window</code> will +refer to the top-level runner test window, not the window of your application.</p> +<p>If you need a reference to the window of your application, you can refer +to <code>this.browserbot.getCurrentWindow()</code> and if you need to use +a locator to refer to a single element in your application page, you can +use <code>this.page().findElement("foo")</code> where "foo" is your locator.</p> +<p>Arguments:</p> +<ul> +<li>script - the JavaScript snippet to run</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>the results of evaluating the snippet</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertEval + ( + script, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotEval + ( + script, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyEval + ( + script, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotEval + ( + script, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForEval + ( + script, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotEval + ( + script, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeExpression"></a>storeExpression + + ( + expression, + + variableName + ) + + </strong> +</dt> +<dd>Returns the specified expression. + +<p>This is useful because of JavaScript preprocessing. +It is used to generate commands like assertExpression and waitForExpression.</p> +<p>Arguments:</p> +<ul> +<li>expression - the value to return</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>the value passed in</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertExpression + ( + expression, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotExpression + ( + expression, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyExpression + ( + expression, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotExpression + ( + expression, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForExpression + ( + expression, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotExpression + ( + expression, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeHtmlSource"></a>storeHtmlSource + + ( + + variableName + ) + + </strong> +</dt> +<dd>Returns the entire HTML source between the opening and +closing "html" tags.<p> +<dl> +<dt>Returns: </dt> +<dd>the entire HTML source</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertHtmlSource + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotHtmlSource + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyHtmlSource + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotHtmlSource + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForHtmlSource + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotHtmlSource + ( + <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeLocation"></a>storeLocation + + ( + + variableName + ) + + </strong> +</dt> +<dd>Gets the absolute URL of the current page.<p> +<dl> +<dt>Returns: </dt> +<dd>the absolute URL of the current page</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertLocation + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotLocation + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyLocation + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotLocation + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForLocation + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotLocation + ( + <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeLogMessages"></a>storeLogMessages + + ( + + variableName + ) + + </strong> +</dt> +<dd>Return the contents of the log. + +<p>This is a placeholder intended to make the code generator make this API +available to clients. The selenium server will intercept this call, however, +and return its recordkeeping of log messages since the last call to this API. +Thus this code in JavaScript will never be called.</p> +<p>The reason I opted for a servercentric solution is to be able to support +multiple frames served from different domains, which would break a +centralized JavaScript logging mechanism under some conditions.</p> +<p> +<dl> +<dt>Returns: </dt> +<dd>all log messages seen since the last call to this API</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertLogMessages + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotLogMessages + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyLogMessages + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotLogMessages + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForLogMessages + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotLogMessages + ( + <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storePrompt"></a>storePrompt + + ( + + variableName + ) + + </strong> +</dt> +<dd>Retrieves the message of a JavaScript question prompt dialog generated during +the previous action. + +<p>Successful handling of the prompt requires prior execution of the +answerOnNextPrompt command. If a prompt is generated but you +do not get/verify it, the next Selenium action will fail.</p> +<p>NOTE: under Selenium, JavaScript prompts will NOT pop up a visible +dialog.</p> +<p>NOTE: Selenium does NOT support JavaScript prompts that are generated in a +page's onload() event handler. In this case a visible dialog WILL be +generated and Selenium will hang until someone manually clicks OK.</p> +<p> +<dl> +<dt>Returns: </dt> +<dd>the message of the most recent JavaScript question prompt</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertPrompt + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotPrompt + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyPrompt + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotPrompt + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForPrompt + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotPrompt + ( + <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeSelectedId"></a>storeSelectedId + + ( + selectLocator, + + variableName + ) + + </strong> +</dt> +<dd>Gets option element ID for selected option in the specified select element.<p>Arguments:</p> +<ul> +<li>selectLocator - an <a href="#locators">element locator</a> identifying a drop-down menu</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>the selected option ID in the specified select drop-down</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertSelectedId + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotSelectedId + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifySelectedId + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotSelectedId + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForSelectedId + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotSelectedId + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeSelectedIds"></a>storeSelectedIds + + ( + selectLocator, + + variableName + ) + + </strong> +</dt> +<dd>Gets all option element IDs for selected options in the specified select or multi-select element.<p>Arguments:</p> +<ul> +<li>selectLocator - an <a href="#locators">element locator</a> identifying a drop-down menu</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>an array of all selected option IDs in the specified select drop-down</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertSelectedIds + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotSelectedIds + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifySelectedIds + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotSelectedIds + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForSelectedIds + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotSelectedIds + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeSelectedIndex"></a>storeSelectedIndex + + ( + selectLocator, + + variableName + ) + + </strong> +</dt> +<dd>Gets option index (option number, starting at 0) for selected option in the specified select element.<p>Arguments:</p> +<ul> +<li>selectLocator - an <a href="#locators">element locator</a> identifying a drop-down menu</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>the selected option index in the specified select drop-down</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertSelectedIndex + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotSelectedIndex + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifySelectedIndex + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotSelectedIndex + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForSelectedIndex + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotSelectedIndex + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeSelectedIndexes"></a>storeSelectedIndexes + + ( + selectLocator, + + variableName + ) + + </strong> +</dt> +<dd>Gets all option indexes (option number, starting at 0) for selected options in the specified select or multi-select element.<p>Arguments:</p> +<ul> +<li>selectLocator - an <a href="#locators">element locator</a> identifying a drop-down menu</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>an array of all selected option indexes in the specified select drop-down</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertSelectedIndexes + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotSelectedIndexes + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifySelectedIndexes + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotSelectedIndexes + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForSelectedIndexes + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotSelectedIndexes + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeSelectedLabel"></a>storeSelectedLabel + + ( + selectLocator, + + variableName + ) + + </strong> +</dt> +<dd>Gets option label (visible text) for selected option in the specified select element.<p>Arguments:</p> +<ul> +<li>selectLocator - an <a href="#locators">element locator</a> identifying a drop-down menu</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>the selected option label in the specified select drop-down</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertSelectedLabel + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotSelectedLabel + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifySelectedLabel + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotSelectedLabel + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForSelectedLabel + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotSelectedLabel + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeSelectedLabels"></a>storeSelectedLabels + + ( + selectLocator, + + variableName + ) + + </strong> +</dt> +<dd>Gets all option labels (visible text) for selected options in the specified select or multi-select element.<p>Arguments:</p> +<ul> +<li>selectLocator - an <a href="#locators">element locator</a> identifying a drop-down menu</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>an array of all selected option labels in the specified select drop-down</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertSelectedLabels + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotSelectedLabels + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifySelectedLabels + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotSelectedLabels + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForSelectedLabels + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotSelectedLabels + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeSelectedValue"></a>storeSelectedValue + + ( + selectLocator, + + variableName + ) + + </strong> +</dt> +<dd>Gets option value (value attribute) for selected option in the specified select element.<p>Arguments:</p> +<ul> +<li>selectLocator - an <a href="#locators">element locator</a> identifying a drop-down menu</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>the selected option value in the specified select drop-down</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertSelectedValue + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotSelectedValue + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifySelectedValue + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotSelectedValue + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForSelectedValue + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotSelectedValue + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeSelectedValues"></a>storeSelectedValues + + ( + selectLocator, + + variableName + ) + + </strong> +</dt> +<dd>Gets all option values (value attributes) for selected options in the specified select or multi-select element.<p>Arguments:</p> +<ul> +<li>selectLocator - an <a href="#locators">element locator</a> identifying a drop-down menu</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>an array of all selected option values in the specified select drop-down</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertSelectedValues + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotSelectedValues + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifySelectedValues + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotSelectedValues + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForSelectedValues + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotSelectedValues + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeSelectOptions"></a>storeSelectOptions + + ( + selectLocator, + + variableName + ) + + </strong> +</dt> +<dd>Gets all option labels in the specified select drop-down.<p>Arguments:</p> +<ul> +<li>selectLocator - an <a href="#locators">element locator</a> identifying a drop-down menu</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>an array of all option labels in the specified select drop-down</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertSelectOptions + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotSelectOptions + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifySelectOptions + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotSelectOptions + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForSelectOptions + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotSelectOptions + ( + selectLocator, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeTable"></a>storeTable + + ( + tableCellAddress, + + variableName + ) + + </strong> +</dt> +<dd>Gets the text from a cell of a table. The cellAddress syntax +tableLocator.row.column, where row and column start at 0.<p>Arguments:</p> +<ul> +<li>tableCellAddress - a cell address, e.g. "foo.1.4"</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>the text from the specified cell</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertTable + ( + tableCellAddress, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotTable + ( + tableCellAddress, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyTable + ( + tableCellAddress, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotTable + ( + tableCellAddress, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForTable + ( + tableCellAddress, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotTable + ( + tableCellAddress, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeText"></a>storeText + + ( + locator, + + variableName + ) + + </strong> +</dt> +<dd>Gets the text of an element. This works for any element that contains +text. This command uses either the textContent (Mozilla-like browsers) or +the innerText (IE-like browsers) of the element, which is the rendered +text shown to the user.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>the text of the element</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertText + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotText + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyText + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotText + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForText + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotText + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeTitle"></a>storeTitle + + ( + + variableName + ) + + </strong> +</dt> +<dd>Gets the title of the current page.<p> +<dl> +<dt>Returns: </dt> +<dd>the title of the current page</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertTitle + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotTitle + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyTitle + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotTitle + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForTitle + ( + <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotTitle + ( + <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeValue"></a>storeValue + + ( + locator, + + variableName + ) + + </strong> +</dt> +<dd>Gets the (whitespace-trimmed) value of an input field (or anything else with a value parameter). +For checkbox/radio elements, the value will be "on" or "off" depending on +whether the element is checked or not.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>the element value, or "on/off" for checkbox/radio elements</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertValue + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>assertNotValue + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyValue + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>verifyNotValue + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForValue + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +<li>waitForNotValue + ( + locator, <a href="#patterns">pattern</a> + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeWhetherThisFrameMatchFrameExpression"></a>storeWhetherThisFrameMatchFrameExpression + + ( + currentFrameString, +target, + + variableName + ) + + </strong> +</dt> +<dd>Determine whether current/locator identify the frame containing this running code. + +<p>This is useful in proxy injection mode, where this code runs in every +browser frame and window, and sometimes the selenium server needs to identify +the "current" frame. In this case, when the test calls selectFrame, this +routine is called for each frame to figure out which one has been selected. +The selected frame will return true, while all others will return false.</p> +<p>Arguments:</p> +<ul> +<li>currentFrameString - starting frame</li> +<li>target - new frame (which might be relative to the current one)</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>true if the new frame is this code's window</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertWhetherThisFrameMatchFrameExpression + ( + currentFrameString, target + ) + </li> +<li>assertNotWhetherThisFrameMatchFrameExpression + ( + currentFrameString, target + ) + </li> +<li>verifyWhetherThisFrameMatchFrameExpression + ( + currentFrameString, target + ) + </li> +<li>verifyNotWhetherThisFrameMatchFrameExpression + ( + currentFrameString, target + ) + </li> +<li>waitForWhetherThisFrameMatchFrameExpression + ( + currentFrameString, target + ) + </li> +<li>waitForNotWhetherThisFrameMatchFrameExpression + ( + currentFrameString, target + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeAlertPresent"></a>storeAlertPresent + + ( + + variableName + ) + + </strong> +</dt> +<dd>Has an alert occurred? + +<p> +This function never throws an exception +</p> +<p> +<dl> +<dt>Returns: </dt> +<dd>true if there is an alert</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertAlertPresent + ( + + ) + </li> +<li>assertAlertNotPresent + ( + + ) + </li> +<li>verifyAlertPresent + ( + + ) + </li> +<li>verifyAlertNotPresent + ( + + ) + </li> +<li>waitForAlertPresent + ( + + ) + </li> +<li>waitForAlertNotPresent + ( + + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeChecked"></a>storeChecked + + ( + locator, + + variableName + ) + + </strong> +</dt> +<dd>Gets whether a toggle-button (checkbox/radio) is checked. Fails if the specified element doesn't exist or isn't a toggle-button.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> pointing to a checkbox or radio button</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>true if the checkbox is checked, false otherwise</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertChecked + ( + locator + ) + </li> +<li>assertNotChecked + ( + locator + ) + </li> +<li>verifyChecked + ( + locator + ) + </li> +<li>verifyNotChecked + ( + locator + ) + </li> +<li>waitForChecked + ( + locator + ) + </li> +<li>waitForNotChecked + ( + locator + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeConfirmationPresent"></a>storeConfirmationPresent + + ( + + variableName + ) + + </strong> +</dt> +<dd>Has confirm() been called? + +<p> +This function never throws an exception +</p> +<p> +<dl> +<dt>Returns: </dt> +<dd>true if there is a pending confirmation</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertConfirmationPresent + ( + + ) + </li> +<li>assertConfirmationNotPresent + ( + + ) + </li> +<li>verifyConfirmationPresent + ( + + ) + </li> +<li>verifyConfirmationNotPresent + ( + + ) + </li> +<li>waitForConfirmationPresent + ( + + ) + </li> +<li>waitForConfirmationNotPresent + ( + + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeEditable"></a>storeEditable + + ( + locator, + + variableName + ) + + </strong> +</dt> +<dd>Determines whether the specified input element is editable, ie hasn't been disabled. +This method will fail if the specified element isn't an input element.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>true if the input element is editable, false otherwise</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertEditable + ( + locator + ) + </li> +<li>assertNotEditable + ( + locator + ) + </li> +<li>verifyEditable + ( + locator + ) + </li> +<li>verifyNotEditable + ( + locator + ) + </li> +<li>waitForEditable + ( + locator + ) + </li> +<li>waitForNotEditable + ( + locator + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeElementPresent"></a>storeElementPresent + + ( + locator, + + variableName + ) + + </strong> +</dt> +<dd>Verifies that the specified element is somewhere on the page.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>true if the element is present, false otherwise</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertElementPresent + ( + locator + ) + </li> +<li>assertElementNotPresent + ( + locator + ) + </li> +<li>verifyElementPresent + ( + locator + ) + </li> +<li>verifyElementNotPresent + ( + locator + ) + </li> +<li>waitForElementPresent + ( + locator + ) + </li> +<li>waitForElementNotPresent + ( + locator + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeOrdered"></a>storeOrdered + + ( + locator1, +locator2, + + variableName + ) + + </strong> +</dt> +<dd>Check if these two elements have same parent and are ordered. Two same elements will +not be considered ordered.<p>Arguments:</p> +<ul> +<li>locator1 - an <a href="#locators">element locator</a> pointing to the first element</li> +<li>locator2 - an <a href="#locators">element locator</a> pointing to the second element</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>true if two elements are ordered and have same parent, false otherwise</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertOrdered + ( + locator1, locator2 + ) + </li> +<li>assertNotOrdered + ( + locator1, locator2 + ) + </li> +<li>verifyOrdered + ( + locator1, locator2 + ) + </li> +<li>verifyNotOrdered + ( + locator1, locator2 + ) + </li> +<li>waitForOrdered + ( + locator1, locator2 + ) + </li> +<li>waitForNotOrdered + ( + locator1, locator2 + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storePromptPresent"></a>storePromptPresent + + ( + + variableName + ) + + </strong> +</dt> +<dd>Has a prompt occurred? + +<p> +This function never throws an exception +</p> +<p> +<dl> +<dt>Returns: </dt> +<dd>true if there is a pending prompt</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertPromptPresent + ( + + ) + </li> +<li>assertPromptNotPresent + ( + + ) + </li> +<li>verifyPromptPresent + ( + + ) + </li> +<li>verifyPromptNotPresent + ( + + ) + </li> +<li>waitForPromptPresent + ( + + ) + </li> +<li>waitForPromptNotPresent + ( + + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeSomethingSelected"></a>storeSomethingSelected + + ( + selectLocator, + + variableName + ) + + </strong> +</dt> +<dd>Determines whether some option in a drop-down menu is selected.<p>Arguments:</p> +<ul> +<li>selectLocator - an <a href="#locators">element locator</a> identifying a drop-down menu</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>true if some option has been selected, false otherwise</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertSomethingSelected + ( + selectLocator + ) + </li> +<li>assertNotSomethingSelected + ( + selectLocator + ) + </li> +<li>verifySomethingSelected + ( + selectLocator + ) + </li> +<li>verifyNotSomethingSelected + ( + selectLocator + ) + </li> +<li>waitForSomethingSelected + ( + selectLocator + ) + </li> +<li>waitForNotSomethingSelected + ( + selectLocator + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeTextPresent"></a>storeTextPresent + + ( + pattern, + + variableName + ) + + </strong> +</dt> +<dd>Verifies that the specified text pattern appears somewhere on the rendered page shown to the user.<p>Arguments:</p> +<ul> +<li>pattern - a <a href="#patterns">pattern</a> to match with the text of the page</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>true if the pattern matches the text, false otherwise</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertTextPresent + ( + pattern + ) + </li> +<li>assertTextNotPresent + ( + pattern + ) + </li> +<li>verifyTextPresent + ( + pattern + ) + </li> +<li>verifyTextNotPresent + ( + pattern + ) + </li> +<li>waitForTextPresent + ( + pattern + ) + </li> +<li>waitForTextNotPresent + ( + pattern + ) + </li> +</ul> +</dd> +<br> +<dt> +<strong><a name="storeVisible"></a>storeVisible + + ( + locator, + + variableName + ) + + </strong> +</dt> +<dd>Determines if the specified element is visible. An +element can be rendered invisible by setting the CSS "visibility" +property to "hidden", or the "display" property to "none", either for the +element itself or one if its ancestors. This method will fail if +the element is not present.<p>Arguments:</p> +<ul> +<li>locator - an <a href="#locators">element locator</a> +</li> +<li>variableName - + the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + </li> +</ul> +<p> +<dl> +<dt>Returns: </dt> +<dd>true if the specified element is visible, false otherwise</dd> +</dl> +</p> +<p>Related Assertions, automatically generated:</p> +<ul> +<li>assertVisible + ( + locator + ) + </li> +<li>assertNotVisible + ( + locator + ) + </li> +<li>verifyVisible + ( + locator + ) + </li> +<li>verifyNotVisible + ( + locator + ) + </li> +<li>waitForVisible + ( + locator + ) + </li> +<li>waitForNotVisible + ( + locator + ) + </li> +</ul> +</dd> +<br> +</dl> +<h2> +<a name="parameter-construction-and-variables">Parameter construction and Variables</a> +</h2> +<blockquote> +<p>All Selenium command parameters can be constructed using both simple + variable substitution as well as full javascript. Both of these + mechanisms can access previously stored variables, but do so using + different syntax.</p> +<p> +<a name="storedVars"></a><strong>Stored Variables</strong> +</p> +<p>The commands <em>store</em>, <em>storeValue</em> and <em>storeText</em> can be used to store a variable + value for later access. Internally, these variables are stored in a map called "storedVars", + with values keyed by the variable name. These commands are documented in the command reference.</p> +<p> +<strong>Variable substitution</strong> +</p> +<p>Variable substitution provides a simple way to include a previously stored variable in a + command parameter. This is a simple mechanism, by which the variable to substitute is indicated + by ${variableName}. Multiple variables can be substituted, and intermixed with static text.</p> +<p>Example:</p> +<blockquote> +<table border="1" class="table"> +<colgroup> +<col width="18%"> +<col width="36%"> +<col width="45%"> +</colgroup> +<tbody valign="top"> +<tr> +<td>store</td><td>Mr</td><td>title</td> +</tr> +<tr> +<td>storeValue</td><td>nameField</td><td>surname</td> +</tr> +<tr> +<td>store</td><td>${title} ${surname}</td><td>fullname</td> +</tr> +<tr> +<td>type</td><td>textElement</td><td>Full name is: ${fullname}</td> +</tr> +</tbody> +</table> +</blockquote> +<p> +<strong>Javascript evaluation</strong> +</p> +<p>Javascript evaluation provides the full power of javascript in constructing a command parameter. + To use this mechanism, the <em>entire</em> parameter value must be prefixed by + 'javascript{' with a trailing '}'. The text inside the braces is evaluated as a javascript expression, + and can access previously stored variables using the <em>storedVars</em> map detailed above. + Note that variable substitution cannot be combined with javascript evaluation.</p> +<p>Example:</p> +<blockquote> +<table border="1" class="table"> +<colgroup> +<col width="9%"> +<col width="44%"> +<col width="46%"> +</colgroup> +<tbody valign="top"> +<tr> +<td>store</td><td>javascript{'merchant' + (new Date()).getTime()}</td><td>merchantId</td> +</tr> +<tr> +<td>type</td><td>textElement</td><td>javascript{storedVars['merchantId'].toUpperCase()}</td> +</tr> +</tbody> +</table> +</blockquote> +</blockquote> +<div class="section" id="extending-selenium"> +<h2> +<a name="extending-selenium">Extending Selenium</a> +</h2> +<blockquote> +<p>It can be quite simple to extend Selenium, adding your own actions, assertions and locator-strategies. + This is done with javascript by adding methods to the Selenium object prototype, and the PageBot + object prototype. On startup, Selenium will automatically look through methods on these prototypes, + using name patterns to recognise which ones are actions, assertions and locators.</p> +<p>The following examples try to give an indication of how Selenium can be extended with javascript.</p> +</blockquote> +<p> +<strong>Actions</strong> +</p> +<blockquote> +<p>All <em>doFoo</em> methods on the Selenium prototype are added as actions. For each action <em>foo</em> there + is also an action <em>fooAndWait</em> registered. An action method can take up to 2 parameters, which + will be passed the second and third column values in the test.</p> +<p>Example: Add a "typeRepeated" action to Selenium, which types the text twice into a text box.</p> +<pre class="literal-block"> + Selenium.prototype.doTypeRepeated = function(locator, text) { + // All locator-strategies are automatically handled by "findElement" + var element = this.page().findElement(locator); + + // Create the text to type + var valueToType = text + text; + + // Replace the element text with the new text + this.page().replaceText(element, valueToType); + }; + </pre> +</blockquote> +<p> +<strong>Accessors/Assertions</strong> +</p> +<blockquote> +<p>All <em>getFoo</em> and <em>isFoo</em> methods on the Selenium prototype are added as accessors (storeFoo). For each accessor there + is an <em>assertFoo</em>, <em>verifyFoo</em> and <em>waitForFoo</em> registered. An assert method can take up to 2 parameters, which + will be passed the second and third column values in the test. You can also define your own assertions literally + as simple "assert" methods, which will also auto-generate "verify" and "waitFor" commands.</p> +<p>Example: Add a <em>valueRepeated</em> assertion, that makes sure that the element value + consists of the supplied text repeated. The 2 commands that would be available in tests would be + <em>assertValueRepeated</em> and <em>verifyValueRepeated</em>.</p> +<pre class="literal-block"> + Selenium.prototype.assertValueRepeated = function(locator, text) { + // All locator-strategies are automatically handled by "findElement" + var element = this.page().findElement(locator); + + // Create the text to verify + var expectedValue = text + text; + + // Get the actual element value + var actualValue = element.value; + + // Make sure the actual value matches the expected + Assert.matches(expectedValue, actualValue); + }; + </pre> +</blockquote> +<p> +<strong>Automatic availability of storeFoo, assertFoo, assertNotFoo, waitForFoo and waitForNotFoo for every getFoo</strong> +</p> +<blockquote> +<p>All <em>getFoo</em> and <em>isFoo</em> methods on the Selenium prototype automatically result in the availability + of storeFoo, assertFoo, assertNotFoo, verifyFoo, verifyNotFoo, waitForFoo, and waitForNotFoo commands.</p> +<p>Example, if you add a getTextLength() method, the following commands will automatically be available: + storeTextLength, assertTextLength, assertNotTextLength, verifyTextLength, verifyNotTextLength, waitForTextLength, and waitForNotTextLength commands.</p> +<pre class="literal-block"> + Selenium.prototype.getTextLength = function(locator, text) { + return this.getText(locator).length; + }; + </pre> +<p>Also note that the <em>assertValueRepeated</em> method described above could have been implemented using + isValueRepeated, with the added benefit of also automatically getting assertNotValueRepeated, storeValueRepeated, + waitForValueRepeated and waitForNotValueRepeated.</p> +</blockquote> +<p> +<strong>Locator Strategies</strong> +</p> +<blockquote> +<p>All <em>locateElementByFoo</em> methods on the PageBot prototype are added as locator-strategies. A locator strategy takes 2 parameters, the first being the locator string (minus the prefix), and the second being the document in which to search.</p> +<p>Example: Add a "valuerepeated=" locator, that finds the first element a value attribute equal to the the supplied value repeated.</p> +<pre class="literal-block"> + // The "inDocument" is a the document you are searching. + PageBot.prototype.locateElementByValueRepeated = function(text, inDocument) { + // Create the text to search for + var expectedValue = text + text; + + // Loop through all elements, looking for ones that have + // a value === our expected value + var allElements = inDocument.getElementsByTagName("*"); + for (var i = 0; i < allElements.length; i++) { + var testElement = allElements[i]; + if (testElement.value && testElement.value === expectedValue) { + return testElement; + } + } + return null; + }; + </pre> +</blockquote> +<p> +<strong>user-extensions.js</strong> +</p> +<blockquote> +<p>By default, Selenium looks for a file called "user-extensions.js", and loads the javascript code found in that file. This file provides a convenient location for adding features to Selenium, without needing to modify the core Selenium sources.</p> +<p>In the standard distibution, this file does not exist. Users can create this file and place their extension code in this common location, removing the need to modify the Selenium sources, and hopefully assisting with the upgrade process.</p> +</blockquote> +</div> +</body> +</html> |