diff options
Diffstat (limited to 'tests/test_tools/selenium/core')
31 files changed, 12297 insertions, 1846 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; +} |