diff options
author | Jehan-Guillaume (ioguix) de Rorthais | 2010-03-12 21:40:19 +0000 |
---|---|---|
committer | Jehan-Guillaume (ioguix) de Rorthais | 2010-03-12 21:40:19 +0000 |
commit | 885e340719ab1a8a71643ee0220c9fb7e8f2dc03 (patch) | |
tree | 9ba9ddc902c8f21e87cb4340f9e78156a8d1a2ee | |
parent | 560814be75fcd86d6b2843cf5bd8988841dc4367 (diff) |
update selenium lib to current 1.0.3
41 files changed, 13653 insertions, 6477 deletions
diff --git a/tests/selenium/selenium-lib/VERSION.txt b/tests/selenium/selenium-lib/VERSION.txt index c5f33ea7..059ee6dc 100644 --- a/tests/selenium/selenium-lib/VERSION.txt +++ b/tests/selenium/selenium-lib/VERSION.txt @@ -1,2 +1,2 @@ -selenium.core.version=0.8.3 -selenium.core.revision=1879
\ No newline at end of file +selenium.core.version=@VERSION@ +selenium.core.revision=@REVISION@
\ No newline at end of file diff --git a/tests/selenium/selenium-lib/coding-conventions.txt b/tests/selenium/selenium-lib/coding-conventions.txt deleted file mode 100644 index 8107d67b..00000000 --- a/tests/selenium/selenium-lib/coding-conventions.txt +++ /dev/null @@ -1,54 +0,0 @@ - Coding standards for Selenium Core Javascript code
- --------------------------------------------------
-
- Here is a set of conventions agreed by the active Selenium Core
- developers at ThoughtWorks. Please stick to these guidelines when
- working on the Selenium Core code-base.
-
-Whitespace: we use spaces, NOT TABS. Indent in 4-space increments.
-
-Braces: we place open-braces on the same line as the associated keyword,
- for example:
-
- if (command.isBreakpoint) {
- this.pause();
- } else {
- window.setTimeout(this.resume.bind(this), delay);
- }
-
-Encapsulation: we prefer to encapsulate functions and variables inside
- objects, where possible.
-
-Variable declarations: declare variables (using "var") ... even if they're
- "global".
-
-Class definitions: we're shifting to "prototype.js" style for
- definition of classes, e.g.
-
- var MyClass = Class.create();
- Object.extend(MyClass.prototype, {
-
- initialize: function() {
- // ... constructor code ...
- },
-
- doStuff: function() {
- // ... method body ...
- }
-
- });
-
-'Private' functions/properties: we simulate "private" properties by
- prepended the name with an underscore ("_"), e.g.
-
- _resumeAfterDelay : function() {
- // ...etc...
- },
-
-Element addressing: use "$(id)" rather than
- "document.getElementById('id')".
-
-Timeout functions: pass function objects to setTimeout(), rather than
- strings, e.g.
-
- window.setTimeout(this.resume.bind(this), delay);
diff --git a/tests/selenium/selenium-lib/core/RemoteRunner.html b/tests/selenium/selenium-lib/core/RemoteRunner.html index 1305c673..5b07e5da 100644 --- a/tests/selenium/selenium-lib/core/RemoteRunner.html +++ b/tests/selenium/selenium-lib/core/RemoteRunner.html @@ -24,7 +24,9 @@ http-equiv="content-type"> <script type="text/javascript" src="scripts/xmlextras.js"></script>
<script language="JavaScript" type="text/javascript" src="lib/prototype.js"></script>
<script language="JavaScript" type="text/javascript" src="lib/cssQuery/cssQuery-p.js"></script>
+<script language="JavaScript" type="text/javascript" src="lib/snapsie.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/htmlutils.js"></script>
+<script language="JavaScript" type="text/javascript" src="scripts/ui-element.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/selenium-browserdetect.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/selenium-browserbot.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/find_matching_child.js"></script>
@@ -34,18 +36,13 @@ http-equiv="content-type"> <script language="JavaScript" type="text/javascript" src="scripts/selenium-remoterunner.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/selenium-logging.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/selenium-version.js"></script>
-<script language="JavaScript" type="text/javascript" src="xpath/misc.js"></script>
+<script language="JavaScript" type="text/javascript" src="xpath/util.js"></script>
+<script language="JavaScript" type="text/javascript" src="xpath/xmltoken.js"></script>
<script language="JavaScript" type="text/javascript" src="xpath/dom.js"></script>
<script language="JavaScript" type="text/javascript" src="xpath/xpath.js"></script>
+<script language="JavaScript" type="text/javascript" src="xpath/javascript-xpath-0.1.11.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/user-extensions.js"></script>
<script language="JavaScript" type="text/javascript">
- function openDomViewer() {
- var autFrame = document.getElementById('selenium_myiframe');
- var autFrameDocument = getIframeDocument(autFrame);
- var domViewer = window.open(getDocumentBase(document) + 'domviewer/domviewer.html');
- domViewer.rootDocument = autFrameDocument;
- return false;
- }
function cleanUp() {
if (LOG != null) {
@@ -58,30 +55,22 @@ http-equiv="content-type"> <body onLoad="setTimeout(function(){runSeleniumTest();},1000)" onUnload="cleanUp()">
-<table border="1" style="height: 100%;">
+<table border="1" style="height: 100%; width: 100%;">
<tr>
- <td width="50%" height="30%">
+ <td width="50%">
<table>
<tr>
- <td>
- <img src="selenium-logo.png">
- </td>
- <td>
- <h1><a href="http://selenium.thoughtworks.com" >Selenium</a> Functional Testing for Web Apps</h1>
- Open Source From <a href="http://www.thoughtworks.com">ThoughtWorks, Inc</a> and Friends
+ <td class="remoterunner">
+ <h4><a href="http://selenium.openqa.org">Selenium</a> Functional Testing for Web Apps</h4>
+ Open Source From <a href="http://selenium.openqa.org/thoughtworks-and-friends.html">ThoughtWorks and Friends</a>
<form action="">
- <br/>Slow Mode:<INPUT TYPE="CHECKBOX" NAME="FASTMODE" VALUE="YES" onmouseup="slowClicked()">
-
+ <br/>
<iframe id="seleniumLoggingFrame" name="seleniumLoggingFrame" src="Blank.html" style="border: 0; height: 0; width: 0; "></iframe>
<fieldset>
- <legend>Tools</legend>
-
- <button type="button" id="domViewer1" onclick="openDomViewer();">
- View DOM
- </button>
<button type="button" onclick="LOG.show();">
Show Log
</button>
+ <label><INPUT TYPE="CHECKBOX" NAME="FASTMODE" VALUE="YES" onmouseup="slowClicked()"> Slow Mode</label>
</fieldset>
</form>
@@ -93,13 +82,15 @@ http-equiv="content-type"> <label id="context" name="context"></label>
</form>
</td>
- <td width="50%" height="30%">
- <b>Last Four Test Commands:</b><br/>
- <div id="commandList"></div>
+ <td width="50%" class="remoterunner">
+ <h4>Command History:</h4>
+ <form name="commands">
+ <textarea style="overflow:auto; height:8em; width:100%" wrap="off" id="commandList"></textarea>
+ </form>
</td>
</tr>
<tr>
- <td colspan="2" height="70%">
+ <td colspan="2" height="100%">
<iframe name="selenium_myiframe" id="selenium_myiframe" src="Blank.html" height="100%" width="100%"></iframe>
</td>
</tr>
diff --git a/tests/selenium/selenium-lib/core/TestPrompt.html b/tests/selenium/selenium-lib/core/TestPrompt.html index 1e95a64e..db202930 100644 --- a/tests/selenium/selenium-lib/core/TestPrompt.html +++ b/tests/selenium/selenium-lib/core/TestPrompt.html @@ -67,7 +67,7 @@ Copyright 2004 ThoughtWorks, Inc }
function go() {
- if (!browserVersion.isHTA) return true;
+ if (!browserVersion.isHTA && !browserVersion.isChrome) return true;
var inputs = document.getElementsByTagName("input");
var queryString = "";
for (var i = 0; i < inputs.length; i++) {
@@ -96,7 +96,7 @@ Copyright 2004 ThoughtWorks, Inc <p>
Test Suite:
- <input id="test" name="test" size="30" value="../../TestSuite.html"/>
+ <input id="test" name="test" size="30" value="../tests/TestSuite.html"/>
</p>
<p align="center"><input type="submit" value="Go"/></p>
@@ -142,4 +142,4 @@ Copyright 2004 ThoughtWorks, Inc </form>
</body>
-</html>
+</html>
\ No newline at end of file diff --git a/tests/selenium/selenium-lib/core/TestRunner.hta b/tests/selenium/selenium-lib/core/TestRunner.hta index f66958f8..a615be5d 100644 --- a/tests/selenium/selenium-lib/core/TestRunner.hta +++ b/tests/selenium/selenium-lib/core/TestRunner.hta @@ -26,14 +26,13 @@ to work-around a bug in IE on Win2K whereby the HTA application doesn't function <title>Selenium Functional Test Runner</title>
<link rel="stylesheet" type="text/css" href="selenium.css"/>
- <script type="text/javascript" src="scripts/narcissus-defs.js"></script>
- <script type="text/javascript" src="scripts/narcissus-parse.js"></script>
- <script type="text/javascript" src="scripts/narcissus-exec.js"></script>
<script type="text/javascript" src="scripts/xmlextras.js"></script>
<script language="JavaScript" type="text/javascript" src="lib/prototype.js"></script>
+ <script language="JavaScript" type="text/javascript" src="lib/cssQuery/cssQuery-p.js"></script>
+ <script language="JavaScript" type="text/javascript" src="lib/snapsie.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/htmlutils.js"></script>
+ <script language="JavaScript" type="text/javascript" src="scripts/ui-element.js"></script>
<script language="JavaScript" type="text/javascript" src="lib/scriptaculous/scriptaculous.js"></script>
- <script language="JavaScript" type="text/javascript" src="lib/cssQuery/cssQuery-p.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/selenium-browserdetect.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/selenium-browserbot.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/find_matching_child.js"></script>
@@ -43,9 +42,11 @@ to work-around a bug in IE on Win2K whereby the HTA application doesn't function <script language="JavaScript" type="text/javascript" src="scripts/selenium-testrunner.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/selenium-logging.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/selenium-version.js"></script>
- <script language="JavaScript" type="text/javascript" src="xpath/misc.js"></script>
+ <script language="JavaScript" type="text/javascript" src="xpath/util.js"></script>
+ <script language="JavaScript" type="text/javascript" src="xpath/xmltoken.js"></script>
<script language="JavaScript" type="text/javascript" src="xpath/dom.js"></script>
<script language="JavaScript" type="text/javascript" src="xpath/xpath.js"></script>
+ <script language="JavaScript" type="text/javascript" src="xpath/javascript-xpath-0.1.11.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/user-extensions.js"></script>
<script language="JavaScript" type="text/javascript">
function openDomViewer() {
@@ -69,14 +70,14 @@ to work-around a bug in IE on Win2K whereby the HTA application doesn't function <iframe name="testSuiteFrame" id="testSuiteFrame" src="./TestPrompt.html" application="yes"></iframe>
</td>
<td width="50%" height="30%">
- <iframe name="testFrame" id="testFrame" application="yes"></iframe>
+ <iframe name="testFrame" id="testFrame" application="yes" src="Blank.html"></iframe>
</td>
<td width="25%">
<table class="layout">
<tr class="selenium">
<th width="25%" height="1" class="header">
- <h1><a href="http://selenium.thoughtworks.com" title="The Selenium Project">Selenium</a> TestRunner
+ <h1><a href="http://selenium.openqa.org" title="The Selenium Project">Selenium</a> TestRunner
</h1>
</th>
</tr>
@@ -89,9 +90,6 @@ to work-around a bug in IE on Win2K whereby the HTA application doesn't function <button type="button" id="runSuite" onClick="htmlTestRunner.startTestSuite();"
title="Run All tests" accesskey="a">
</button>
- <button type="button" id="runRemainTests" onClick="htmlTestRunner.runRemainTests();"
- title="Run the tests from the selected" accesskey="f">
- </button>
<button type="button" id="runSeleniumTest" onClick="htmlTestRunner.runSingleTest();"
title="Run the Selected test" accesskey="r">
</button>
diff --git a/tests/selenium/selenium-lib/core/TestRunner.html b/tests/selenium/selenium-lib/core/TestRunner.html index 87a52245..a615be5d 100644 --- a/tests/selenium/selenium-lib/core/TestRunner.html +++ b/tests/selenium/selenium-lib/core/TestRunner.html @@ -26,14 +26,13 @@ to work-around a bug in IE on Win2K whereby the HTA application doesn't function <title>Selenium Functional Test Runner</title>
<link rel="stylesheet" type="text/css" href="selenium.css"/>
- <script type="text/javascript" src="scripts/narcissus-defs.js"></script>
- <script type="text/javascript" src="scripts/narcissus-parse.js"></script>
- <script type="text/javascript" src="scripts/narcissus-exec.js"></script>
<script type="text/javascript" src="scripts/xmlextras.js"></script>
<script language="JavaScript" type="text/javascript" src="lib/prototype.js"></script>
+ <script language="JavaScript" type="text/javascript" src="lib/cssQuery/cssQuery-p.js"></script>
+ <script language="JavaScript" type="text/javascript" src="lib/snapsie.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/htmlutils.js"></script>
+ <script language="JavaScript" type="text/javascript" src="scripts/ui-element.js"></script>
<script language="JavaScript" type="text/javascript" src="lib/scriptaculous/scriptaculous.js"></script>
- <script language="JavaScript" type="text/javascript" src="lib/cssQuery/cssQuery-p.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/selenium-browserdetect.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/selenium-browserbot.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/find_matching_child.js"></script>
@@ -43,9 +42,11 @@ to work-around a bug in IE on Win2K whereby the HTA application doesn't function <script language="JavaScript" type="text/javascript" src="scripts/selenium-testrunner.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/selenium-logging.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/selenium-version.js"></script>
- <script language="JavaScript" type="text/javascript" src="xpath/misc.js"></script>
+ <script language="JavaScript" type="text/javascript" src="xpath/util.js"></script>
+ <script language="JavaScript" type="text/javascript" src="xpath/xmltoken.js"></script>
<script language="JavaScript" type="text/javascript" src="xpath/dom.js"></script>
<script language="JavaScript" type="text/javascript" src="xpath/xpath.js"></script>
+ <script language="JavaScript" type="text/javascript" src="xpath/javascript-xpath-0.1.11.js"></script>
<script language="JavaScript" type="text/javascript" src="scripts/user-extensions.js"></script>
<script language="JavaScript" type="text/javascript">
function openDomViewer() {
@@ -69,14 +70,14 @@ to work-around a bug in IE on Win2K whereby the HTA application doesn't function <iframe name="testSuiteFrame" id="testSuiteFrame" src="./TestPrompt.html" application="yes"></iframe>
</td>
<td width="50%" height="30%">
- <iframe name="testFrame" id="testFrame" application="yes"></iframe>
+ <iframe name="testFrame" id="testFrame" application="yes" src="Blank.html"></iframe>
</td>
<td width="25%">
<table class="layout">
<tr class="selenium">
<th width="25%" height="1" class="header">
- <h1><a href="http://selenium.thoughtworks.com" title="The Selenium Project">Selenium</a> TestRunner
+ <h1><a href="http://selenium.openqa.org" title="The Selenium Project">Selenium</a> TestRunner
</h1>
</th>
</tr>
@@ -89,9 +90,6 @@ to work-around a bug in IE on Win2K whereby the HTA application doesn't function <button type="button" id="runSuite" onClick="htmlTestRunner.startTestSuite();"
title="Run All tests" accesskey="a">
</button>
- <button type="button" id="runRemainTests" onClick="htmlTestRunner.runRemainTests();"
- title="Run the tests from the selected" accesskey="f">
- </button>
<button type="button" id="runSeleniumTest" onClick="htmlTestRunner.runSingleTest();"
title="Run the Selected test" accesskey="r">
</button>
diff --git a/tests/selenium/selenium-lib/core/domviewer/domviewer.css b/tests/selenium/selenium-lib/core/domviewer/domviewer.css deleted file mode 100644 index b64b2435..00000000 --- a/tests/selenium/selenium-lib/core/domviewer/domviewer.css +++ /dev/null @@ -1,298 +0,0 @@ -/******************************************************************************
-* Defines default styles for site pages. *
-******************************************************************************/
-.hidden {
- display: none;
-}
-
-img{
- display: inline;
- border: none;
-}
-
-.box{
- background: #fcfcfc;
- border: 1px solid #000;
- border-color: blue;
- color: #000000;
- margin: 10px auto;
- padding: 3px;
- vertical-align: bottom;
-}
-a {
- text-decoration: none;
-}
-
-body {
- background-color: #ffffff;
- color: #000000;
- font-family: Arial, Helvetica, sans-serif;
- font-size: 10pt;
-}
-
-h2 {
- font-size: 140%;
-}
-
-h3 {
- font-size: 120%;
-}
-
-h4 {
- font-size: 100%;
-}
-
-pre {
- font-family: Courier New, Courier, monospace;
- font-size: 80%;
-}
-
-td, th {
- font-family: Arial, Helvetica, sans-serif;
- font-size: 10pt;
- text-align: left;
- vertical-align: top;
-}
-
-th {
- font-weight: bold;
- vertical-align: bottom;
-}
-
-ul {
- list-style-type: square;
-}
-
-#demoBox {
- border-color: #000000;
- border-style: solid;
- border-width: 1px;
- padding: 8px;
- width: 24em;
-}
-
-.footer {
- margin-bottom: 0px;
- text-align: center;
-}
-
-/* Boxed table styles */
-
-table.boxed {
- border-spacing: 2px;
- empty-cells: hide;
-}
-
-td.boxed, th.boxed, th.boxedHeader {
- background-color: #ffffff;
- border-color: #000000;
- border-style: solid;
- border-width: 1px;
- color: #000000;
- padding: 2px;
- padding-left: 8px;
- padding-right: 8px;
-}
-
-th.boxed {
- background-color: #c0c0c0;
-}
-
-th.boxedHeader {
- background-color: #808080;
- color: #ffffff;
-}
-
-a.object {
- color: #0000ff;
-}
-
-li {
- white-space: nowrap;
-}
-
-ul {
- list-style-type: square;
- margin-left: 0px;
- padding-left: 1em;
-}
-
-.boxlevel1{
- background: #FFD700;
-}
-
-.boxlevel2{
- background: #D2691E;
-}
-
-.boxlevel3{
- background: #DCDCDC;
-}
-
-.boxlevel4{
- background: #F5F5F5;
-}
-
-.boxlevel5{
- background: #BEBEBE;
-}
-
-.boxlevel6{
- background: #D3D3D3;
-}
-
-.boxlevel7{
- background: #A9A9A9;
-}
-
-.boxlevel8{
- background: #191970;
-}
-
-.boxlevel9{
- background: #000080;
-}
-
-.boxlevel10{
- background: #6495ED;
-}
-
-.boxlevel11{
- background: #483D8B;
-}
-
-.boxlevel12{
- background: #6A5ACD;
-}
-
-.boxlevel13{
- background: #7B68EE;
-}
-
-.boxlevel14{
- background: #8470FF;
-}
-
-.boxlevel15{
- background: #0000CD;
-}
-
-.boxlevel16{
- background: #4169E1;
-}
-
-.boxlevel17{
- background: #0000FF;
-}
-
-.boxlevel18{
- background: #1E90FF;
-}
-
-.boxlevel19{
- background: #00BFFF;
-}
-
-.boxlevel20{
- background: #87CEEB;
-}
-
-.boxlevel21{
- background: #B0C4DE;
-}
-
-.boxlevel22{
- background: #ADD8E6;
-}
-
-.boxlevel23{
- background: #00CED1;
-}
-
-.boxlevel24{
- background: #48D1CC;
-}
-
-.boxlevel25{
- background: #40E0D0;
-}
-
-.boxlevel26{
- background: #008B8B;
-}
-
-.boxlevel27{
- background: #00FFFF;
-}
-
-.boxlevel28{
- background: #E0FFFF;
-}
-
-.boxlevel29{
- background: #5F9EA0;
-}
-
-.boxlevel30{
- background: #66CDAA;
-}
-
-.boxlevel31{
- background: #7FFFD4;
-}
-
-.boxlevel32{
- background: #006400;
-}
-
-.boxlevel33{
- background: #556B2F;
-}
-
-.boxlevel34{
- background: #8FBC8F;
-}
-
-.boxlevel35{
- background: #2E8B57;
-}
-
-.boxlevel36{
- background: #3CB371;
-}
-
-.boxlevel37{
- background: #20B2AA;
-}
-
-.boxlevel38{
- background: #00FF7F;
-}
-
-.boxlevel39{
- background: #7CFC00;
-}
-
-.boxlevel40{
- background: #90EE90;
-}
-
-.boxlevel41{
- background: #00FF00;
-}
-
-.boxlevel41{
- background: #7FFF00;
-}
-
-.boxlevel42{
- background: #00FA9A;
-}
-
-.boxlevel43{
- background: #ADFF2F;
-}
-
-.boxlevel44{
- background: #32CD32;
-}
\ No newline at end of file diff --git a/tests/selenium/selenium-lib/core/domviewer/domviewer.html b/tests/selenium/selenium-lib/core/domviewer/domviewer.html deleted file mode 100644 index 7f0cebdb..00000000 --- a/tests/selenium/selenium-lib/core/domviewer/domviewer.html +++ /dev/null @@ -1,16 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
- <head>
- <title>DOM Viewer</title>
- <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
- <link rel="stylesheet" type="text/css" href="domviewer.css"/>
- <script type="text/javascript" src="selenium-domviewer.js"></script>
- </head>
- <body onload="loadDomViewer();">
- <h3>DOM Viewer</h3>
- <p> This page is generated using JavaScript. If you see this text, your
- browser doesn't support JavaScript.</p>
- </body>
-
-</html>
diff --git a/tests/selenium/selenium-lib/core/domviewer/selenium-domviewer.js b/tests/selenium/selenium-lib/core/domviewer/selenium-domviewer.js deleted file mode 100644 index 941aab16..00000000 --- a/tests/selenium/selenium-lib/core/domviewer/selenium-domviewer.js +++ /dev/null @@ -1,205 +0,0 @@ -var HIDDEN="hidden"; -var LEVEL = "level"; -var PLUS_SRC="butplus.gif"; -var MIN_SRC="butmin.gif"; -var newRoot; -var maxColumns=1; - -function loadDomViewer() { - // See if the rootDocument variable has been set on this window. - var rootDocument = window.rootDocument; - - // If not look to the opener for an explicity rootDocument variable, otherwise, use the opener document - if (!rootDocument && window.opener) { - rootDocument = window.opener.rootDocument || window.opener.document; - } - - if (rootDocument) { - document.body.innerHTML = displayDOM(rootDocument); - } - else { - document.body.innerHTML = "<b>Must specify rootDocument for window. This can be done by setting the rootDocument variable on this window, or on the opener window for a popup window.</b>"; - } -} - - -function displayDOM(root){ - var str = ""; - str+="<table>"; - str += treeTraversal(root,0); - // to make table columns work well. - str += "<tr>"; - for (var i=0; i < maxColumns; i++) { - str+= "<td> </td>"; - } - str += "</tr>"; - str += "</table>"; - return str; -} - -function checkForChildren(element){ - if(!element.hasChildNodes()) - return false; - - var nodes = element.childNodes; - var size = nodes.length; - var count=0; - - for(var i=0; i< size; i++){ - var node = nodes.item(i); - //if(node.toString()=="[object Text]"){ - //this is equalent to the above - //but will work with more browsers - if(node.nodeType!=1){ - count++; - } - } - - if(count == size) - return false; - else - return true; -} - -function treeTraversal(root, level){ - var str = ""; - var nodes= null; - var size = null; - //it is supposed to show the last node, - //but the last node is always nodeText type - //and we don't show it - if(!root.hasChildNodes()) - return "";//displayNode(root,level,false); - - nodes = root.childNodes; - size = nodes.length; - - for(var i=0; i< size; i++){ - var element = nodes.item(i); - //if the node is textNode, don't display - if(element.nodeType==1){ - str+= displayNode(element,level,checkForChildren(element)); - str+=treeTraversal(element, level+1); - } - } - return str; -} - -function displayNode(element, level, isLink){ - nodeContent = getNodeContent(element); - columns = Math.round((nodeContent.length / 12) + 0.5); - if (columns + level > maxColumns) { - maxColumns = columns + level; - } - var str ="<tr class='"+LEVEL+level+"'>"; - for (var i=0; i < level; i++) - str+= "<td> </td>"; - str+="<td colspan='"+ columns +"' class='box"+" boxlevel"+level+"' >"; - if(isLink){ - str+='<a onclick="hide(this);return false;" href="javascript:void();">'; - str+='<img src="'+MIN_SRC+'" />'; - } - str += nodeContent; - if(isLink) - str+="</a></td></tr>"; - return str; -} - -function getNodeContent(element) { - - str = ""; - id =""; - if (element.id != null && element.id != "") { - id = " ID(" + element.id +")"; - } - name =""; - if (element.name != null && element.name != "") { - name = " NAME(" + element.name + ")"; - } - value =""; - if (element.value != null && element.value != "") { - value = " VALUE(" + element.value + ")"; - } - href =""; - if (element.href != null && element.href != "") { - href = " HREF(" + element.href + ")"; - } - clazz = ""; - if (element.className != null && element.className != "") { - clazz = " CLASS(" + element.className + ")"; - } - src = ""; - if (element.src != null && element.src != "") { - src = " SRC(" + element.src + ")"; - } - alt = ""; - if (element.alt != null && element.alt != "") { - alt = " ALT(" + element.alt + ")"; - } - type = ""; - if (element.type != null && element.type != "") { - type = " TYPE(" + element.type + ")"; - } - text =""; - if (element.text != null && element.text != "" && element.text != "undefined") { - text = " #TEXT(" + trim(element.text) +")"; - } - str+=" <b>"+ element.nodeName + id + alt + type + clazz + name + value + href + src + text + "</b>"; - return str; - -} - -function trim(val) { - val2 = val.substring(0,40) + " "; - var spaceChr = String.fromCharCode(32); - var length = val2.length; - var retVal = ""; - var ix = length -1; - - while(ix > -1){ - if(val2.charAt(ix) == spaceChr) { - } else { - retVal = val2.substring(0, ix +1); - break; - } - ix = ix-1; - } - if (val.length > 40) { - retVal += "..."; - } - return retVal; -} - -function hide(hlink){ - var isHidden = false; - var image = hlink.firstChild; - if(image.src.toString().indexOf(MIN_SRC)!=-1){ - image.src=PLUS_SRC; - isHidden=true; - }else{ - image.src=MIN_SRC; - } - var rowObj= hlink.parentNode.parentNode; - var rowLevel = parseInt(rowObj.className.substring(LEVEL.length)); - - var sibling = rowObj.nextSibling; - var siblingLevel = sibling.className.substring(LEVEL.length); - if(siblingLevel.indexOf(HIDDEN)!=-1){ - siblingLevel = siblingLevel.substring(0,siblingLevel.length - HIDDEN.length-1); - } - siblingLevel=parseInt(siblingLevel); - while(sibling!=null && rowLevel<siblingLevel){ - if(isHidden){ - sibling.className += " "+ HIDDEN; - }else if(!isHidden && sibling.className.indexOf(HIDDEN)!=-1){ - var str = sibling.className; - sibling.className=str.substring(0, str.length - HIDDEN.length-1); - } - sibling = sibling.nextSibling; - siblingLevel = parseInt(sibling.className.substring(LEVEL.length)); - } -} - -function LOG(message) { - window.opener.LOG.warn(message); -} diff --git a/tests/selenium/selenium-lib/core/iedoc-core.xml b/tests/selenium/selenium-lib/core/iedoc-core.xml index c11d11dc..72fad8b1 100644 --- a/tests/selenium/selenium-lib/core/iedoc-core.xml +++ b/tests/selenium/selenium-lib/core/iedoc-core.xml @@ -79,6 +79,16 @@ Select the element using css selectors. Please refer to <a href="http://www.w3.o </ul> <p>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). </p> </li> + +<li><strong>ui</strong>=<em>uiSpecifierString</em>: +Locate an element by resolving the UI specifier string to another locator, and evaluating it. See the <a href="http://svn.openqa.org/fisheye/browse/~raw,r=trunk/selenium/trunk/src/main/resources/core/scripts/ui-doc.html">Selenium UI-Element Reference</a> for more details. +<ul class="first last simple"> +<li>ui=loginPages::loginButton()</li> +<li>ui=settingsPages::toggle(label=Hide Email)</li> +<li>ui=forumPages::postBody(index=2)//a[2]</li> +</ul> +</li> + </ul> <p> @@ -123,6 +133,8 @@ string.</li> <li><strong>regexp:</strong><em>regexp</em>: Match a string using a regular-expression. The full power of JavaScript regular-expressions is available.</li> +<li><strong>regexpi:</strong><em>regexpi</em>: +Match a string using a case-insensitive regular-expression.</li> <li><strong>exact:</strong><em>string</em>: Match a string exactly, verbatim, without any of that fancy wildcard @@ -131,6 +143,14 @@ stuff.</li> <p> If no pattern prefix is specified, Selenium assumes that it's a "glob" pattern. +</p> +<p> +For commands that return multiple values (such as verifySelectOptions), +the string being matched is a comma-separated list of the return values, +where both commas and backslashes in the values are backslash-escaped. +When providing a pattern, the optional matching syntax (i.e. glob, +regexp, etc.) is specified once, as usual, at the beginning of the +pattern. </p></top> <function name="click"> @@ -153,6 +173,14 @@ waitForPageToLoad.</comment> </function> +<function name="contextMenu"> +
+<param name="locator">an element locator</param> +
+<comment>Simulates opening the context menu for the specified element (as might happen if the user "right-clicked" on the element).</comment> +
+</function> +
<function name="clickAt"> <param name="locator">an element locator</param> @@ -177,6 +205,16 @@ waitForPageToLoad.</comment> </function> +<function name="contextMenuAt"> +
+<param name="locator">an element locator</param> +
+<param name="coordString">specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator.</param> +
+<comment>Simulates opening the context menu for the specified element (as might happen if the user "right-clicked" on the element).</comment> +
+</function> +
<function name="fireEvent"> <param name="locator">an <a href="#locators">element locator</a></param> @@ -188,6 +226,14 @@ handler.</comment> </function> +<function name="focus"> +
+<param name="locator">an <a href="#locators">element locator</a></param> +
+<comment>Move the focus to the specified element; for example, if the element is an input field, move the cursor to that field.</comment> +
+</function> +
<function name="keyPress"> <param name="locator">an <a href="#locators">element locator</a></param> @@ -286,7 +332,16 @@ handler.</comment> <param name="locator">an <a href="#locators">element locator</a></param> -<comment>Simulates a user pressing the mouse button (without releasing it yet) on +<comment>Simulates a user pressing the left mouse button (without releasing it yet) on +the specified element.</comment> +
+</function> +
+<function name="mouseDownRight"> +
+<param name="locator">an <a href="#locators">element locator</a></param> +
+<comment>Simulates a user pressing the right mouse button (without releasing it yet) on the specified element.</comment> </function> @@ -297,7 +352,18 @@ the specified element.</comment> <param name="coordString">specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator.</param> -<comment>Simulates a user pressing the mouse button (without releasing it yet) at +<comment>Simulates a user pressing the left mouse button (without releasing it yet) at +the specified location.</comment> +
+</function> +
+<function name="mouseDownRightAt"> +
+<param name="locator">an <a href="#locators">element locator</a></param> +
+<param name="coordString">specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator.</param> +
+<comment>Simulates a user pressing the right mouse button (without releasing it yet) at the specified location.</comment> </function> @@ -311,6 +377,15 @@ holding the button down) on the specified element.</comment> </function> +<function name="mouseUpRight"> +
+<param name="locator">an <a href="#locators">element locator</a></param> +
+<comment>Simulates the event that occurs when the user releases the right mouse button (i.e., stops +holding the button down) on the specified element.</comment> +
+</function> +
<function name="mouseUpAt"> <param name="locator">an <a href="#locators">element locator</a></param> @@ -322,6 +397,17 @@ holding the button down) at the specified location.</comment> </function> +<function name="mouseUpRightAt"> +
+<param name="locator">an <a href="#locators">element locator</a></param> +
+<param name="coordString">specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator.</param> +
+<comment>Simulates the event that occurs when the user releases the right mouse button (i.e., stops +holding the button down) at the specified location.</comment> +
+</function> +
<function name="mouseMove"> <param name="locator">an <a href="#locators">element locator</a></param> @@ -386,6 +472,8 @@ the delay is 0 milliseconds.</comment> <function name="getSpeed"> +<return type="string">the execution speed in milliseconds.</return> +
<comment>Get execution speed (i.e., get the millisecond length of the delay following each selenium operation). By default, there is no such delay, i.e., the delay is 0 milliseconds. @@ -538,27 +626,43 @@ an empty (blank) url, like this: openWindow("", "myFunnyWindow").</p></comment> <param name="windowID">the JavaScript window ID of the window to select</param> -<comment>Selects a popup window; once a popup window has been selected, all +<comment>Selects a popup window using a window locator; once a popup window has been selected, all commands go to that window. To select the main window again, use null as the target. -<p>Note that there is a big difference between a window's internal JavaScript "name" property -and the "title" of a given window's document (which is normally what you actually see, as an end user, -in the title bar of the window). The "name" is normally invisible to the end-user; it's the second -parameter "windowName" passed to the JavaScript method window.open(url, windowName, windowFeatures, replaceFlag) -(which selenium intercepts).</p> - -<p>Selenium has several strategies for finding the window object referred to by the "windowID" parameter.</p> +<p> +Window locators provide different ways of specifying the window object: +by title, by internal JavaScript "name," or by JavaScript variable. +</p> +<ul> +<li><strong>title</strong>=<em>My Special Window</em>: +Finds the window using the text that appears in the title bar. Be careful; +two windows can share the same title. If that happens, this locator will +just pick one. +</li> +<li><strong>name</strong>=<em>myWindow</em>: +Finds the window using its internal JavaScript "name" property. This is the second +parameter "windowName" passed to the JavaScript method window.open(url, windowName, windowFeatures, replaceFlag) +(which Selenium intercepts). +</li> +<li><strong>var</strong>=<em>variableName</em>: +Some pop-up windows are unnamed (anonymous), but are associated with a JavaScript variable name in the current +application window, e.g. "window.foo = window.open(url);". In those cases, you can open the window using +"var=foo". +</li> +</ul> +<p> +If no window locator prefix is provided, we'll try to guess what you mean like this:</p> <p>1.) if windowID is null, (or the string "null") then it is assumed the user is referring to the original window instantiated by the browser).</p> <p>2.) if the value of the "windowID" parameter is a JavaScript variable name in the current application window, then it is assumed that this variable contains the return value from a call to the JavaScript window.open() method.</p> <p>3.) Otherwise, selenium looks in a hash it maintains that maps string names to window "names".</p> -<p>4.) If <i>that</i> fails, we'll try looping over all of the known windows to try to find the appropriate "title". +<p>4.) If <em>that</em> fails, we'll try looping over all of the known windows to try to find the appropriate "title". Since "title" is not necessarily unique, this may have unexpected behavior.</p> -<p>If you're having trouble figuring out what is the name of a window that you want to manipulate, look at the selenium log messages -which identify the names of windows created via window.open (and therefore intercepted by selenium). You will see messages +<p>If you're having trouble figuring out the name of a window that you want to manipulate, look at the Selenium log messages +which identify the names of windows created via window.open (and therefore intercepted by Selenium). You will see messages like the following for each window as it is opened:</p> <p><code>debug: window.open call intercepted; window ID (which you can use with selectWindow()) is "myNewWindow"</code></p> @@ -569,6 +673,36 @@ an empty (blank) url, like this: openWindow("", "myFunnyWindow").</p></comment> </function> +<function name="selectPopUp"> +
+<param name="windowID">an identifier for the popup window, which can take on a number of different meanings</param> +
+<comment>Simplifies the process of selecting a popup window (and does not offer +functionality beyond what <code>selectWindow()</code> already provides). +<ul> +<li>If <code>windowID</code> is either not specified, or specified as +"null", the first non-top window is selected. The top window is the one +that would be selected by <code>selectWindow()</code> without providing a +<code>windowID</code> . This should not be used when more than one popup +window is in play.</li> +<li>Otherwise, the window will be looked up considering +<code>windowID</code> as the following in order: 1) the "name" of the +window, as specified to <code>window.open()</code>; 2) a javascript +variable which is a reference to a window; and 3) the title of the +window. This is the same ordered lookup performed by +<code>selectWindow</code> .</li> +</ul></comment> +
+</function> +
+<function name="deselectPopUp"> +
+<comment>Selects the main window. Functionally equivalent to using +<code>selectWindow()</code> and specifying no value for +<code>windowID</code>.</comment> +
+</function> +
<function name="selectFrame"> <param name="locator">an <a href="#locators">element locator</a> identifying a frame or iframe</param> @@ -622,9 +756,9 @@ The selected window will return true, while all others will return false.</p></c <function name="waitForPopUp"> -<param name="windowID">the JavaScript window ID of the window that will appear</param> +<param name="windowID">the JavaScript window "name" of the window that will appear (not the text of the title bar) If unspecified, or specified as "null", this command will wait for the first non-top window to appear (don't rely on this if you are working with multiple popups simultaneously).</param> -<param name="timeout">a timeout in milliseconds, after which the action will return with an error</param> +<param name="timeout">a timeout in milliseconds, after which the action will return with an error. If this value is not specified, the default Selenium timeout will be used. See the setTimeout() command.</param> <comment>Waits for a popup window to appear and load up.</comment> @@ -632,26 +766,40 @@ The selected window will return true, while all others will return false.</p></c <function name="chooseCancelOnNextConfirmation"> -<comment>By default, Selenium's overridden window.confirm() function will +<comment><p> +By default, Selenium's overridden window.confirm() function will return true, as if the user had manually clicked OK; after running this command, the next call to confirm() will return false, as if the user had clicked Cancel. Selenium will then resume using the default behavior for future confirmations, automatically returning true (OK) unless/until you explicitly call this command for each -confirmation.</comment> +confirmation. +</p> +<p> +Take note - every time a confirmation comes up, you must +consume it with a corresponding getConfirmation, or else +the next selenium operation will fail. +</p></comment> </function> <function name="chooseOkOnNextConfirmation"> -<comment>Undo the effect of calling chooseCancelOnNextConfirmation. Note +<comment><p> +Undo the effect of calling chooseCancelOnNextConfirmation. Note that Selenium's overridden window.confirm() function will normally automatically return true, as if the user had manually clicked OK, so you shouldn't need to use this command unless for some reason you need to change your mind prior to the next confirmation. After any confirmation, Selenium will resume using the default behavior for future confirmations, automatically returning true (OK) unless/until you explicitly call chooseCancelOnNextConfirmation for each -confirmation.</comment> +confirmation. +</p> +<p> +Take note - every time a confirmation comes up, you must +consume it with a corresponding getConfirmation, or else +the next selenium operation will fail. +</p></comment> </function> @@ -726,13 +874,13 @@ This function never throws an exception <comment>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 +alert is generated but you do not consume it with getAlert, the next Selenium action will fail.</p> -<p>NOTE: under Selenium, JavaScript alerts will NOT pop up a visible alert +<p>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 +<p>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></comment> @@ -748,8 +896,11 @@ 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. +chooseCancelOnNextConfirmation command. +</p> +<p> +If an confirmation is generated but you do not consume it with getConfirmation, +the next Selenium action will fail. </p> <p> @@ -989,7 +1140,9 @@ tableLocator.row.column, where row and column start at 0.</comment> <param name="attributeLocator">an element locator followed by an @ sign and then the name of the attribute, e.g. "foo@bar"</param> -<comment>Gets the value of an element attribute.</comment> +<comment>Gets the value of an element attribute. The value of the attribute may +differ across browsers (this is the case for the "style" attribute, for +example).</comment> </function> @@ -1074,7 +1227,7 @@ This method will fail if the specified element isn't an input element.</comment> <param name="attributeName">name of an attribute on the windows</param> -<comment>Returns every instance of some attribute from all known windows.</comment> +<comment>Returns an array of JavaScript property values from all known windows having one.</comment> </function> @@ -1144,25 +1297,25 @@ just send one "mousemove" at the start location and then one final one at the en <function name="getAllWindowIds"> -<return type="string[]">the IDs of all windows that the browser knows about.</return> +<return type="string[]">Array of identifiers of all windows that the browser knows about.</return> -<comment>Returns the IDs of all windows that the browser knows about.</comment> +<comment>Returns the IDs of all windows that the browser knows about in an array.</comment> </function> <function name="getAllWindowNames"> -<return type="string[]">the names of all windows that the browser knows about.</return> +<return type="string[]">Array of names of all windows that the browser knows about.</return> -<comment>Returns the names of all windows that the browser knows about.</comment> +<comment>Returns the names of all windows that the browser knows about in an array.</comment> </function> <function name="getAllWindowTitles"> -<return type="string[]">the titles of all windows that the browser knows about.</return> +<return type="string[]">Array of titles of all windows that the browser knows about.</return> -<comment>Returns the titles of all windows that the browser knows about.</comment> +<comment>Returns the titles of all windows that the browser knows about in an array.</comment> </function> @@ -1313,6 +1466,22 @@ version is much slower than the native implementations.</comment> </function> +<function name="ignoreAttributesWithoutValue"> +
+<param name="ignore">boolean, true means we'll ignore attributes without value at the expense of xpath "correctness"; false means we'll sacrifice speed for correctness.</param> +
+<comment>Specifies whether Selenium will ignore xpath attributes that have no +value, i.e. are the empty string, when using the non-native xpath +evaluation engine. You'd want to do this for performance reasons in IE. +However, this could break certain xpaths, for example an xpath that looks +for an attribute whose value is NOT the empty string. + +The hope is that such xpaths are relatively rare, but the user should +have the option of using them. Note that this only influences xpath +evaluation when using the ajaxslt engine (i.e. not "javascript-xpath").</comment> +
+</function> +
<function name="waitForCondition"> <param name="script">the JavaScript snippet to run</param> @@ -1380,11 +1549,31 @@ See waitForPageToLoad for more information.</comment> </function> +<function name="getCookieByName"> +
+<return type="string">the value of the cookie</return> +
+<param name="name">the name of the cookie</param> +
+<comment>Returns the value of the cookie with the specified name, or throws an error if the cookie is not present.</comment> +
+</function> +
+<function name="isCookiePresent"> +
+<return type="boolean">true if a cookie with the specified name is present, or false otherwise.</return> +
+<param name="name">the name of the cookie</param> +
+<comment>Returns true if a cookie with the specified name is present, or false otherwise.</comment> +
+</function> +
<function name="createCookie"> <param name="nameValuePair">name and value of the cookie in a format "name=value"</param> -<param name="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.</param> +<param name="optionsString">options for the cookie. Currently supported options include 'path', 'max_age' and 'domain'. the optionsString's format is "path=/path/, max_age=60, domain=.foo.com". The order of options are irrelevant, the unit of the value of 'max_age' is second. Note that specifying a domain that isn't a subset of the current domain will usually fail.</param> <comment>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.</comment> @@ -1395,9 +1584,26 @@ under test, unless you specified a path for this cookie explicitly.</comment> <param name="name">the name of the cookie to be deleted</param> -<param name="path">the path property of the cookie to be deleted</param> +<param name="optionsString">options for the cookie. Currently supported options include 'path', 'domain' and 'recurse.' The optionsString's format is "path=/path/, domain=.foo.com, recurse=true". The order of options are irrelevant. Note that specifying a domain that isn't a subset of the current domain will usually fail.</param> -<comment>Delete a named cookie with specified path.</comment> +<comment>Delete a named cookie with specified path and domain. Be careful; to delete a cookie, you +need to delete it using the exact same path and domain that were used to create the cookie. +If the path is wrong, or the domain is wrong, the cookie simply won't be deleted. Also +note that specifying a domain that isn't a subset of the current domain will usually fail. + +Since there's no way to discover at runtime the original path and domain of a given cookie, +we've added an option called 'recurse' to try all sub-domains of the current domain with +all paths that are a subset of the current path. Beware; this option can be slow. In +big-O notation, it operates in O(n*m) time, where n is the number of dots in the domain +name and m is the number of slashes in the path.</comment> +
+</function> +
+<function name="deleteAllVisibleCookies"> +
+<comment>Calls deleteCookie with recurse=true on all cookies visible to the current page. +As noted on the documentation for deleteCookie, recurse=true can be much slower +than simply deleting the cookies using a known domain/path.</comment> </function> @@ -1449,6 +1655,74 @@ The function must return null if the element can't be found.</comment> </function> +<function name="captureEntirePageScreenshot"> +
+<param name="filename">the path to the file to persist the screenshot as. No filename extension will be appended by default. Directories will not be created if they do not exist, and an exception will be thrown, possibly by native code.</param> +
+<param name="kwargs">a kwargs string that modifies the way the screenshot is captured. Example: "background=#CCFFDD" . Currently valid options: <dl> <dt>background</dt> <dd>the background CSS for the HTML document. This may be useful to set for capturing screenshots of less-than-ideal layouts, for example where absolute positioning causes the calculation of the canvas dimension to fail and a black background is exposed (possibly obscuring black text).</dd> </dl></param> +
+<comment>Saves the entire contents of the current window canvas to a PNG file. +Contrast this with the captureScreenshot command, which captures the +contents of the OS viewport (i.e. whatever is currently being displayed +on the monitor), and is implemented in the RC only. Currently this only +works in Firefox when running in chrome mode, and in IE non-HTA using +the EXPERIMENTAL "Snapsie" utility. The Firefox implementation is mostly +borrowed from the Screengrab! Firefox extension. Please see +http://www.screengrab.org and http://snapsie.sourceforge.net/ for +details.</comment> +
+</function> +
+<function name="rollup"> +
+<param name="rollupName">the name of the rollup command</param> +
+<param name="kwargs">keyword arguments string that influences how the rollup expands into commands</param> +
+<comment>Executes a command rollup, which is a series of commands with a unique +name, and optionally arguments that control the generation of the set of +commands. If any one of the rolled-up commands fails, the rollup is +considered to have failed. Rollups may also contain nested rollups.</comment> +
+</function> +
+<function name="addScript"> +
+<param name="scriptContent">the Javascript content of the script to add</param> +
+<param name="scriptTagId">(optional) the id of the new script tag. If specified, and an element with this id already exists, this operation will fail.</param> +
+<comment>Loads script content into a new script tag in the Selenium document. This +differs from the runScript command in that runScript adds the script tag +to the document of the AUT, not the Selenium document. The following +entities in the script content are replaced by the characters they +represent: + + < + > + & + +The corresponding remove command is removeScript.</comment> +
+</function> +
+<function name="removeScript"> +
+<param name="scriptTagId">the id of the script element to remove.</param> +
+<comment>Removes a script tag from the Selenium document identified by the given +id. Does nothing if the referenced tag doesn't exist.</comment> +
+</function> +
+<function name="useXpathLibrary"> +
+<param name="libraryName">name of the desired library Only the following three can be chosen: <ul> <li>"ajaxslt" - Google's library</li> <li>"javascript-xpath" - Cybozu Labs' faster library</li> <li>"default" - The default library. Currently the default library is "ajaxslt" .</li> </ul> If libraryName isn't one of these three, then no change will be made.</param> +
+<comment>Allows choice of one of the available libraries.</comment> +
+</function> +
<function name="pause"> <param name="waitTime">the amount of time to sleep (in milliseconds)</param> diff --git a/tests/selenium/selenium-lib/core/iedoc.xml b/tests/selenium/selenium-lib/core/iedoc.xml index 23c1bebd..c8f86584 100644 --- a/tests/selenium/selenium-lib/core/iedoc.xml +++ b/tests/selenium/selenium-lib/core/iedoc.xml @@ -79,6 +79,16 @@ Select the element using css selectors. Please refer to <a href="http://www.w3.o </ul> <p>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). </p> </li> + +<li><strong>ui</strong>=<em>uiSpecifierString</em>: +Locate an element by resolving the UI specifier string to another locator, and evaluating it. See the <a href="http://svn.openqa.org/fisheye/browse/~raw,r=trunk/selenium/trunk/src/main/resources/core/scripts/ui-doc.html">Selenium UI-Element Reference</a> for more details. +<ul class="first last simple"> +<li>ui=loginPages::loginButton()</li> +<li>ui=settingsPages::toggle(label=Hide Email)</li> +<li>ui=forumPages::postBody(index=2)//a[2]</li> +</ul> +</li> + </ul> <p> @@ -123,6 +133,8 @@ string.</li> <li><strong>regexp:</strong><em>regexp</em>: Match a string using a regular-expression. The full power of JavaScript regular-expressions is available.</li> +<li><strong>regexpi:</strong><em>regexpi</em>: +Match a string using a case-insensitive regular-expression.</li> <li><strong>exact:</strong><em>string</em>: Match a string exactly, verbatim, without any of that fancy wildcard @@ -131,6 +143,14 @@ stuff.</li> <p> If no pattern prefix is specified, Selenium assumes that it's a "glob" pattern. +</p> +<p> +For commands that return multiple values (such as verifySelectOptions), +the string being matched is a comma-separated list of the return values, +where both commas and backslashes in the values are backslash-escaped. +When providing a pattern, the optional matching syntax (i.e. glob, +regexp, etc.) is specified once, as usual, at the beginning of the +pattern. </p></top> <function name="click"> @@ -153,6 +173,14 @@ waitForPageToLoad.</comment> </function> +<function name="contextMenu"> +
+<param name="locator">an element locator</param> +
+<comment>Simulates opening the context menu for the specified element (as might happen if the user "right-clicked" on the element).</comment> +
+</function> +
<function name="clickAt"> <param name="locator">an element locator</param> @@ -177,6 +205,16 @@ waitForPageToLoad.</comment> </function> +<function name="contextMenuAt"> +
+<param name="locator">an element locator</param> +
+<param name="coordString">specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator.</param> +
+<comment>Simulates opening the context menu for the specified element (as might happen if the user "right-clicked" on the element).</comment> +
+</function> +
<function name="fireEvent"> <param name="locator">an <a href="#locators">element locator</a></param> @@ -188,6 +226,14 @@ handler.</comment> </function> +<function name="focus"> +
+<param name="locator">an <a href="#locators">element locator</a></param> +
+<comment>Move the focus to the specified element; for example, if the element is an input field, move the cursor to that field.</comment> +
+</function> +
<function name="keyPress"> <param name="locator">an <a href="#locators">element locator</a></param> @@ -286,7 +332,16 @@ handler.</comment> <param name="locator">an <a href="#locators">element locator</a></param> -<comment>Simulates a user pressing the mouse button (without releasing it yet) on +<comment>Simulates a user pressing the left mouse button (without releasing it yet) on +the specified element.</comment> +
+</function> +
+<function name="mouseDownRight"> +
+<param name="locator">an <a href="#locators">element locator</a></param> +
+<comment>Simulates a user pressing the right mouse button (without releasing it yet) on the specified element.</comment> </function> @@ -297,7 +352,18 @@ the specified element.</comment> <param name="coordString">specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator.</param> -<comment>Simulates a user pressing the mouse button (without releasing it yet) at +<comment>Simulates a user pressing the left mouse button (without releasing it yet) at +the specified location.</comment> +
+</function> +
+<function name="mouseDownRightAt"> +
+<param name="locator">an <a href="#locators">element locator</a></param> +
+<param name="coordString">specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator.</param> +
+<comment>Simulates a user pressing the right mouse button (without releasing it yet) at the specified location.</comment> </function> @@ -311,6 +377,15 @@ holding the button down) on the specified element.</comment> </function> +<function name="mouseUpRight"> +
+<param name="locator">an <a href="#locators">element locator</a></param> +
+<comment>Simulates the event that occurs when the user releases the right mouse button (i.e., stops +holding the button down) on the specified element.</comment> +
+</function> +
<function name="mouseUpAt"> <param name="locator">an <a href="#locators">element locator</a></param> @@ -322,6 +397,17 @@ holding the button down) at the specified location.</comment> </function> +<function name="mouseUpRightAt"> +
+<param name="locator">an <a href="#locators">element locator</a></param> +
+<param name="coordString">specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator.</param> +
+<comment>Simulates the event that occurs when the user releases the right mouse button (i.e., stops +holding the button down) at the specified location.</comment> +
+</function> +
<function name="mouseMove"> <param name="locator">an <a href="#locators">element locator</a></param> @@ -386,6 +472,8 @@ the delay is 0 milliseconds.</comment> <function name="getSpeed"> +<return type="string">the execution speed in milliseconds.</return> +
<comment>Get execution speed (i.e., get the millisecond length of the delay following each selenium operation). By default, there is no such delay, i.e., the delay is 0 milliseconds. @@ -538,27 +626,43 @@ an empty (blank) url, like this: openWindow("", "myFunnyWindow").</p></comment> <param name="windowID">the JavaScript window ID of the window to select</param> -<comment>Selects a popup window; once a popup window has been selected, all +<comment>Selects a popup window using a window locator; once a popup window has been selected, all commands go to that window. To select the main window again, use null as the target. -<p>Note that there is a big difference between a window's internal JavaScript "name" property -and the "title" of a given window's document (which is normally what you actually see, as an end user, -in the title bar of the window). The "name" is normally invisible to the end-user; it's the second -parameter "windowName" passed to the JavaScript method window.open(url, windowName, windowFeatures, replaceFlag) -(which selenium intercepts).</p> - -<p>Selenium has several strategies for finding the window object referred to by the "windowID" parameter.</p> +<p> +Window locators provide different ways of specifying the window object: +by title, by internal JavaScript "name," or by JavaScript variable. +</p> +<ul> +<li><strong>title</strong>=<em>My Special Window</em>: +Finds the window using the text that appears in the title bar. Be careful; +two windows can share the same title. If that happens, this locator will +just pick one. +</li> +<li><strong>name</strong>=<em>myWindow</em>: +Finds the window using its internal JavaScript "name" property. This is the second +parameter "windowName" passed to the JavaScript method window.open(url, windowName, windowFeatures, replaceFlag) +(which Selenium intercepts). +</li> +<li><strong>var</strong>=<em>variableName</em>: +Some pop-up windows are unnamed (anonymous), but are associated with a JavaScript variable name in the current +application window, e.g. "window.foo = window.open(url);". In those cases, you can open the window using +"var=foo". +</li> +</ul> +<p> +If no window locator prefix is provided, we'll try to guess what you mean like this:</p> <p>1.) if windowID is null, (or the string "null") then it is assumed the user is referring to the original window instantiated by the browser).</p> <p>2.) if the value of the "windowID" parameter is a JavaScript variable name in the current application window, then it is assumed that this variable contains the return value from a call to the JavaScript window.open() method.</p> <p>3.) Otherwise, selenium looks in a hash it maintains that maps string names to window "names".</p> -<p>4.) If <i>that</i> fails, we'll try looping over all of the known windows to try to find the appropriate "title". +<p>4.) If <em>that</em> fails, we'll try looping over all of the known windows to try to find the appropriate "title". Since "title" is not necessarily unique, this may have unexpected behavior.</p> -<p>If you're having trouble figuring out what is the name of a window that you want to manipulate, look at the selenium log messages -which identify the names of windows created via window.open (and therefore intercepted by selenium). You will see messages +<p>If you're having trouble figuring out the name of a window that you want to manipulate, look at the Selenium log messages +which identify the names of windows created via window.open (and therefore intercepted by Selenium). You will see messages like the following for each window as it is opened:</p> <p><code>debug: window.open call intercepted; window ID (which you can use with selectWindow()) is "myNewWindow"</code></p> @@ -569,6 +673,36 @@ an empty (blank) url, like this: openWindow("", "myFunnyWindow").</p></comment> </function> +<function name="selectPopUp"> +
+<param name="windowID">an identifier for the popup window, which can take on a number of different meanings</param> +
+<comment>Simplifies the process of selecting a popup window (and does not offer +functionality beyond what <code>selectWindow()</code> already provides). +<ul> +<li>If <code>windowID</code> is either not specified, or specified as +"null", the first non-top window is selected. The top window is the one +that would be selected by <code>selectWindow()</code> without providing a +<code>windowID</code> . This should not be used when more than one popup +window is in play.</li> +<li>Otherwise, the window will be looked up considering +<code>windowID</code> as the following in order: 1) the "name" of the +window, as specified to <code>window.open()</code>; 2) a javascript +variable which is a reference to a window; and 3) the title of the +window. This is the same ordered lookup performed by +<code>selectWindow</code> .</li> +</ul></comment> +
+</function> +
+<function name="deselectPopUp"> +
+<comment>Selects the main window. Functionally equivalent to using +<code>selectWindow()</code> and specifying no value for +<code>windowID</code>.</comment> +
+</function> +
<function name="selectFrame"> <param name="locator">an <a href="#locators">element locator</a> identifying a frame or iframe</param> @@ -622,9 +756,9 @@ The selected window will return true, while all others will return false.</p></c <function name="waitForPopUp"> -<param name="windowID">the JavaScript window ID of the window that will appear</param> +<param name="windowID">the JavaScript window "name" of the window that will appear (not the text of the title bar) If unspecified, or specified as "null", this command will wait for the first non-top window to appear (don't rely on this if you are working with multiple popups simultaneously).</param> -<param name="timeout">a timeout in milliseconds, after which the action will return with an error</param> +<param name="timeout">a timeout in milliseconds, after which the action will return with an error. If this value is not specified, the default Selenium timeout will be used. See the setTimeout() command.</param> <comment>Waits for a popup window to appear and load up.</comment> @@ -632,26 +766,40 @@ The selected window will return true, while all others will return false.</p></c <function name="chooseCancelOnNextConfirmation"> -<comment>By default, Selenium's overridden window.confirm() function will +<comment><p> +By default, Selenium's overridden window.confirm() function will return true, as if the user had manually clicked OK; after running this command, the next call to confirm() will return false, as if the user had clicked Cancel. Selenium will then resume using the default behavior for future confirmations, automatically returning true (OK) unless/until you explicitly call this command for each -confirmation.</comment> +confirmation. +</p> +<p> +Take note - every time a confirmation comes up, you must +consume it with a corresponding getConfirmation, or else +the next selenium operation will fail. +</p></comment> </function> <function name="chooseOkOnNextConfirmation"> -<comment>Undo the effect of calling chooseCancelOnNextConfirmation. Note +<comment><p> +Undo the effect of calling chooseCancelOnNextConfirmation. Note that Selenium's overridden window.confirm() function will normally automatically return true, as if the user had manually clicked OK, so you shouldn't need to use this command unless for some reason you need to change your mind prior to the next confirmation. After any confirmation, Selenium will resume using the default behavior for future confirmations, automatically returning true (OK) unless/until you explicitly call chooseCancelOnNextConfirmation for each -confirmation.</comment> +confirmation. +</p> +<p> +Take note - every time a confirmation comes up, you must +consume it with a corresponding getConfirmation, or else +the next selenium operation will fail. +</p></comment> </function> @@ -726,13 +874,13 @@ This function never throws an exception <comment>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 +alert is generated but you do not consume it with getAlert, the next Selenium action will fail.</p> -<p>NOTE: under Selenium, JavaScript alerts will NOT pop up a visible alert +<p>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 +<p>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></comment> @@ -748,8 +896,11 @@ 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. +chooseCancelOnNextConfirmation command. +</p> +<p> +If an confirmation is generated but you do not consume it with getConfirmation, +the next Selenium action will fail. </p> <p> @@ -989,7 +1140,9 @@ tableLocator.row.column, where row and column start at 0.</comment> <param name="attributeLocator">an element locator followed by an @ sign and then the name of the attribute, e.g. "foo@bar"</param> -<comment>Gets the value of an element attribute.</comment> +<comment>Gets the value of an element attribute. The value of the attribute may +differ across browsers (this is the case for the "style" attribute, for +example).</comment> </function> @@ -1074,7 +1227,7 @@ This method will fail if the specified element isn't an input element.</comment> <param name="attributeName">name of an attribute on the windows</param> -<comment>Returns every instance of some attribute from all known windows.</comment> +<comment>Returns an array of JavaScript property values from all known windows having one.</comment> </function> @@ -1144,25 +1297,25 @@ just send one "mousemove" at the start location and then one final one at the en <function name="getAllWindowIds"> -<return type="string[]">the IDs of all windows that the browser knows about.</return> +<return type="string[]">Array of identifiers of all windows that the browser knows about.</return> -<comment>Returns the IDs of all windows that the browser knows about.</comment> +<comment>Returns the IDs of all windows that the browser knows about in an array.</comment> </function> <function name="getAllWindowNames"> -<return type="string[]">the names of all windows that the browser knows about.</return> +<return type="string[]">Array of names of all windows that the browser knows about.</return> -<comment>Returns the names of all windows that the browser knows about.</comment> +<comment>Returns the names of all windows that the browser knows about in an array.</comment> </function> <function name="getAllWindowTitles"> -<return type="string[]">the titles of all windows that the browser knows about.</return> +<return type="string[]">Array of titles of all windows that the browser knows about.</return> -<comment>Returns the titles of all windows that the browser knows about.</comment> +<comment>Returns the titles of all windows that the browser knows about in an array.</comment> </function> @@ -1313,6 +1466,22 @@ version is much slower than the native implementations.</comment> </function> +<function name="ignoreAttributesWithoutValue"> +
+<param name="ignore">boolean, true means we'll ignore attributes without value at the expense of xpath "correctness"; false means we'll sacrifice speed for correctness.</param> +
+<comment>Specifies whether Selenium will ignore xpath attributes that have no +value, i.e. are the empty string, when using the non-native xpath +evaluation engine. You'd want to do this for performance reasons in IE. +However, this could break certain xpaths, for example an xpath that looks +for an attribute whose value is NOT the empty string. + +The hope is that such xpaths are relatively rare, but the user should +have the option of using them. Note that this only influences xpath +evaluation when using the ajaxslt engine (i.e. not "javascript-xpath").</comment> +
+</function> +
<function name="waitForCondition"> <param name="script">the JavaScript snippet to run</param> @@ -1380,11 +1549,31 @@ See waitForPageToLoad for more information.</comment> </function> +<function name="getCookieByName"> +
+<return type="string">the value of the cookie</return> +
+<param name="name">the name of the cookie</param> +
+<comment>Returns the value of the cookie with the specified name, or throws an error if the cookie is not present.</comment> +
+</function> +
+<function name="isCookiePresent"> +
+<return type="boolean">true if a cookie with the specified name is present, or false otherwise.</return> +
+<param name="name">the name of the cookie</param> +
+<comment>Returns true if a cookie with the specified name is present, or false otherwise.</comment> +
+</function> +
<function name="createCookie"> <param name="nameValuePair">name and value of the cookie in a format "name=value"</param> -<param name="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.</param> +<param name="optionsString">options for the cookie. Currently supported options include 'path', 'max_age' and 'domain'. the optionsString's format is "path=/path/, max_age=60, domain=.foo.com". The order of options are irrelevant, the unit of the value of 'max_age' is second. Note that specifying a domain that isn't a subset of the current domain will usually fail.</param> <comment>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.</comment> @@ -1395,9 +1584,26 @@ under test, unless you specified a path for this cookie explicitly.</comment> <param name="name">the name of the cookie to be deleted</param> -<param name="path">the path property of the cookie to be deleted</param> +<param name="optionsString">options for the cookie. Currently supported options include 'path', 'domain' and 'recurse.' The optionsString's format is "path=/path/, domain=.foo.com, recurse=true". The order of options are irrelevant. Note that specifying a domain that isn't a subset of the current domain will usually fail.</param> +
+<comment>Delete a named cookie with specified path and domain. Be careful; to delete a cookie, you +need to delete it using the exact same path and domain that were used to create the cookie. +If the path is wrong, or the domain is wrong, the cookie simply won't be deleted. Also +note that specifying a domain that isn't a subset of the current domain will usually fail. + +Since there's no way to discover at runtime the original path and domain of a given cookie, +we've added an option called 'recurse' to try all sub-domains of the current domain with +all paths that are a subset of the current path. Beware; this option can be slow. In +big-O notation, it operates in O(n*m) time, where n is the number of dots in the domain +name and m is the number of slashes in the path.</comment> +
+</function> +
+<function name="deleteAllVisibleCookies"> -<comment>Delete a named cookie with specified path.</comment> +<comment>Calls deleteCookie with recurse=true on all cookies visible to the current page. +As noted on the documentation for deleteCookie, recurse=true can be much slower +than simply deleting the cookies using a known domain/path.</comment> </function> @@ -1449,6 +1655,74 @@ The function must return null if the element can't be found.</comment> </function> +<function name="captureEntirePageScreenshot"> +
+<param name="filename">the path to the file to persist the screenshot as. No filename extension will be appended by default. Directories will not be created if they do not exist, and an exception will be thrown, possibly by native code.</param> +
+<param name="kwargs">a kwargs string that modifies the way the screenshot is captured. Example: "background=#CCFFDD" . Currently valid options: <dl> <dt>background</dt> <dd>the background CSS for the HTML document. This may be useful to set for capturing screenshots of less-than-ideal layouts, for example where absolute positioning causes the calculation of the canvas dimension to fail and a black background is exposed (possibly obscuring black text).</dd> </dl></param> +
+<comment>Saves the entire contents of the current window canvas to a PNG file. +Contrast this with the captureScreenshot command, which captures the +contents of the OS viewport (i.e. whatever is currently being displayed +on the monitor), and is implemented in the RC only. Currently this only +works in Firefox when running in chrome mode, and in IE non-HTA using +the EXPERIMENTAL "Snapsie" utility. The Firefox implementation is mostly +borrowed from the Screengrab! Firefox extension. Please see +http://www.screengrab.org and http://snapsie.sourceforge.net/ for +details.</comment> +
+</function> +
+<function name="rollup"> +
+<param name="rollupName">the name of the rollup command</param> +
+<param name="kwargs">keyword arguments string that influences how the rollup expands into commands</param> +
+<comment>Executes a command rollup, which is a series of commands with a unique +name, and optionally arguments that control the generation of the set of +commands. If any one of the rolled-up commands fails, the rollup is +considered to have failed. Rollups may also contain nested rollups.</comment> +
+</function> +
+<function name="addScript"> +
+<param name="scriptContent">the Javascript content of the script to add</param> +
+<param name="scriptTagId">(optional) the id of the new script tag. If specified, and an element with this id already exists, this operation will fail.</param> +
+<comment>Loads script content into a new script tag in the Selenium document. This +differs from the runScript command in that runScript adds the script tag +to the document of the AUT, not the Selenium document. The following +entities in the script content are replaced by the characters they +represent: + + < + > + & + +The corresponding remove command is removeScript.</comment> +
+</function> +
+<function name="removeScript"> +
+<param name="scriptTagId">the id of the script element to remove.</param> +
+<comment>Removes a script tag from the Selenium document identified by the given +id. Does nothing if the referenced tag doesn't exist.</comment> +
+</function> +
+<function name="useXpathLibrary"> +
+<param name="libraryName">name of the desired library Only the following three can be chosen: <ul> <li>"ajaxslt" - Google's library</li> <li>"javascript-xpath" - Cybozu Labs' faster library</li> <li>"default" - The default library. Currently the default library is "ajaxslt" .</li> </ul> If libraryName isn't one of these three, then no change will be made.</param> +
+<comment>Allows choice of one of the available libraries.</comment> +
+</function> +
<function name="setContext"> <param name="context">the message to be sent to the browser</param> @@ -1458,6 +1732,16 @@ log.</comment> </function> +<function name="attachFile"> +
+<param name="fieldLocator">an <a href="#locators">element locator</a></param> +
+<param name="fileLocator">a URL pointing to the specified file. Before the file can be set in the input field (fieldLocator), Selenium RC may need to transfer the file to the local machine before attaching the file in a web page form. This is common in selenium grid configurations where the RC server driving the browser is not the same machine that started the test. Supported Browsers: Firefox ("*chrome") only.</param> +
+<comment>Sets a file input (upload) field to the file listed in fileLocator</comment> +
+</function> +
<function name="captureScreenshot"> <param name="filename">the absolute path to the file to be written, e.g. "c:\blah\screenshot.png"</param> @@ -1466,4 +1750,81 @@ log.</comment> </function> +<function name="captureScreenshotToString"> +
+<return type="string">The base 64 encoded string of the screen shot (PNG file)</return> +
+<comment>Capture a PNG screenshot. It then returns the file as a base 64 encoded string.</comment> +
+</function> +
+<function name="captureEntirePageScreenshotToString"> +
+<return type="string">The base 64 encoded string of the page screenshot (PNG file)</return> +
+<param name="kwargs">A kwargs string that modifies the way the screenshot is captured. Example: "background=#CCFFDD". This may be useful to set for capturing screenshots of less-than-ideal layouts, for example where absolute positioning causes the calculation of the canvas dimension to fail and a black background is exposed (possibly obscuring black text).</param> +
+<comment>Downloads a screenshot of the browser current window canvas to a +based 64 encoded PNG file. The <em>entire</em> windows canvas is captured, +including parts rendered outside of the current view port. + +Currently this only works in Mozilla and when running in chrome mode.</comment> +
+</function> +
+<function name="shutDownSeleniumServer"> +
+<comment>Kills the running Selenium Server and all browser sessions. After you run this command, you will no longer be able to send +commands to the server; you can't remotely start the server once it has been stopped. Normally +you should prefer to run the "stop" command, which terminates the current browser session, rather than +shutting down the entire server.</comment> +
+</function> +
+<function name="retrieveLastRemoteControlLogs"> +
+<return type="string">The last N log messages as a multi-line string.</return> +
+<comment>Retrieve the last messages logged on a specific remote control. Useful for error reports, especially +when running multiple remote controls in a distributed environment. The maximum number of log messages +that can be retrieve is configured on remote control startup.</comment> +
+</function> +
+<function name="keyDownNative"> +
+<param name="keycode">an integer keycode number corresponding to a java.awt.event.KeyEvent; note that Java keycodes are NOT the same thing as JavaScript keycodes!</param> +
+<comment>Simulates a user pressing a key (without releasing it yet) by sending a native operating system keystroke. +This function uses the java.awt.Robot class to send a keystroke; this more accurately simulates typing +a key on the keyboard. It does not honor settings from the shiftKeyDown, controlKeyDown, altKeyDown and +metaKeyDown commands, and does not target any particular HTML element. To send a keystroke to a particular +element, focus on the element first before running this command.</comment> +
+</function> +
+<function name="keyUpNative"> +
+<param name="keycode">an integer keycode number corresponding to a java.awt.event.KeyEvent; note that Java keycodes are NOT the same thing as JavaScript keycodes!</param> +
+<comment>Simulates a user releasing a key by sending a native operating system keystroke. +This function uses the java.awt.Robot class to send a keystroke; this more accurately simulates typing +a key on the keyboard. It does not honor settings from the shiftKeyDown, controlKeyDown, altKeyDown and +metaKeyDown commands, and does not target any particular HTML element. To send a keystroke to a particular +element, focus on the element first before running this command.</comment> +
+</function> +
+<function name="keyPressNative"> +
+<param name="keycode">an integer keycode number corresponding to a java.awt.event.KeyEvent; note that Java keycodes are NOT the same thing as JavaScript keycodes!</param> +
+<comment>Simulates a user pressing and releasing a key by sending a native operating system keystroke. +This function uses the java.awt.Robot class to send a keystroke; this more accurately simulates typing +a key on the keyboard. It does not honor settings from the shiftKeyDown, controlKeyDown, altKeyDown and +metaKeyDown commands, and does not target any particular HTML element. To send a keystroke to a particular +element, focus on the element first before running this command.</comment> +
+</function> +
</apidoc>
diff --git a/tests/selenium/selenium-lib/core/lib/snapsie.js b/tests/selenium/selenium-lib/core/lib/snapsie.js new file mode 100644 index 00000000..23503792 --- /dev/null +++ b/tests/selenium/selenium-lib/core/lib/snapsie.js @@ -0,0 +1,91 @@ +/**
+ * This file wraps the Snapsie ActiveX object, exposing a single saveSnapshot()
+ * method on a the object.
+ *
+ * See http://snapsie.sourceforge.net/
+ */
+
+function Snapsie() {
+ // private methods
+
+ function isQuirksMode(inDocument) {
+ return (inDocument.compatMode == 'BackCompat');
+ }
+
+ function getDrawableElement(inDocument) {
+ if (isQuirksMode(inDocument)) {
+ var body = inDocument.getElementsByTagName('body')[0];
+ return body;
+ }
+ else {
+ // standards mode
+ return inDocument.documentElement;
+ }
+ }
+
+ /**
+ * Returns the canonical Windows path for a given path. This means
+ * basically replacing any forwards slashes with backslashes.
+ *
+ * @param path the path whose canonical form to return
+ */
+ function getCanonicalPath(path) {
+ path = path.replace(/\//g, '\\');
+ path = path.replace(/\\\\/g, '\\');
+ return path;
+ }
+
+ // public methods
+
+ /**
+ * Saves a screenshot of the current document to a file. If frameId is
+ * specified, a screenshot of just the frame is captured instead.
+ *
+ * @param outputFile the file to which to save the screenshot
+ * @param frameId the frame to capture; omit to capture entire document
+ */
+ this.saveSnapshot = function(outputFile, frameId) {
+ var drawableElement = getDrawableElement(document);
+ var drawableInfo = {
+ overflow : drawableElement.style.overflow
+ , scrollLeft: drawableElement.scrollLeft
+ , scrollTop : drawableElement.scrollTop
+ };
+ drawableElement.style.overflow = 'hidden';
+
+ var capturableDocument;
+ var frameBCR = { left: 0, top: 0 };
+ if (!frameId) {
+ capturableDocument = document;
+ }
+ else {
+ var frame = document.getElementById(frameId);
+ capturableDocument = frame.document;
+
+ // scroll as much of the frame into view as possible
+ frameBCR = frame.getBoundingClientRect();
+ window.scroll(frameBCR.left, frameBCR.top);
+ frameBCR = frame.getBoundingClientRect();
+ }
+
+ var nativeObj = new ActiveXObject('Snapsie.CoSnapsie');
+ nativeObj.saveSnapshot(
+ getCanonicalPath(outputFile),
+ frameId,
+ drawableElement.scrollWidth,
+ drawableElement.scrollHeight,
+ drawableElement.clientWidth,
+ drawableElement.clientHeight,
+ drawableElement.clientLeft,
+ drawableElement.clientTop,
+ frameBCR.left,
+ frameBCR.top
+ );
+
+ // revert
+
+ drawableElement.style.overflow = drawableInfo.overflow;
+ drawableElement.scrollLeft = drawableInfo.scrollLeft;
+ drawableElement.scrollTop = drawableInfo.scrollTop;
+ }
+};
diff --git a/tests/selenium/selenium-lib/core/scripts/htmlutils.js b/tests/selenium/selenium-lib/core/scripts/htmlutils.js index 337fb5b5..886f8972 100644 --- a/tests/selenium/selenium-lib/core/scripts/htmlutils.js +++ b/tests/selenium/selenium-lib/core/scripts/htmlutils.js @@ -121,6 +121,46 @@ String.prototype.startsWith = function(str) { return this.indexOf(str) == 0;
};
+/**
+ * Given a string literal that would appear in an XPath, puts it in quotes and
+ * returns it. Special consideration is given to literals who themselves
+ * contain quotes. It's possible for a concat() expression to be returned.
+ */
+String.prototype.quoteForXPath = function()
+{
+ if (/\'/.test(this)) {
+ if (/\"/.test(this)) {
+ // concat scenario
+ var pieces = [];
+ var a = "'", b = '"', c;
+ for (var i = 0, j = 0; i < this.length;) {
+ if (this.charAt(i) == a) {
+ // encountered a quote that cannot be contained in current
+ // quote, so need to flip-flop quoting scheme
+ if (j < i) {
+ pieces.push(a + this.substring(j, i) + a);
+ j = i;
+ }
+ c = a;
+ a = b;
+ b = c;
+ }
+ else {
+ ++i;
+ }
+ }
+ pieces.push(a + this.substring(j) + a);
+ return 'concat(' + pieces.join(', ') + ')';
+ }
+ else {
+ // quote with doubles
+ return '"' + this + '"';
+ }
+ }
+ // quote with singles
+ return "'" + this + "'";
+};
+
// Returns the text in this element
function getText(element) {
var text = "";
@@ -141,6 +181,7 @@ function getText(element) { }
function getTextContent(element, preformatted) {
+ if (element.style && (element.style.visibility == 'hidden' || element.style.display == 'none')) return '';
if (element.nodeType == 3 /*Node.TEXT_NODE*/) {
var text = element.data;
if (!preformatted) {
@@ -148,7 +189,7 @@ function getTextContent(element, preformatted) { }
return text;
}
- if (element.nodeType == 1 /*Node.ELEMENT_NODE*/) {
+ if (element.nodeType == 1 /*Node.ELEMENT_NODE*/ && element.nodeName != 'SCRIPT') {
var childrenPreformatted = preformatted || (element.tagName == "PRE");
var text = "";
for (var i = 0; i < element.childNodes.length; i++) {
@@ -171,7 +212,7 @@ function getTextContent(element, preformatted) { }
/**
- * Convert all newlines to \m
+ * Convert all newlines to \n
*/
function normalizeNewlines(text)
{
@@ -246,7 +287,7 @@ function getInputValue(inputElement) { /* Fire an event in a browser-compatible manner */
function triggerEvent(element, eventType, canBubble, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown) {
canBubble = (typeof(canBubble) == undefined) ? true : canBubble;
- if (element.fireEvent) {
+ if (element.fireEvent && element.ownerDocument && element.ownerDocument.createEventObject) { // IE
var evt = createEventObject(element, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown);
element.fireEvent('on' + eventType, evt);
}
@@ -299,7 +340,7 @@ function createEventObject(element, controlKeyDown, altKeyDown, shiftKeyDown, me function triggerKeyEvent(element, eventType, keySequence, canBubble, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown) {
var keycode = getKeyCodeFromKeySequence(keySequence);
canBubble = (typeof(canBubble) == undefined) ? true : canBubble;
- if (element.fireEvent) {
+ if (element.fireEvent && element.ownerDocument && element.ownerDocument.createEventObject) { // IE
var keyEvent = createEventObject(element, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown);
keyEvent.keyCode = keycode;
element.fireEvent('on' + eventType, keyEvent);
@@ -518,6 +559,10 @@ function reassembleLocation(loc) { }
function canonicalize(url) {
+ if(url == "about:blank")
+ {
+ return url;
+ }
var tempLink = window.document.createElement("link");
tempLink.href = url; // this will canonicalize the href on most browsers
var loc = parseUrl(tempLink.href)
@@ -626,6 +671,20 @@ PatternMatcher.strategies = { return this.regexp.test(actual);
};
},
+
+ regexpi: function(regexpString) {
+ this.regexp = new RegExp(regexpString, "i");
+ this.matches = function(actual) {
+ return this.regexp.test(actual);
+ };
+ },
+
+ regexi: function(regexpString) {
+ this.regexp = new RegExp(regexpString, "i");
+ this.matches = function(actual) {
+ return this.regexp.test(actual);
+ };
+ },
/**
* "globContains" (aka "wildmat") patterns, e.g. "glob:one,two,*",
@@ -674,52 +733,54 @@ PatternMatcher.regexpFromGlob = function(glob) { return "^" + PatternMatcher.convertGlobMetaCharsToRegexpMetaChars(glob) + "$";
};
-var Assert = {
+if (!this["Assert"]) Assert = {};
- fail: function(message) {
- throw new AssertionFailedError(message);
- },
+
+Assert.fail = function(message) {
+ throw new AssertionFailedError(message);
+};
/*
* Assert.equals(comment?, expected, actual)
*/
- equals: function() {
- var args = new AssertionArguments(arguments);
- if (args.expected === args.actual) {
- return;
- }
- Assert.fail(args.comment +
- "Expected '" + args.expected +
- "' but was '" + args.actual + "'");
- },
+Assert.equals = function() {
+ var args = new AssertionArguments(arguments);
+ if (args.expected === args.actual) {
+ return;
+ }
+ Assert.fail(args.comment +
+ "Expected '" + args.expected +
+ "' but was '" + args.actual + "'");
+};
+
+Assert.assertEquals = Assert.equals;
/*
* Assert.matches(comment?, pattern, actual)
*/
- matches: function() {
- var args = new AssertionArguments(arguments);
- if (PatternMatcher.matches(args.expected, args.actual)) {
- return;
- }
- Assert.fail(args.comment +
- "Actual value '" + args.actual +
- "' did not match '" + args.expected + "'");
- },
+Assert.matches = function() {
+ var args = new AssertionArguments(arguments);
+ if (PatternMatcher.matches(args.expected, args.actual)) {
+ return;
+ }
+ Assert.fail(args.comment +
+ "Actual value '" + args.actual +
+ "' did not match '" + args.expected + "'");
+}
/*
* Assert.notMtches(comment?, pattern, actual)
*/
- notMatches: function() {
- var args = new AssertionArguments(arguments);
- if (!PatternMatcher.matches(args.expected, args.actual)) {
- return;
- }
- Assert.fail(args.comment +
- "Actual value '" + args.actual +
- "' did match '" + args.expected + "'");
+Assert.notMatches = function() {
+ var args = new AssertionArguments(arguments);
+ if (!PatternMatcher.matches(args.expected, args.actual)) {
+ return;
}
+ Assert.fail(args.comment +
+ "Actual value '" + args.actual +
+ "' did match '" + args.expected + "'");
+}
-};
// Preprocess the arguments to allow for an optional comment.
function AssertionArguments(args) {
@@ -795,7 +856,7 @@ function openSeparateApplicationWindow(url, suppressMozillaWarning) { window.resizeTo(1200, 500);
window.moveTo(window.screenX, 0);
- var appWindow = window.open(url + '?start=true', 'main');
+ var appWindow = window.open(url + '?start=true', 'selenium_main_app_window');
if (appWindow == null) {
var errorMessage = "Couldn't open app window; is the pop-up blocker enabled?"
LOG.error(errorMessage);
@@ -892,3 +953,671 @@ function safeScrollIntoView(element) { // TODO: work out how to scroll browsers that don't support
// scrollIntoView (like Konqueror)
}
+
+/**
+ * Returns the absolute time represented as an offset of the current time.
+ * Throws a SeleniumException if timeout is invalid.
+ *
+ * @param timeout the number of milliseconds from "now" whose absolute time
+ * to return
+ */
+function getTimeoutTime(timeout) {
+ var now = new Date().getTime();
+ var timeoutLength = parseInt(timeout);
+
+ if (isNaN(timeoutLength)) {
+ throw new SeleniumError("Timeout is not a number: '" + timeout + "'");
+ }
+
+ return now + timeoutLength;
+}
+
+/**
+ * Returns true iff the current environment is the IDE, and is not the chrome
+ * runner launched by the IDE.
+ */
+function is_IDE() {
+ var locstr = window.location.href;
+
+ if (locstr.indexOf('chrome://selenium-ide-testrunner') == 0) {
+ return false;
+ }
+
+ return (typeof(SeleniumIDE) != 'undefined');
+}
+
+/**
+ * Logs a message if the Logger exists, and does nothing if it doesn't exist.
+ *
+ * @param level the level to log at
+ * @param msg the message to log
+ */
+function safe_log(level, msg)
+{
+ try {
+ LOG[level](msg);
+ }
+ catch (e) {
+ // couldn't log!
+ }
+}
+
+/**
+ * Displays a warning message to the user appropriate to the context under
+ * which the issue is encountered. This is primarily used to avoid popping up
+ * alert dialogs that might pause an automated test suite.
+ *
+ * @param msg the warning message to display
+ */
+function safe_alert(msg)
+{
+ if (is_IDE()) {
+ alert(msg);
+ }
+}
+
+/**
+ * Returns true iff the given element represents a link with a javascript
+ * href attribute, and does not have an onclick attribute defined.
+ *
+ * @param element the element to test
+ */
+function hasJavascriptHref(element) {
+ if (getTagName(element) != 'a') {
+ return false;
+ }
+ if (element.getAttribute('onclick')) {
+ return false;
+ }
+ if (! element.href) {
+ return false;
+ }
+ if (! /\s*javascript:/i.test(element.href)) {
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Returns the given element, or its nearest ancestor, that satisfies
+ * hasJavascriptHref(). Returns null if none is found.
+ *
+ * @param element the element whose ancestors to test
+ */
+function getAncestorOrSelfWithJavascriptHref(element) {
+ if (hasJavascriptHref(element)) {
+ return element;
+ }
+ if (element.parentNode == null) {
+ return null;
+ }
+ return getAncestorOrSelfWithJavascriptHref(element.parentNode);
+}
+
+//******************************************************************************
+// Locator evaluation support
+
+/**
+ * Parses a Selenium locator, returning its type and the unprefixed locator
+ * string as an object.
+ *
+ * @param locator the locator to parse
+ */
+function parse_locator(locator)
+{
+ var result = locator.match(/^([A-Za-z]+)=(.+)/);
+ if (result) {
+ return { type: result[1].toLowerCase(), string: result[2] };
+ }
+ return { type: 'implicit', string: locator };
+}
+
+/**
+ * Evaluates an xpath on a document, and returns a list containing nodes in the
+ * resulting nodeset. The browserbot xpath methods are now backed by this
+ * function. A context node may optionally be provided, and the xpath will be
+ * evaluated from that context.
+ *
+ * @param xpath the xpath to evaluate
+ * @param inDocument the document in which to evaluate the xpath.
+ * @param opts (optional) An object containing various flags that can
+ * modify how the xpath is evaluated. Here's a listing of
+ * the meaningful keys:
+ *
+ * contextNode:
+ * the context node from which to evaluate the xpath. If
+ * unspecified, the context will be the root document
+ * element.
+ *
+ * namespaceResolver:
+ * the namespace resolver function. Defaults to null.
+ *
+ * xpathLibrary:
+ * the javascript library to use for XPath. "ajaxslt" is
+ * the default. "javascript-xpath" is newer and faster,
+ * but needs more testing.
+ *
+ * allowNativeXpath:
+ * whether to allow native evaluate(). Defaults to true.
+ *
+ * ignoreAttributesWithoutValue:
+ * whether it's ok to ignore attributes without value
+ * when evaluating the xpath. This can greatly improve
+ * performance in IE; however, if your xpaths depend on
+ * such attributes, you can't ignore them! Defaults to
+ * true.
+ *
+ * returnOnFirstMatch:
+ * whether to optimize the XPath evaluation to only
+ * return the first match. The match, if any, will still
+ * be returned in a list. Defaults to false.
+ */
+function eval_xpath(xpath, inDocument, opts)
+{
+ if (!opts) {
+ var opts = {};
+ }
+ var contextNode = opts.contextNode
+ ? opts.contextNode : inDocument;
+ var namespaceResolver = opts.namespaceResolver
+ ? opts.namespaceResolver : null;
+ var xpathLibrary = opts.xpathLibrary
+ ? opts.xpathLibrary : null;
+ var allowNativeXpath = (opts.allowNativeXpath != undefined)
+ ? opts.allowNativeXpath : true;
+ var ignoreAttributesWithoutValue = (opts.ignoreAttributesWithoutValue != undefined)
+ ? opts.ignoreAttributesWithoutValue : true;
+ var returnOnFirstMatch = (opts.returnOnFirstMatch != undefined)
+ ? opts.returnOnFirstMatch : false;
+
+ // Trim any trailing "/": not valid xpath, and remains from attribute
+ // locator.
+ if (xpath.charAt(xpath.length - 1) == '/') {
+ xpath = xpath.slice(0, -1);
+ }
+ // HUGE hack - remove namespace from xpath for IE
+ if (browserVersion && browserVersion.isIE) {
+ xpath = xpath.replace(/x:/g, '')
+ }
+
+ var nativeXpathAvailable = inDocument.evaluate;
+ var useNativeXpath = allowNativeXpath && nativeXpathAvailable;
+ var useDocumentEvaluate = useNativeXpath;
+
+ // When using the new and faster javascript-xpath library,
+ // we'll use the TestRunner's document object, not the App-Under-Test's document.
+ // The new library only modifies the TestRunner document with the new
+ // functionality.
+ if (xpathLibrary == 'javascript-xpath' && !useNativeXpath) {
+ documentForXpath = document;
+ useDocumentEvaluate = true;
+ } else {
+ documentForXpath = inDocument;
+ }
+ var results = [];
+
+ // this is either native xpath or javascript-xpath via TestRunner.evaluate
+ if (useDocumentEvaluate) {
+ try {
+ // Regarding use of the second argument to document.evaluate():
+ // http://groups.google.com/group/comp.lang.javascript/browse_thread/thread/a59ce20639c74ba1/a9d9f53e88e5ebb5
+ var xpathResult = documentForXpath
+ .evaluate((contextNode == inDocument ? xpath : '.' + xpath),
+ contextNode, namespaceResolver, 0, null);
+ }
+ catch (e) {
+ throw new SeleniumError("Invalid xpath [1]: " + extractExceptionMessage(e));
+ }
+ finally{
+ if (xpathResult == null) {
+ // If the result is null, we should still throw an Error.
+ throw new SeleniumError("Invalid xpath [2]: " + xpath);
+ }
+ }
+ var result = xpathResult.iterateNext();
+ while (result) {
+ results.push(result);
+ result = xpathResult.iterateNext();
+ }
+ return results;
+ }
+
+ // If not, fall back to slower JavaScript implementation
+ // DGF set xpathdebug = true (using getEval, if you like) to turn on JS XPath debugging
+ //xpathdebug = true;
+ var context;
+ if (contextNode == inDocument) {
+ context = new ExprContext(inDocument);
+ }
+ else {
+ // provide false values to get the default constructor values
+ context = new ExprContext(contextNode, false, false,
+ contextNode.parentNode);
+ }
+ context.setCaseInsensitive(true);
+ context.setIgnoreAttributesWithoutValue(ignoreAttributesWithoutValue);
+ context.setReturnOnFirstMatch(returnOnFirstMatch);
+ var xpathObj;
+ try {
+ xpathObj = xpathParse(xpath);
+ }
+ catch (e) {
+ throw new SeleniumError("Invalid xpath [3]: " + extractExceptionMessage(e));
+ }
+ var xpathResult = xpathObj.evaluate(context);
+ if (xpathResult && xpathResult.value) {
+ for (var i = 0; i < xpathResult.value.length; ++i) {
+ results.push(xpathResult.value[i]);
+ }
+ }
+ return results;
+}
+
+/**
+ * Returns the full resultset of a CSS selector evaluation.
+ */
+function eval_css(locator, inDocument)
+{
+ return cssQuery(locator, inDocument);
+}
+
+/**
+ * This function duplicates part of BrowserBot.findElement() to open up locator
+ * evaluation on arbitrary documents. It returns a plain old array of located
+ * elements found by using a Selenium locator.
+ *
+ * Multiple results may be generated for xpath and CSS locators. Even though a
+ * list could potentially be generated for other locator types, such as link,
+ * we don't try for them, because they aren't very expressive location
+ * strategies; if you want a list, use xpath or CSS. Furthermore, strategies
+ * for these locators have been optimized to only return the first result. For
+ * these types of locators, performance is more important than ideal behavior.
+ *
+ * @param locator a locator string
+ * @param inDocument the document in which to apply the locator
+ * @param opt_contextNode the context within which to evaluate the locator
+ *
+ * @return a list of result elements
+ */
+function eval_locator(locator, inDocument, opt_contextNode)
+{
+ locator = parse_locator(locator);
+
+ var pageBot;
+ if (typeof(selenium) != 'undefined' && selenium != undefined) {
+ if (typeof(editor) == 'undefined' || editor.state == 'playing') {
+ safe_log('info', 'Trying [' + locator.type + ']: '
+ + locator.string);
+ }
+ pageBot = selenium.browserbot;
+ }
+ else {
+ if (!UI_GLOBAL.mozillaBrowserBot) {
+ // create a browser bot to evaluate the locator. Hand it the IDE
+ // window as a dummy window, and cache it for future use.
+ UI_GLOBAL.mozillaBrowserBot = new MozillaBrowserBot(window)
+ }
+ pageBot = UI_GLOBAL.mozillaBrowserBot;
+ }
+
+ var results = [];
+
+ if (locator.type == 'xpath' || (locator.string.charAt(0) == '/' &&
+ locator.type == 'implicit')) {
+ results = eval_xpath(locator.string, inDocument,
+ { contextNode: opt_contextNode });
+ }
+ else if (locator.type == 'css') {
+ results = eval_css(locator.string, inDocument);
+ }
+ else {
+ var element = pageBot
+ .findElementBy(locator.type, locator.string, inDocument);
+ if (element != null) {
+ results.push(element);
+ }
+ }
+
+ return results;
+}
+
+//******************************************************************************
+// UI-Element
+
+/**
+ * Escapes the special regular expression characters in a string intended to be
+ * used as a regular expression.
+ *
+ * Based on: http://simonwillison.net/2006/Jan/20/escape/
+ */
+RegExp.escape = (function() {
+ var specials = [
+ '/', '.', '*', '+', '?', '|', '^', '$',
+ '(', ')', '[', ']', '{', '}', '\\'
+ ];
+
+ var sRE = new RegExp(
+ '(\\' + specials.join('|\\') + ')', 'g'
+ );
+
+ return function(text) {
+ return text.replace(sRE, '\\$1');
+ }
+})();
+
+/**
+ * Returns true if two arrays are identical, and false otherwise.
+ *
+ * @param a1 the first array, may only contain simple values (strings or
+ * numbers)
+ * @param a2 the second array, same restricts on data as for a1
+ * @return true if the arrays are equivalent, false otherwise.
+ */
+function are_equal(a1, a2)
+{
+ if (typeof(a1) != typeof(a2))
+ return false;
+
+ switch(typeof(a1)) {
+ case 'object':
+ // arrays
+ if (a1.length) {
+ if (a1.length != a2.length)
+ return false;
+ for (var i = 0; i < a1.length; ++i) {
+ if (!are_equal(a1[i], a2[i]))
+ return false
+ }
+ }
+ // associative arrays
+ else {
+ var keys = {};
+ for (var key in a1) {
+ keys[key] = true;
+ }
+ for (var key in a2) {
+ keys[key] = true;
+ }
+ for (var key in keys) {
+ if (!are_equal(a1[key], a2[key]))
+ return false;
+ }
+ }
+ return true;
+
+ default:
+ return a1 == a2;
+ }
+}
+
+
+/**
+ * Create a clone of an object and return it. This is a deep copy of everything
+ * but functions, whose references are copied. You shouldn't expect a deep copy
+ * of functions anyway.
+ *
+ * @param orig the original object to copy
+ * @return a deep copy of the original object. Any functions attached,
+ * however, will have their references copied only.
+ */
+function clone(orig) {
+ var copy;
+ switch(typeof(orig)) {
+ case 'object':
+ copy = (orig.length) ? [] : {};
+ for (var attr in orig) {
+ copy[attr] = clone(orig[attr]);
+ }
+ break;
+ default:
+ copy = orig;
+ break;
+ }
+ return copy;
+}
+
+/**
+ * Emulates php's print_r() functionality. Returns a nicely formatted string
+ * representation of an object. Very useful for debugging.
+ *
+ * @param object the object to dump
+ * @param maxDepth the maximum depth to recurse into the object. Ellipses will
+ * be shown for objects whose depth exceeds the maximum.
+ * @param indent the string to use for indenting progressively deeper levels
+ * of the dump.
+ * @return a string representing a dump of the object
+ */
+function print_r(object, maxDepth, indent)
+{
+ var parentIndent, attr, str = "";
+ if (arguments.length == 1) {
+ var maxDepth = Number.MAX_VALUE;
+ } else {
+ maxDepth--;
+ }
+ if (arguments.length < 3) {
+ parentIndent = ''
+ var indent = ' ';
+ } else {
+ parentIndent = indent;
+ indent += ' ';
+ }
+
+ switch(typeof(object)) {
+ case 'object':
+ if (object.length != undefined) {
+ if (object.length == 0) {
+ str += "Array ()\r\n";
+ }
+ else {
+ str += "Array (\r\n";
+ for (var i = 0; i < object.length; ++i) {
+ str += indent + '[' + i + '] => ';
+ if (maxDepth == 0)
+ str += "...\r\n";
+ else
+ str += print_r(object[i], maxDepth, indent);
+ }
+ str += parentIndent + ")\r\n";
+ }
+ }
+ else {
+ str += "Object (\r\n";
+ for (attr in object) {
+ str += indent + "[" + attr + "] => ";
+ if (maxDepth == 0)
+ str += "...\r\n";
+ else
+ str += print_r(object[attr], maxDepth, indent);
+ }
+ str += parentIndent + ")\r\n";
+ }
+ break;
+ case 'boolean':
+ str += (object ? 'true' : 'false') + "\r\n";
+ break;
+ case 'function':
+ str += "Function\r\n";
+ break;
+ default:
+ str += object + "\r\n";
+ break;
+
+ }
+ return str;
+}
+
+/**
+ * Return an array containing all properties of an object. Perl-style.
+ *
+ * @param object the object whose keys to return
+ * @return array of object keys, as strings
+ */
+function keys(object)
+{
+ var keys = [];
+ for (var k in object) {
+ keys.push(k);
+ }
+ return keys;
+}
+
+/**
+ * Emulates python's range() built-in. Returns an array of integers, counting
+ * up (or down) from start to end. Note that the range returned is up to, but
+ * NOT INCLUDING, end.
+ *.
+ * @param start integer from which to start counting. If the end parameter is
+ * not provided, this value is considered the end and start will
+ * be zero.
+ * @param end integer to which to count. If omitted, the function will count
+ * up from zero to the value of the start parameter. Note that
+ * the array returned will count up to but will not include this
+ * value.
+ * @return an array of consecutive integers.
+ */
+function range(start, end)
+{
+ if (arguments.length == 1) {
+ var end = start;
+ start = 0;
+ }
+
+ var r = [];
+ if (start < end) {
+ while (start != end)
+ r.push(start++);
+ }
+ else {
+ while (start != end)
+ r.push(start--);
+ }
+ return r;
+}
+
+/**
+ * Parses a python-style keyword arguments string and returns the pairs in a
+ * new object.
+ *
+ * @param kwargs a string representing a set of keyword arguments. It should
+ * look like <tt>keyword1=value1, keyword2=value2, ...</tt>
+ * @return an object mapping strings to strings
+ */
+function parse_kwargs(kwargs)
+{
+ var args = new Object();
+ var pairs = kwargs.split(/,/);
+ for (var i = 0; i < pairs.length;) {
+ if (i > 0 && pairs[i].indexOf('=') == -1) {
+ // the value string contained a comma. Glue the parts back together.
+ pairs[i-1] += ',' + pairs.splice(i, 1)[0];
+ }
+ else {
+ ++i;
+ }
+ }
+ for (var i = 0; i < pairs.length; ++i) {
+ var splits = pairs[i].split(/=/);
+ if (splits.length == 1) {
+ continue;
+ }
+ var key = splits.shift();
+ var value = splits.join('=');
+ args[key.trim()] = value.trim();
+ }
+ return args;
+}
+
+/**
+ * Creates a python-style keyword arguments string from an object.
+ *
+ * @param args an associative array mapping strings to strings
+ * @param sortedKeys (optional) a list of keys of the args parameter that
+ * specifies the order in which the arguments will appear in
+ * the returned kwargs string
+ *
+ * @return a kwarg string representation of args
+ */
+function to_kwargs(args, sortedKeys)
+{
+ var s = '';
+ if (!sortedKeys) {
+ var sortedKeys = keys(args).sort();
+ }
+ for (var i = 0; i < sortedKeys.length; ++i) {
+ var k = sortedKeys[i];
+ if (args[k] != undefined) {
+ if (s) {
+ s += ', ';
+ }
+ s += k + '=' + args[k];
+ }
+ }
+ return s;
+}
+
+/**
+ * Returns true if a node is an ancestor node of a target node, and false
+ * otherwise.
+ *
+ * @param node the node being compared to the target node
+ * @param target the target node
+ * @return true if node is an ancestor node of target, false otherwise.
+ */
+function is_ancestor(node, target)
+{
+ while (target.parentNode) {
+ target = target.parentNode;
+ if (node == target)
+ return true;
+ }
+ return false;
+}
+
+//******************************************************************************
+// parseUri 1.2.1
+// MIT License
+
+/*
+Copyright (c) 2007 Steven Levithan <stevenlevithan.com>
+
+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.
+*/
+
+function parseUri (str) {
+ var o = parseUri.options,
+ m = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
+ uri = {},
+ i = 14;
+
+ while (i--) uri[o.key[i]] = m[i] || "";
+
+ uri[o.q.name] = {};
+ uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
+ if ($1) uri[o.q.name][$1] = $2;
+ });
+
+ return uri;
+};
+
+parseUri.options = {
+ strictMode: false,
+ key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
+ q: {
+ name: "queryKey",
+ parser: /(?:^|&)([^&=]*)=?([^&]*)/g
+ },
+ parser: {
+ strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
+ loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
+ }
+};
+
diff --git a/tests/selenium/selenium-lib/core/scripts/injection.html b/tests/selenium/selenium-lib/core/scripts/injection.html index 3d8aa03a..546e7500 100644 --- a/tests/selenium/selenium-lib/core/scripts/injection.html +++ b/tests/selenium/selenium-lib/core/scripts/injection.html @@ -46,7 +46,7 @@ function selenium_frameRunTest(oldOnLoadRoutine) { }
function seleniumOnLoad() {
- injectedSessionId = @SESSION_ID@;
+ injectedSessionId = "@SESSION_ID@";
window["selenium_has_been_loaded_into_this_window"] = true;
runSeleniumTest();
}
@@ -66,7 +66,7 @@ else { throw "causing a JavaScript error to tell the world that I did not arrange to be run on load";
}
-injectedSessionId = @SESSION_ID@;
+injectedSessionId = "@SESSION_ID@";
proxyInjectionMode = true;
}
</script>
diff --git a/tests/selenium/selenium-lib/core/scripts/js2html.js b/tests/selenium/selenium-lib/core/scripts/js2html.js deleted file mode 100644 index a384dce3..00000000 --- a/tests/selenium/selenium-lib/core/scripts/js2html.js +++ /dev/null @@ -1,70 +0,0 @@ -/*
-
-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/selenium/selenium-lib/core/scripts/narcissus-defs.js b/tests/selenium/selenium-lib/core/scripts/narcissus-defs.js deleted file mode 100644 index 5869397d..00000000 --- a/tests/selenium/selenium-lib/core/scripts/narcissus-defs.js +++ /dev/null @@ -1,175 +0,0 @@ -/* ***** 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 <[email protected]>. - * 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/selenium/selenium-lib/core/scripts/narcissus-exec.js b/tests/selenium/selenium-lib/core/scripts/narcissus-exec.js deleted file mode 100644 index e2c88f81..00000000 --- a/tests/selenium/selenium-lib/core/scripts/narcissus-exec.js +++ /dev/null @@ -1,1054 +0,0 @@ -/* ***** 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 <[email protected]>. - * 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/selenium/selenium-lib/core/scripts/narcissus-parse.js b/tests/selenium/selenium-lib/core/scripts/narcissus-parse.js deleted file mode 100644 index d6acb836..00000000 --- a/tests/selenium/selenium-lib/core/scripts/narcissus-parse.js +++ /dev/null @@ -1,1003 +0,0 @@ -/* ***** 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 <[email protected]>. - * 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/selenium/selenium-lib/core/scripts/se2html.js b/tests/selenium/selenium-lib/core/scripts/se2html.js deleted file mode 100644 index 67054a49..00000000 --- a/tests/selenium/selenium-lib/core/scripts/se2html.js +++ /dev/null @@ -1,63 +0,0 @@ -/*
-
-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/selenium/selenium-lib/core/scripts/selenium-api.js b/tests/selenium/selenium-lib/core/scripts/selenium-api.js index da21679b..30954010 100644 --- a/tests/selenium/selenium-lib/core/scripts/selenium-api.js +++ b/tests/selenium/selenium-lib/core/scripts/selenium-api.js @@ -98,8 +98,18 @@ function Selenium(browserbot) { * </ul>
* <p>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). </p>
* </li>
- * </ul>
*
+ * <li><strong>ui</strong>=<em>uiSpecifierString</em>:
+ * Locate an element by resolving the UI specifier string to another locator, and evaluating it. See the <a href="http://svn.openqa.org/fisheye/browse/~raw,r=trunk/selenium/trunk/src/main/resources/core/scripts/ui-doc.html">Selenium UI-Element Reference</a> for more details.
+ * <ul class="first last simple">
+ * <li>ui=loginPages::loginButton()</li>
+ * <li>ui=settingsPages::toggle(label=Hide Email)</li>
+ * <li>ui=forumPages::postBody(index=2)//a[2]</li>
+ * </ul>
+ * </li>
+ *
+ * </ul>
+ *
* <p>
* Without an explicit locator prefix, Selenium uses the following default
* strategies:
@@ -142,6 +152,8 @@ function Selenium(browserbot) { * <li><strong>regexp:</strong><em>regexp</em>:
* Match a string using a regular-expression. The full power of JavaScript
* regular-expressions is available.</li>
+ * <li><strong>regexpi:</strong><em>regexpi</em>:
+ * Match a string using a case-insensitive regular-expression.</li>
* <li><strong>exact:</strong><em>string</em>:
*
* Match a string exactly, verbatim, without any of that fancy wildcard
@@ -151,6 +163,14 @@ function Selenium(browserbot) { * If no pattern prefix is specified, Selenium assumes that it's a "glob"
* pattern.
* </p>
+ * <p>
+ * For commands that return multiple values (such as verifySelectOptions),
+ * the string being matched is a comma-separated list of the return values,
+ * where both commas and backslashes in the values are backslash-escaped.
+ * When providing a pattern, the optional matching syntax (i.e. glob,
+ * regexp, etc.) is specified once, as usual, at the beginning of the
+ * pattern.
+ * </p>
*/
this.browserbot = browserbot;
this.optionLocatorFactory = new OptionLocatorFactory();
@@ -159,25 +179,23 @@ function Selenium(browserbot) { return browserbot;
};
this.defaultTimeout = Selenium.DEFAULT_TIMEOUT;
- this.mouseSpeed = 10;
+ this.mouseSpeed = Selenium.DEFAULT_MOUSE_SPEED;
}
Selenium.DEFAULT_TIMEOUT = 30 * 1000;
Selenium.DEFAULT_MOUSE_SPEED = 10;
+Selenium.RIGHT_MOUSE_CLICK = 2;
Selenium.decorateFunctionWithTimeout = function(f, timeout) {
if (f == null) {
return null;
}
- var timeoutValue = parseInt(timeout);
- if (isNaN(timeoutValue)) {
- throw new SeleniumError("Timeout is not a number: '" + timeout + "'");
- }
- var now = new Date().getTime();
- var timeoutTime = now + timeoutValue;
+
+ var timeoutTime = getTimeoutTime(timeout);
+
return function() {
if (new Date().getTime() > timeoutTime) {
- throw new SeleniumError("Timed out after " + timeoutValue + "ms");
+ throw new SeleniumError("Timed out after " + timeout + "ms");
}
return f();
};
@@ -206,8 +224,56 @@ Selenium.prototype.doClick = function(locator) { * @param locator an element locator
*
*/
- var element = this.browserbot.findElement(locator);
- this.browserbot.clickElement(element);
+ var element = this.browserbot.findElement(locator);
+ var elementWithHref = getAncestorOrSelfWithJavascriptHref(element);
+
+ if (browserVersion.isChrome && elementWithHref != null) {
+ // SEL-621: Firefox chrome: Race condition bug in alert-handling code
+ //
+ // This appears to be because javascript href's are being executed in a
+ // separate thread from the main thread when running in chrome mode.
+ //
+ // This workaround injects a callback into the executing href that
+ // lowers a flag, which is initially raised. Execution of this click
+ // command will wait for the flag to be lowered.
+
+ var win = elementWithHref.ownerDocument.defaultView;
+ var originalLocation = win.location.href;
+ var originalHref = elementWithHref.href;
+
+ elementWithHref.href = 'javascript:try { '
+ + originalHref.replace(/^\s*javascript:/i, "")
+ + ' } finally { window._executingJavascriptHref = undefined; }' ;
+
+ win._executingJavascriptHref = true;
+
+ this.browserbot.clickElement(element);
+
+ return Selenium.decorateFunctionWithTimeout(function() {
+ if (win.closed) {
+ return true;
+ }
+ if (win.location.href != originalLocation) {
+ // navigated to some other page ... javascript from previous
+ // page can't still be executing!
+ return true;
+ }
+ if (! win._executingJavascriptHref) {
+ try {
+ elementWithHref.href = originalHref;
+ }
+ catch (e) {
+ // maybe the javascript removed the element ... should be
+ // no danger in not reverting its href attribute
+ }
+ return true;
+ }
+
+ return false;
+ }, this.defaultTimeout);
+ }
+
+ this.browserbot.clickElement(element);
};
Selenium.prototype.doDoubleClick = function(locator) {
@@ -223,6 +289,17 @@ Selenium.prototype.doDoubleClick = function(locator) { this.browserbot.doubleClickElement(element);
};
+Selenium.prototype.doContextMenu = function(locator) {
+ /**
+ * Simulates opening the context menu for the specified element (as might happen if the user "right-clicked" on the element).
+ *
+ * @param locator an element locator
+ *
+ */
+ var element = this.browserbot.findElement(locator);
+ this.browserbot.contextMenuOnElement(element);
+};
+
Selenium.prototype.doClickAt = function(locator, coordString) {
/**
* Clicks on a link, button, checkbox or radio button. If the click action
@@ -236,7 +313,10 @@ Selenium.prototype.doClickAt = function(locator, coordString) { */
var element = this.browserbot.findElement(locator);
var clientXY = getClientXY(element, coordString)
+ this.doMouseMove(locator);
+ this.doMouseDown(locator);
this.browserbot.clickElement(element, clientXY[0], clientXY[1]);
+ this.doMouseUp(locator);
};
Selenium.prototype.doDoubleClickAt = function(locator, coordString) {
@@ -252,7 +332,24 @@ Selenium.prototype.doDoubleClickAt = function(locator, coordString) { */
var element = this.browserbot.findElement(locator);
var clientXY = getClientXY(element, coordString)
+ this.doMouseMove(locator);
+ this.doMouseDown(locator);
this.browserbot.doubleClickElement(element, clientXY[0], clientXY[1]);
+ this.doMouseUp(locator);
+};
+
+Selenium.prototype.doContextMenuAt = function(locator, coordString) {
+ /**
+ * Simulates opening the context menu for the specified element (as might happen if the user "right-clicked" on the element).
+ *
+ * @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.browserbot.findElement(locator);
+ var clientXY = getClientXY(element, coordString)
+ this.browserbot.contextMenuOnElement(element, clientXY[0], clientXY[1]);
};
Selenium.prototype.doFireEvent = function(locator, eventName) {
@@ -267,6 +364,19 @@ Selenium.prototype.doFireEvent = function(locator, eventName) { triggerEvent(element, eventName, false);
};
+Selenium.prototype.doFocus = function(locator) {
+ /** Move the focus to the specified element; for example, if the element is an input field, move the cursor to that field.
+ *
+ * @param locator an <a href="#locators">element locator</a>
+ */
+ var element = this.browserbot.findElement(locator);
+ if (element.focus) {
+ element.focus();
+ } else {
+ triggerEvent(element, "focus", false);
+ }
+}
+
Selenium.prototype.doKeyPress = function(locator, keySequence) {
/**
* Simulates a user pressing and releasing a key.
@@ -423,7 +533,7 @@ Selenium.prototype.doMouseOut = function(locator) { Selenium.prototype.doMouseDown = function(locator) {
/**
- * Simulates a user pressing the mouse button (without releasing it yet) on
+ * Simulates a user pressing the left mouse button (without releasing it yet) on
* the specified element.
*
* @param locator an <a href="#locators">element locator</a>
@@ -432,9 +542,20 @@ Selenium.prototype.doMouseDown = function(locator) { this.browserbot.triggerMouseEvent(element, 'mousedown', true);
};
+Selenium.prototype.doMouseDownRight = function(locator) {
+ /**
+ * Simulates a user pressing the right mouse button (without releasing it yet) on
+ * the specified element.
+ *
+ * @param locator an <a href="#locators">element locator</a>
+ */
+ var element = this.browserbot.findElement(locator);
+ this.browserbot.triggerMouseEvent(element, 'mousedown', true, undefined, undefined, Selenium.RIGHT_MOUSE_CLICK);
+};
+
Selenium.prototype.doMouseDownAt = function(locator, coordString) {
/**
- * Simulates a user pressing the mouse button (without releasing it yet) at
+ * Simulates a user pressing the left mouse button (without releasing it yet) at
* the specified location.
*
* @param locator an <a href="#locators">element locator</a>
@@ -447,6 +568,21 @@ Selenium.prototype.doMouseDownAt = function(locator, coordString) { this.browserbot.triggerMouseEvent(element, 'mousedown', true, clientXY[0], clientXY[1]);
};
+Selenium.prototype.doMouseDownRightAt = function(locator, coordString) {
+ /**
+ * Simulates a user pressing the right mouse button (without releasing it yet) at
+ * the specified location.
+ *
+ * @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.browserbot.findElement(locator);
+ var clientXY = getClientXY(element, coordString)
+
+ this.browserbot.triggerMouseEvent(element, 'mousedown', true, clientXY[0], clientXY[1], Selenium.RIGHT_MOUSE_CLICK);
+};
+
Selenium.prototype.doMouseUp = function(locator) {
/**
* Simulates the event that occurs when the user releases the mouse button (i.e., stops
@@ -458,6 +594,17 @@ Selenium.prototype.doMouseUp = function(locator) { this.browserbot.triggerMouseEvent(element, 'mouseup', true);
};
+Selenium.prototype.doMouseUpRight = function(locator) {
+ /**
+ * Simulates the event that occurs when the user releases the right mouse button (i.e., stops
+ * holding the button down) on the specified element.
+ *
+ * @param locator an <a href="#locators">element locator</a>
+ */
+ var element = this.browserbot.findElement(locator);
+ this.browserbot.triggerMouseEvent(element, 'mouseup', true, undefined, undefined, Selenium.RIGHT_MOUSE_CLICK);
+};
+
Selenium.prototype.doMouseUpAt = function(locator, coordString) {
/**
* Simulates the event that occurs when the user releases the mouse button (i.e., stops
@@ -473,6 +620,21 @@ Selenium.prototype.doMouseUpAt = function(locator, coordString) { this.browserbot.triggerMouseEvent(element, 'mouseup', true, clientXY[0], clientXY[1]);
};
+Selenium.prototype.doMouseUpRightAt = function(locator, coordString) {
+ /**
+ * Simulates the event that occurs when the user releases the right mouse button (i.e., stops
+ * holding the button down) at the specified location.
+ *
+ * @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.browserbot.findElement(locator);
+ var clientXY = getClientXY(element, coordString)
+
+ this.browserbot.triggerMouseEvent(element, 'mouseup', true, clientXY[0], clientXY[1], Selenium.RIGHT_MOUSE_CLICK);
+};
+
Selenium.prototype.doMouseMove = function(locator) {
/**
* Simulates a user pressing the mouse button (without releasing it yet) on
@@ -557,12 +719,14 @@ Selenium.prototype.doSetSpeed = function(value) { throw new SeleniumError("this operation is only implemented in selenium-rc, and should never result in a request making it across the wire");
};
-Selenium.prototype.doGetSpeed = function() {
+Selenium.prototype.getSpeed = function() {
/**
* Get execution speed (i.e., get the millisecond length of the delay following each selenium operation). By default, there is no such delay, i.e.,
* the delay is 0 milliseconds.
*
* See also setSpeed.
+ *
+ * @return string the execution speed in milliseconds.
*/
throw new SeleniumError("this operation is only implemented in selenium-rc, and should never result in a request making it across the wire");
};
@@ -721,6 +885,10 @@ Selenium.prototype.makePageLoadCondition = function(timeout) { if (timeout == null) {
timeout = this.defaultTimeout;
}
+ // if the timeout is zero, we won't wait for the page to load before returning
+ if (timeout == 0) {
+ return;
+ }
return Selenium.decorateFunctionWithTimeout(fnBind(this._isNewPageLoaded, this), timeout);
};
@@ -763,27 +931,43 @@ Selenium.prototype.doOpenWindow = function(url, windowID) { Selenium.prototype.doSelectWindow = function(windowID) {
/**
- * Selects a popup window; once a popup window has been selected, all
+ * Selects a popup window using a window locator; once a popup window has been selected, all
* commands go to that window. To select the main window again, use null
* as the target.
*
- * <p>Note that there is a big difference between a window's internal JavaScript "name" property
- * and the "title" of a given window's document (which is normally what you actually see, as an end user,
- * in the title bar of the window). The "name" is normally invisible to the end-user; it's the second
- * parameter "windowName" passed to the JavaScript method window.open(url, windowName, windowFeatures, replaceFlag)
- * (which selenium intercepts).</p>
- *
- * <p>Selenium has several strategies for finding the window object referred to by the "windowID" parameter.</p>
+ * <p>
*
+ * Window locators provide different ways of specifying the window object:
+ * by title, by internal JavaScript "name," or by JavaScript variable.
+ * </p>
+ * <ul>
+ * <li><strong>title</strong>=<em>My Special Window</em>:
+ * Finds the window using the text that appears in the title bar. Be careful;
+ * two windows can share the same title. If that happens, this locator will
+ * just pick one.
+ * </li>
+ * <li><strong>name</strong>=<em>myWindow</em>:
+ * Finds the window using its internal JavaScript "name" property. This is the second
+ * parameter "windowName" passed to the JavaScript method window.open(url, windowName, windowFeatures, replaceFlag)
+ * (which Selenium intercepts).
+ * </li>
+ * <li><strong>var</strong>=<em>variableName</em>:
+ * Some pop-up windows are unnamed (anonymous), but are associated with a JavaScript variable name in the current
+ * application window, e.g. "window.foo = window.open(url);". In those cases, you can open the window using
+ * "var=foo".
+ * </li>
+ * </ul>
+ * <p>
+ * If no window locator prefix is provided, we'll try to guess what you mean like this:</p>
* <p>1.) if windowID is null, (or the string "null") then it is assumed the user is referring to the original window instantiated by the browser).</p>
* <p>2.) if the value of the "windowID" parameter is a JavaScript variable name in the current application window, then it is assumed
* that this variable contains the return value from a call to the JavaScript window.open() method.</p>
* <p>3.) Otherwise, selenium looks in a hash it maintains that maps string names to window "names".</p>
- * <p>4.) If <i>that</i> fails, we'll try looping over all of the known windows to try to find the appropriate "title".
+ * <p>4.) If <em>that</em> fails, we'll try looping over all of the known windows to try to find the appropriate "title".
* Since "title" is not necessarily unique, this may have unexpected behavior.</p>
- *
- * <p>If you're having trouble figuring out what is the name of a window that you want to manipulate, look at the selenium log messages
- * which identify the names of windows created via window.open (and therefore intercepted by selenium). You will see messages
+ *
+ * <p>If you're having trouble figuring out the name of a window that you want to manipulate, look at the Selenium log messages
+ * which identify the names of windows created via window.open (and therefore intercepted by Selenium). You will see messages
* like the following for each window as it is opened:</p>
*
* <p><code>debug: window.open call intercepted; window ID (which you can use with selectWindow()) is "myNewWindow"</code></p>
@@ -797,6 +981,39 @@ Selenium.prototype.doSelectWindow = function(windowID) { this.browserbot.selectWindow(windowID);
};
+Selenium.prototype.doSelectPopUp = function(windowID) {
+ /**
+ * Simplifies the process of selecting a popup window (and does not offer
+ * functionality beyond what <code>selectWindow()</code> already provides).
+ * <ul>
+ * <li>If <code>windowID</code> is either not specified, or specified as
+ * "null", the first non-top window is selected. The top window is the one
+ * that would be selected by <code>selectWindow()</code> without providing a
+ * <code>windowID</code> . This should not be used when more than one popup
+ * window is in play.</li>
+ * <li>Otherwise, the window will be looked up considering
+ * <code>windowID</code> as the following in order: 1) the "name" of the
+ * window, as specified to <code>window.open()</code>; 2) a javascript
+ * variable which is a reference to a window; and 3) the title of the
+ * window. This is the same ordered lookup performed by
+ * <code>selectWindow</code> .</li>
+ * </ul>
+ *
+ * @param windowID an identifier for the popup window, which can take on a
+ * number of different meanings
+ */
+ this.browserbot.selectPopUp(windowID);
+};
+
+Selenium.prototype.doDeselectPopUp = function() {
+ /**
+ * Selects the main window. Functionally equivalent to using
+ * <code>selectWindow()</code> and specifying no value for
+ * <code>windowID</code>.
+ */
+ this.browserbot.selectWindow();
+}
+
Selenium.prototype.doSelectFrame = function(locator) {
/**
* Selects a frame within the current window. (You may invoke this command
@@ -854,11 +1071,37 @@ Selenium.prototype.doWaitForPopUp = function(windowID, timeout) { /**
* Waits for a popup window to appear and load up.
*
- * @param windowID the JavaScript window ID of the window that will appear
- * @param timeout a timeout in milliseconds, after which the action will return with an error
+ * @param windowID the JavaScript window "name" of the window that will appear (not the text of the title bar)
+ * If unspecified, or specified as "null", this command will
+ * wait for the first non-top window to appear (don't rely
+ * on this if you are working with multiple popups
+ * simultaneously).
+ * @param timeout a timeout in milliseconds, after which the action will return with an error.
+ * If this value is not specified, the default Selenium
+ * timeout will be used. See the setTimeout() command.
*/
+ if (! timeout) {
+ var timeout = this.defaultTimeout;
+ }
+ var timeoutTime = getTimeoutTime(timeout);
+
var popupLoadedPredicate = function () {
- var targetWindow = selenium.browserbot.getWindowByName(windowID, true);
+ var targetWindow;
+ try {
+ if (windowID && windowID != 'null') {
+ targetWindow = selenium.browserbot.getWindowByName(windowID, true);
+ }
+ else {
+ var names = selenium.browserbot.getNonTopWindowNames();
+ targetWindow = selenium.browserbot.getWindowByName(names[0], true);
+ }
+ }
+ catch (e) {
+ if (new Date().getTime() > timeoutTime) {
+ throw e;
+ }
+ }
+
if (!targetWindow) return false;
if (!targetWindow.location) return false;
if ("about:blank" == targetWindow.location) return false;
@@ -892,6 +1135,7 @@ Selenium.prototype.doWaitForPopUp.dontCheckAlertsAndConfirms = true; Selenium.prototype.doChooseCancelOnNextConfirmation = function() {
/**
+ * <p>
* By default, Selenium's overridden window.confirm() function will
* return true, as if the user had manually clicked OK; after running
* this command, the next call to confirm() will return false, as if
@@ -899,13 +1143,19 @@ Selenium.prototype.doChooseCancelOnNextConfirmation = function() { * default behavior for future confirmations, automatically returning
* true (OK) unless/until you explicitly call this command for each
* confirmation.
- *
+ * </p>
+ * <p>
+ * Take note - every time a confirmation comes up, you must
+ * consume it with a corresponding getConfirmation, or else
+ * the next selenium operation will fail.
+ * </p>
*/
this.browserbot.cancelNextConfirmation(false);
};
Selenium.prototype.doChooseOkOnNextConfirmation = function() {
/**
+ * <p>
* Undo the effect of calling chooseCancelOnNextConfirmation. Note
* that Selenium's overridden window.confirm() function will normally automatically
* return true, as if the user had manually clicked OK, so you shouldn't
@@ -914,6 +1164,12 @@ Selenium.prototype.doChooseOkOnNextConfirmation = function() { * default behavior for future confirmations, automatically returning
* true (OK) unless/until you explicitly call chooseCancelOnNextConfirmation for each
* confirmation.
+ * </p>
+ * <p>
+ * Take note - every time a confirmation comes up, you must
+ * consume it with a corresponding getConfirmation, or else
+ * the next selenium operation will fail.
+ * </p>
*
*/
this.browserbot.cancelNextConfirmation(true);
@@ -1003,16 +1259,17 @@ 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
+ * alert is generated but you do not consume it with getAlert, the next Selenium action
* will fail.</p>
*
- * <p>NOTE: under Selenium, JavaScript alerts will NOT pop up a visible alert
+ * <p>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
+ * <p>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>
* @return string The message of the most recent JavaScript alert
+
*/
if (!this.browserbot.hasAlerts()) {
Assert.fail("There were no alerts");
@@ -1029,8 +1286,11 @@ Selenium.prototype.getConfirmation = function() { * <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.
+ * chooseCancelOnNextConfirmation command.
+ * </p>
+ * <p>
+ * If an confirmation is generated but you do not consume it with getConfirmation,
+ * the next Selenium action will fail.
* </p>
*
* <p>
@@ -1082,7 +1342,7 @@ Selenium.prototype.getLocation = function() { *
* @return string the absolute URL of the current page
*/
- return this.browserbot.getCurrentWindow().location;
+ return this.browserbot.getCurrentWindow().location.href;
};
Selenium.prototype.getTitle = function() {
@@ -1162,7 +1422,7 @@ Selenium.prototype.getEval = function(script) { if (null == result) return "null";
return result;
} catch (e) {
- throw new SeleniumError("Threw an exception: " + e.message);
+ throw new SeleniumError("Threw an exception: " + extractExceptionMessage(e));
}
};
@@ -1357,7 +1617,9 @@ Selenium.prototype.getSelectOptions = function(selectLocator) { Selenium.prototype.getAttribute = function(attributeLocator) {
/**
- * Gets the value of an element attribute.
+ * Gets the value of an element attribute. The value of the attribute may
+ * differ across browsers (this is the case for the "style" attribute, for
+ * example).
*
* @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
@@ -1417,6 +1679,18 @@ Selenium.prototype.isVisible = function(locator) { */
var element;
element = this.browserbot.findElement(locator);
+ // DGF if it's an input tag of type "hidden" then it's not visible
+ if (element.tagName) {
+ var tagName = new String(element.tagName).toLowerCase();
+ if (tagName == "input") {
+ if (element.type) {
+ var elementType = new String(element.type).toLowerCase();
+ if (elementType == "hidden") {
+ return false;
+ }
+ }
+ }
+ }
var visibility = this.findEffectiveStyleProperty(element, "visibility");
var _isDisplayed = this._isDisplayed(element);
return (visibility != "hidden" && _isDisplayed);
@@ -1444,9 +1718,10 @@ Selenium.prototype.findEffectiveStyle = function(element) { if (element.style == undefined) {
return undefined; // not a styled element
}
+ var window = this.browserbot.getCurrentWindow();
if (window.getComputedStyle) {
// DOM-Level-2-CSS
- return this.browserbot.getCurrentWindow().getComputedStyle(element, null);
+ return window.getComputedStyle(element, null);
}
if (element.currentStyle) {
// non-standard IE alternative
@@ -1455,6 +1730,12 @@ Selenium.prototype.findEffectiveStyle = function(element) { // currentStyle is not identical to getComputedStyle()
// ... but it's good enough for "visibility"
}
+
+ if (window.document.defaultView && window.document.defaultView.getComputedStyle) {
+ return window.document.defaultView.getComputedStyle(element, null);
+ }
+
+
throw new SeleniumError("cannot determine effective stylesheet in this browser");
};
@@ -1470,7 +1751,24 @@ Selenium.prototype.isEditable = function(locator) { if (element.value == undefined) {
Assert.fail("Element " + locator + " is not an input.");
}
- return !element.disabled;
+ if (element.disabled) {
+ return false;
+ }
+ // DGF "readonly" is a bit goofy... it doesn't necessarily have a value
+ // You can write <input readonly value="black">
+ var readOnlyNode = element.getAttributeNode('readonly');
+ if (readOnlyNode) {
+ // DGF on IE, every input element has a readOnly node, but it may be false
+ if (typeof(readOnlyNode.nodeValue) == "boolean") {
+ var readOnly = readOnlyNode.nodeValue;
+ if (readOnly) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ return true;
};
Selenium.prototype.getAllButtons = function() {
@@ -1504,7 +1802,7 @@ Selenium.prototype.getAllFields = function() { };
Selenium.prototype.getAttributeFromAllWindows = function(attributeName) {
- /** Returns every instance of some attribute from all known windows.
+ /** Returns an array of JavaScript property values from all known windows having one.
*
* @param attributeName name of an attribute on the windows
* @return string[] the set of values of this attribute from all known windows.
@@ -1578,7 +1876,13 @@ Selenium.prototype.doSetMouseSpeed = function(pixels) { * just send one "mousemove" at the start location and then one final one at the end location.</p>
* @param pixels the number of pixels between "mousemove" events
*/
- this.mouseSpeed = pixels;
+ var intValue = new Number(pixels);
+ if (intValue.constructor != Number ||
+ intValue < 0 ) {
+ this.mouseSpeed = Selenium.DEFAULT_MOUSE_SPEED;
+ } else {
+ this.mouseSpeed = pixels;
+ }
}
Selenium.prototype.getMouseSpeed = function() {
@@ -1586,7 +1890,7 @@ Selenium.prototype.getMouseSpeed = function() { *
* @return number the number of pixels between "mousemove" events during dragAndDrop commands (default=10)
*/
- this.mouseSpeed = pixels;
+ return this.mouseSpeed;
}
@@ -1674,25 +1978,25 @@ Selenium.prototype.doWindowMaximize = function() { };
Selenium.prototype.getAllWindowIds = function() {
- /** Returns the IDs of all windows that the browser knows about.
+ /** Returns the IDs of all windows that the browser knows about in an array.
*
- * @return string[] the IDs of all windows that the browser knows about.
+ * @return string[] Array of identifiers 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.
+ /** Returns the names of all windows that the browser knows about in an array.
*
- * @return string[] the names of all windows that the browser knows about.
+ * @return string[] Array of 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.
+ /** Returns the titles of all windows that the browser knows about in an array.
*
- * @return string[] the titles of all windows that the browser knows about.
+ * @return string[] Array of titles of all windows that the browser knows about.
*/
return this.getAttributeFromAllWindows("document.title");
};
@@ -2013,6 +2317,28 @@ Selenium.prototype.doAllowNativeXpath = function(allow) { this.browserbot.allowNativeXpath = allow;
}
+Selenium.prototype.doIgnoreAttributesWithoutValue = function(ignore) {
+ /**
+ * Specifies whether Selenium will ignore xpath attributes that have no
+ * value, i.e. are the empty string, when using the non-native xpath
+ * evaluation engine. You'd want to do this for performance reasons in IE.
+ * However, this could break certain xpaths, for example an xpath that looks
+ * for an attribute whose value is NOT the empty string.
+ *
+ * The hope is that such xpaths are relatively rare, but the user should
+ * have the option of using them. Note that this only influences xpath
+ * evaluation when using the ajaxslt engine (i.e. not "javascript-xpath").
+ *
+ * @param ignore boolean, true means we'll ignore attributes without value
+ * at the expense of xpath "correctness"; false means
+ * we'll sacrifice speed for correctness.
+ */
+ if ('false' == ignore || '0' == ignore) {
+ ignore = false;
+ }
+ this.browserbot.ignoreAttributesWithoutValue = ignore;
+}
+
Selenium.prototype.doWaitForCondition = function(script, timeout) {
/**
* Runs the specified JavaScript snippet repeatedly until it evaluates to "true".
@@ -2139,15 +2465,40 @@ Selenium.prototype.getCookie = function() { return doc.cookie;
};
+Selenium.prototype.getCookieByName = function(name) {
+ /**
+ * Returns the value of the cookie with the specified name, or throws an error if the cookie is not present.
+ * @param name the name of the cookie
+ * @return string the value of the cookie
+ */
+ var v = this.browserbot.getCookieByName(name);
+ if (v === null) {
+ throw new SeleniumError("Cookie '"+name+"' was not found");
+ }
+ return v;
+};
+
+Selenium.prototype.isCookiePresent = function(name) {
+ /**
+ * Returns true if a cookie with the specified name is present, or false otherwise.
+ * @param name the name of the cookie
+ * @return boolean true if a cookie with the specified name is present, or false otherwise.
+ */
+ var v = this.browserbot.getCookieByName(name);
+ var absent = (v === null);
+ return !absent;
+}
+
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.
+ * @param optionsString options for the cookie. Currently supported options include 'path', 'max_age' and 'domain'.
+ * the optionsString's format is "path=/path/, max_age=60, domain=.foo.com". The order of options are irrelevant, the unit
+ * of the value of 'max_age' is second. Note that specifying a domain that isn't a subset of the current domain will
+ * usually fail.
*/
var results = /[^\s=\[\]\(\),"\/\?@:;]+=[^\s=\[\]\(\),"\/\?@:;]*/.test(nameValuePair);
if (!results) {
@@ -2170,29 +2521,91 @@ Selenium.prototype.doCreateCookie = function(nameValuePair, optionsString) { }
cookie += "; path=" + path;
}
+ results = /domain=([^\s,]+)[,]?/.exec(optionsString);
+ if (results) {
+ var domain = results[1];
+ cookie += "; domain=" + domain;
+ }
LOG.debug("Setting cookie to: " + cookie);
this.browserbot.getDocument().cookie = cookie;
}
-Selenium.prototype.doDeleteCookie = function(name,path) {
+Selenium.prototype.doDeleteCookie = function(name,optionsString) {
/**
- * Delete a named cookie with specified path.
+ * Delete a named cookie with specified path and domain. Be careful; to delete a cookie, you
+ * need to delete it using the exact same path and domain that were used to create the cookie.
+ * If the path is wrong, or the domain is wrong, the cookie simply won't be deleted. Also
+ * note that specifying a domain that isn't a subset of the current domain will usually fail.
+ *
+ * Since there's no way to discover at runtime the original path and domain of a given cookie,
+ * we've added an option called 'recurse' to try all sub-domains of the current domain with
+ * all paths that are a subset of the current path. Beware; this option can be slow. In
+ * big-O notation, it operates in O(n*m) time, where n is the number of dots in the domain
+ * name and m is the number of slashes in the path.
*
* @param name the name of the cookie to be deleted
- * @param path the path property of the cookie to be deleted
+ * @param optionsString options for the cookie. Currently supported options include 'path', 'domain'
+ * and 'recurse.' The optionsString's format is "path=/path/, domain=.foo.com, recurse=true".
+ * The order of options are irrelevant. Note that specifying a domain that isn't a subset of
+ * the current domain will usually fail.
*/
// set the expire time of the cookie to be deleted to one minute before now.
- path = path.trim();
+ var path = "";
+ var domain = "";
+ var recurse = false;
+ var matched = false;
+ results = /path=([^\s,]+)[,]?/.exec(optionsString);
+ if (results) {
+ matched = true;
+ path = results[1];
+ }
+ results = /domain=([^\s,]+)[,]?/.exec(optionsString);
+ if (results) {
+ matched = true;
+ domain = results[1];
+ }
+ results = /recurse=([^\s,]+)[,]?/.exec(optionsString);
+ if (results) {
+ matched = true;
+ recurse = results[1];
+ if ("false" == recurse) {
+ recurse = false;
+ }
+ }
+ // Treat the entire optionsString as a path (for backwards compatibility)
+ if (optionsString && !matched) {
+ LOG.warn("Using entire optionsString as a path; please change the argument to deleteCookie to use path="+optionsString);
+ path = optionsString;
+ }
if (browserVersion.khtml) {
// Safari and conquerer don't like paths with / at the end
if ("/" != path) {
path = path.replace(/\/$/, "");
}
+ }
+ path = path.trim();
+ domain = domain.trim();
+ var cookieName = name.trim();
+ if (recurse) {
+ this.browserbot.recursivelyDeleteCookie(cookieName, domain, path);
+ } else {
+ this.browserbot.deleteCookie(cookieName, domain, path);
+ }
+}
+
+Selenium.prototype.doDeleteAllVisibleCookies = function() {
+ /** Calls deleteCookie with recurse=true on all cookies visible to the current page.
+ * As noted on the documentation for deleteCookie, recurse=true can be much slower
+ * than simply deleting the cookies using a known domain/path.
+ */
+ var win = this.browserbot.getCurrentWindow();
+ var doc = win.document;
+ var cookieNames = this.browserbot.getAllCookieNames(doc);
+ var domain = doc.domain;
+ var path = win.location.pathname;
+ for (var i = 0; i < cookieNames.length; i++) {
+ this.browserbot.recursivelyDeleteCookie(cookieNames[i], domain, path, win);
}
- var expireDateInMilliseconds = (new Date()).getTime() + (-1 * 1000);
- var cookie = name.trim() + "=deleted; path=" + path + "; expires=" + new Date(expireDateInMilliseconds).toGMTString();
- LOG.debug("Setting cookie to: " + cookie);
- this.browserbot.getDocument().cookie = cookie;
}
Selenium.prototype.doSetBrowserLogLevel = function(logLevel) {
@@ -2274,6 +2687,423 @@ Selenium.prototype.doAddLocationStrategy = function(strategyName, functionDefini this.browserbot.locationStrategies[strategyName] = safeStrategyFunction;
}
+Selenium.prototype.doCaptureEntirePageScreenshot = function(filename, kwargs) {
+ /**
+ * Saves the entire contents of the current window canvas to a PNG file.
+ * Contrast this with the captureScreenshot command, which captures the
+ * contents of the OS viewport (i.e. whatever is currently being displayed
+ * on the monitor), and is implemented in the RC only. Currently this only
+ * works in Firefox when running in chrome mode, and in IE non-HTA using
+ * the EXPERIMENTAL "Snapsie" utility. The Firefox implementation is mostly
+ * borrowed from the Screengrab! Firefox extension. Please see
+ * http://www.screengrab.org and http://snapsie.sourceforge.net/ for
+ * details.
+ *
+ * @param filename the path to the file to persist the screenshot as. No
+ * filename extension will be appended by default.
+ * Directories will not be created if they do not exist,
+ * and an exception will be thrown, possibly by native
+ * code.
+ * @param kwargs a kwargs string that modifies the way the screenshot
+ * is captured. Example: "background=#CCFFDD" .
+ * Currently valid options:
+ * <dl>
+ * <dt>background</dt>
+ * <dd>the background CSS for the HTML document. This
+ * may be useful to set for capturing screenshots of
+ * less-than-ideal layouts, for example where absolute
+ * positioning causes the calculation of the canvas
+ * dimension to fail and a black background is exposed
+ * (possibly obscuring black text).</dd>
+ * </dl>
+ */
+ if (! browserVersion.isChrome &&
+ ! (browserVersion.isIE && ! browserVersion.isHTA)) {
+ throw new SeleniumError('captureEntirePageScreenshot is only '
+ + 'implemented for Firefox ("firefox" or "chrome", NOT '
+ + '"firefoxproxy") and IE non-HTA ("iexploreproxy", NOT "iexplore" '
+ + 'or "iehta"). The current browser isn\'t one of them!');
+ }
+
+ // do or do not ... there is no try
+
+ if (browserVersion.isIE) {
+ // targeting snapsIE >= 0.2
+ function getFailureMessage(exceptionMessage) {
+ var msg = 'Snapsie failed: ';
+ if (exceptionMessage) {
+ if (exceptionMessage ==
+ "Automation server can't create object") {
+ msg += 'Is it installed? Does it have permission to run '
+ + 'as an add-on? See http://snapsie.sourceforge.net/';
+ }
+ else {
+ msg += exceptionMessage;
+ }
+ }
+ else {
+ msg += 'Undocumented error';
+ }
+ return msg;
+ }
+
+ if (typeof(runOptions) != 'undefined' &&
+ runOptions.isMultiWindowMode() == false) {
+ // framed mode
+ try {
+ new Snapsie().saveSnapshot(filename, 'selenium_myiframe');
+ }
+ catch (e) {
+ throw new SeleniumError(getFailureMessage(e.message));
+ }
+ }
+ else {
+ // multi-window mode
+ if (!this.snapsieSrc) {
+ // XXX - cache snapsie, and capture the screenshot as a
+ // callback. Definitely a hack, because we may be late taking
+ // the first screenshot, but saves us from polluting other code
+ // for now. I wish there were an easier way to get at the
+ // contents of a referenced script!
+ var snapsieUrl = (this.browserbot.buttonWindow.location.href)
+ .replace(/(Test|Remote)Runner\.html/, 'lib/snapsie.js');
+ var self = this;
+ new Ajax.Request(snapsieUrl, {
+ method: 'get'
+ , onSuccess: function(transport) {
+ self.snapsieSrc = transport.responseText;
+ self.doCaptureEntirePageScreenshot(filename, kwargs);
+ }
+ });
+ return;
+ }
+
+ // it's going into a string, so escape the backslashes
+ filename = filename.replace(/\\/g, '\\\\');
+
+ // this is sort of hackish. We insert a script into the document,
+ // and remove it before anyone notices.
+ var doc = selenium.browserbot.getDocument();
+ var script = doc.createElement('script');
+ var scriptContent = this.snapsieSrc
+ + 'try {'
+ + ' new Snapsie().saveSnapshot("' + filename + '");'
+ + '}'
+ + 'catch (e) {'
+ + ' document.getElementById("takeScreenshot").failure ='
+ + ' e.message;'
+ + '}';
+ script.id = 'takeScreenshot';
+ script.language = 'javascript';
+ script.text = scriptContent;
+ doc.body.appendChild(script);
+ script.parentNode.removeChild(script);
+ if (script.failure) {
+ throw new SeleniumError(getFailureMessage(script.failure));
+ }
+ }
+ return;
+ }
+
+ var grabber = {
+ prepareCanvas: function(width, height) {
+ var styleWidth = width + 'px';
+ var styleHeight = height + 'px';
+
+ var grabCanvas = document.getElementById('screenshot_canvas');
+ if (!grabCanvas) {
+ // create the canvas
+ var ns = 'http://www.w3.org/1999/xhtml';
+ grabCanvas = document.createElementNS(ns, 'html:canvas');
+ grabCanvas.id = 'screenshot_canvas';
+ grabCanvas.style.display = 'none';
+ document.documentElement.appendChild(grabCanvas);
+ }
+
+ grabCanvas.width = width;
+ grabCanvas.style.width = styleWidth;
+ grabCanvas.style.maxWidth = styleWidth;
+ grabCanvas.height = height;
+ grabCanvas.style.height = styleHeight;
+ grabCanvas.style.maxHeight = styleHeight;
+
+ return grabCanvas;
+ },
+
+ prepareContext: function(canvas, box) {
+ var context = canvas.getContext('2d');
+ context.clearRect(box.x, box.y, box.width, box.height);
+ context.save();
+ return context;
+ }
+ };
+
+ var SGNsUtils = {
+ dataUrlToBinaryInputStream: function(dataUrl) {
+ var nsIoService = Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService);
+ var channel = nsIoService
+ .newChannelFromURI(nsIoService.newURI(dataUrl, null, null));
+ var binaryInputStream = Components.classes["@mozilla.org/binaryinputstream;1"]
+ .createInstance(Components.interfaces.nsIBinaryInputStream);
+
+ binaryInputStream.setInputStream(channel.open());
+ return binaryInputStream;
+ },
+
+ newFileOutputStream: function(nsFile) {
+ var writeFlag = 0x02; // write only
+ var createFlag = 0x08; // create
+ var truncateFlag = 0x20; // truncate
+ var fileOutputStream = Components.classes["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Components.interfaces.nsIFileOutputStream);
+
+ fileOutputStream.init(nsFile,
+ writeFlag | createFlag | truncateFlag, 0664, null);
+ return fileOutputStream;
+ },
+
+ writeBinaryInputStreamToFileOutputStream:
+ function(binaryInputStream, fileOutputStream) {
+ var numBytes = binaryInputStream.available();
+ var bytes = binaryInputStream.readBytes(numBytes);
+ fileOutputStream.write(bytes, numBytes);
+ }
+ };
+
+ // compute dimensions
+ var window = this.browserbot.getCurrentWindow();
+ var doc = window.document.documentElement;
+ var box = {
+ x: 0,
+ y: 0,
+ width: doc.scrollWidth,
+ height: doc.scrollHeight
+ };
+ LOG.debug('computed dimensions');
+
+ var originalBackground = doc.style.background;
+
+ if (kwargs) {
+ var args = parse_kwargs(kwargs);
+ if (args.background) {
+ doc.style.background = args.background;
+ }
+ }
+
+ // grab
+ var format = 'png';
+ var canvas = grabber.prepareCanvas(box.width, box.height);
+ var context = grabber.prepareContext(canvas, box);
+ context.drawWindow(window, box.x, box.y, box.width, box.height,
+ 'rgb(0, 0, 0)');
+ context.restore();
+ var dataUrl = canvas.toDataURL("image/" + format);
+ LOG.debug('grabbed to canvas');
+
+ doc.style.background = originalBackground;
+
+ // save to file
+ var nsFile = Components.classes["@mozilla.org/file/local;1"]
+ .createInstance(Components.interfaces.nsILocalFile);
+ try {
+ nsFile.initWithPath(filename);
+ }
+ catch (e) {
+ if (/NS_ERROR_FILE_UNRECOGNIZED_PATH/.test(e.message)) {
+ // try using the opposite file separator
+ if (filename.indexOf('/') != -1) {
+ filename = filename.replace(/\//g, '\\');
+ }
+ else {
+ filename = filename.replace(/\\/g, '/');
+ }
+ nsFile.initWithPath(filename);
+ }
+ else {
+ throw e;
+ }
+ }
+ var binaryInputStream = SGNsUtils.dataUrlToBinaryInputStream(dataUrl);
+ var fileOutputStream = SGNsUtils.newFileOutputStream(nsFile);
+ SGNsUtils.writeBinaryInputStreamToFileOutputStream(binaryInputStream,
+ fileOutputStream);
+ fileOutputStream.close();
+ LOG.debug('saved to file');
+};
+
+Selenium.prototype.doRollup = function(rollupName, kwargs) {
+ /**
+ * Executes a command rollup, which is a series of commands with a unique
+ * name, and optionally arguments that control the generation of the set of
+ * commands. If any one of the rolled-up commands fails, the rollup is
+ * considered to have failed. Rollups may also contain nested rollups.
+ *
+ * @param rollupName the name of the rollup command
+ * @param kwargs keyword arguments string that influences how the
+ * rollup expands into commands
+ */
+ // we have to temporarily hijack the commandStarted, nextCommand(),
+ // commandComplete(), and commandError() methods of the TestLoop object.
+ // When the expanded rollup commands are done executing (or an error has
+ // occurred), we'll restore them to their original values.
+ var loop = currentTest || htmlTestRunner.currentTest;
+ var backupManager = {
+ backup: function() {
+ for (var item in this.data) {
+ this.data[item] = loop[item];
+ }
+ }
+ , restore: function() {
+ for (var item in this.data) {
+ loop[item] = this.data[item];
+ }
+ }
+ , data: {
+ requiresCallBack: null
+ , commandStarted: null
+ , nextCommand: null
+ , commandComplete: null
+ , commandError: null
+ , pendingRollupCommands: null
+ , rollupFailed: null
+ , rollupFailedMessage: null
+ }
+ };
+
+ var rule = RollupManager.getInstance().getRollupRule(rollupName);
+ var expandedCommands = rule.getExpandedCommands(kwargs);
+
+ // hold your breath ...
+ try {
+ backupManager.backup();
+ loop.requiresCallBack = false;
+ loop.commandStarted = function() {};
+ loop.nextCommand = function() {
+ if (this.pendingRollupCommands.length == 0) {
+ return null;
+ }
+ var command = this.pendingRollupCommands.shift();
+ return command;
+ };
+ loop.commandComplete = function(result) {
+ if (result.failed) {
+ this.rollupFailed = true;
+ this.rollupFailureMessages.push(result.failureMessage);
+ }
+
+ if (this.pendingRollupCommands.length == 0) {
+ result = {
+ failed: this.rollupFailed
+ , failureMessage: this.rollupFailureMessages.join('; ')
+ };
+ LOG.info('Rollup execution complete: ' + (result.failed
+ ? 'failed! (see error messages below)' : 'ok'));
+ backupManager.restore();
+ this.commandComplete(result);
+ }
+ };
+ loop.commandError = function(errorMessage) {
+ LOG.info('Rollup execution complete: bombed!');
+ backupManager.restore();
+ this.commandError(errorMessage);
+ };
+
+ loop.pendingRollupCommands = expandedCommands;
+ loop.rollupFailed = false;
+ loop.rollupFailureMessages = [];
+ }
+ catch (e) {
+ LOG.error('Rollup error: ' + e);
+ backupManager.restore();
+ }
+};
+
+Selenium.prototype.doAddScript = function(scriptContent, scriptTagId) {
+ /**
+ * Loads script content into a new script tag in the Selenium document. This
+ * differs from the runScript command in that runScript adds the script tag
+ * to the document of the AUT, not the Selenium document. The following
+ * entities in the script content are replaced by the characters they
+ * represent:
+ *
+ * <
+ * >
+ * &
+ *
+ * The corresponding remove command is removeScript.
+ *
+ * @param scriptContent the Javascript content of the script to add
+ * @param scriptTagId (optional) the id of the new script tag. If
+ * specified, and an element with this id already
+ * exists, this operation will fail.
+ */
+ if (scriptTagId && document.getElementById(scriptTagId)) {
+ var msg = "Element with id '" + scriptTagId + "' already exists!";
+ throw new SeleniumError(msg);
+ }
+
+ var head = document.getElementsByTagName('head')[0];
+ var script = document.createElement('script');
+
+ script.type = 'text/javascript';
+
+ if (scriptTagId) {
+ script.id = scriptTagId;
+ }
+
+ // replace some entities
+ scriptContent = scriptContent
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/&/g, '&');
+
+ script.text = scriptContent;
+ head.appendChild(script);
+};
+
+Selenium.prototype.doRemoveScript = function(scriptTagId) {
+ /**
+ * Removes a script tag from the Selenium document identified by the given
+ * id. Does nothing if the referenced tag doesn't exist.
+ *
+ * @param scriptTagId the id of the script element to remove.
+ */
+ var script = document.getElementById(scriptTagId);
+
+ if (script && getTagName(script) == 'script') {
+ script.parentNode.removeChild(script);
+ }
+};
+
+Selenium.prototype.doUseXpathLibrary = function(libraryName) {
+ /**
+ * Allows choice of one of the available libraries.
+ * @param libraryName name of the desired library
+ * Only the following three can be chosen:
+ * <ul>
+ * <li>"ajaxslt" - Google's library</li>
+ * <li>"javascript-xpath" - Cybozu Labs' faster library</li>
+ * <li>"default" - The default library. Currently the default library is "ajaxslt" .</li>
+ * </ul>
+ * If libraryName isn't one of these three, then
+ * no change will be made.
+ *
+ */
+
+ if (libraryName == "default") {
+ this.browserbot.xpathLibrary = this.browserbot.defaultXpathLibrary;
+ return;
+ }
+
+ if ((libraryName != 'ajaxslt') && (libraryName != 'javascript-xpath')) {
+ return;
+ }
+
+ this.browserbot.xpathLibrary = libraryName;
+
+};
+
/**
* Factory for creating "Option Locators".
* An OptionLocator is an object for dealing with Select options (e.g. for
@@ -2304,7 +3134,7 @@ OptionLocatorFactory.prototype.fromLocatorString = function(locatorString) { if (this.optionLocators[locatorType]) {
return new this.optionLocators[locatorType](locatorValue);
}
- throw new SeleniumError("Unkown option locator type: " + locatorType);
+ throw new SeleniumError("Unknown option locator type: " + locatorType);
};
/**
@@ -2407,3 +3237,4 @@ OptionLocatorFactory.prototype.OptionLocatorById = function(id) { Assert.matches(this.id, selectedId)
};
};
+
diff --git a/tests/selenium/selenium-lib/core/scripts/selenium-browserbot.js b/tests/selenium/selenium-lib/core/scripts/selenium-browserbot.js index 54a9d725..0a835a0a 100644 --- a/tests/selenium/selenium-lib/core/scripts/selenium-browserbot.js +++ b/tests/selenium/selenium-lib/core/scripts/selenium-browserbot.js @@ -37,6 +37,7 @@ var BrowserBot = function(topLevelApplicationWindow) { this.currentWindow = this.topWindow;
this.currentWindowName = null;
this.allowNativeXpath = true;
+ this.xpathLibrary = this.defaultXpathLibrary = 'ajaxslt' // change to "javascript-xpath" for the newer, faster engine
// We need to know this in advance, in case the frame closes unexpectedly
this.isSubFrameSelected = false;
@@ -176,6 +177,9 @@ BrowserBot.prototype.resetPopups = function() { BrowserBot.prototype.getNextAlert = function() {
var t = this.recordedAlerts.shift();
+ if (t) {
+ t = t.replace(/\n/g, " "); // because Selenese loses \n's when retrieving text from HTML table
+ }
this.relayBotToRC("browserbot.recordedAlerts");
return t;
};
@@ -202,7 +206,7 @@ BrowserBot.prototype.getNextPrompt = function() { /* Fire a mouse event in a browser-compatible manner */
-BrowserBot.prototype.triggerMouseEvent = function(element, eventType, canBubble, clientX, clientY) {
+BrowserBot.prototype.triggerMouseEvent = function(element, eventType, canBubble, clientX, clientY, button) {
clientX = clientX ? clientX : 0;
clientY = clientY ? clientY : 0;
@@ -211,10 +215,10 @@ BrowserBot.prototype.triggerMouseEvent = function(element, eventType, canBubble, var screenY = 0;
canBubble = (typeof(canBubble) == undefined) ? true : canBubble;
- if (element.fireEvent) {
+ if (element.fireEvent && element.ownerDocument && element.ownerDocument.createEventObject) { //IE
var evt = createEventObject(element, this.controlKeyDown, this.altKeyDown, this.shiftKeyDown, this.metaKeyDown);
evt.detail = 0;
- evt.button = 1;
+ evt.button = button ? button : 1; // default will be the left mouse click ( http://www.javascriptkit.com/jsref/event.shtml )
evt.relatedTarget = null;
if (!screenX && !screenY && !clientX && !clientY && !this.controlKeyDown && !this.altKeyDown && !this.shiftKeyDown && !this.metaKeyDown) {
element.fireEvent('on' + eventType);
@@ -247,9 +251,11 @@ BrowserBot.prototype.triggerMouseEvent = function(element, eventType, canBubble, var evt = document.createEvent('MouseEvents');
if (evt.initMouseEvent)
{
+ // see http://developer.mozilla.org/en/docs/DOM:event.button and
+ // http://developer.mozilla.org/en/docs/DOM:event.initMouseEvent for button ternary logic logic
//Safari
evt.initMouseEvent(eventType, canBubble, true, document.defaultView, 1, screenX, screenY, clientX, clientY,
- this.controlKeyDown, this.altKeyDown, this.shiftKeyDown, this.metaKeyDown, 0, null);
+ this.controlKeyDown, this.altKeyDown, this.shiftKeyDown, this.metaKeyDown, button ? button : 0, null);
}
else {
LOG.warn("element doesn't have initMouseEvent; firing an event which should -- but doesn't -- have other mouse-event related attributes here, as well as controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown");
@@ -259,7 +265,10 @@ BrowserBot.prototype.triggerMouseEvent = function(element, eventType, canBubble, evt.metaKey = this.metaKeyDown;
evt.altKey = this.altKeyDown;
evt.ctrlKey = this.controlKeyDown;
-
+ if(button)
+ {
+ evt.button = button;
+ }
}
element.dispatchEvent(evt);
}
@@ -301,16 +310,36 @@ BrowserBot.prototype._modifyWindow = function(win) { };
BrowserBot.prototype.selectWindow = function(target) {
- // TODO implement a locator syntax here
- if (target && target != "null") {
- try {
- this._selectWindowByName(target);
- }
- catch (e) {
- this._selectWindowByTitle(target);
- }
- } else {
+ if (!target || target == "null") {
this._selectTopWindow();
+ return;
+ }
+ var result = target.match(/^([a-zA-Z]+)=(.*)/);
+ if (!result) {
+ this._selectWindowByWindowId(target);
+ return;
+ }
+ locatorType = result[1];
+ locatorValue = result[2];
+ if (locatorType == "title") {
+ this._selectWindowByTitle(locatorValue);
+ }
+ // TODO separate name and var into separate functions
+ else if (locatorType == "name") {
+ this._selectWindowByName(locatorValue);
+ } else if (locatorType == "var") {
+ this._selectWindowByName(locatorValue);
+ } else {
+ throw new SeleniumError("Window locator not recognized: " + locatorType);
+ }
+};
+
+BrowserBot.prototype.selectPopUp = function(windowId) {
+ if (! windowId || windowId == 'null') {
+ this._selectFirstNonTopWindow();
+ }
+ else {
+ this._selectWindowByWindowId(windowId);
}
};
@@ -321,6 +350,15 @@ BrowserBot.prototype._selectTopWindow = function() { this.isSubFrameSelected = false;
}
+BrowserBot.prototype._selectWindowByWindowId = function(windowId) {
+ try {
+ this._selectWindowByName(windowId);
+ }
+ catch (e) {
+ this._selectWindowByTitle(windowId);
+ }
+};
+
BrowserBot.prototype._selectWindowByName = function(target) {
this.currentWindow = this.getWindowByName(target, false);
this.topFrame = this.currentWindow;
@@ -337,20 +375,27 @@ BrowserBot.prototype._selectWindowByTitle = function(target) { }
}
+BrowserBot.prototype._selectFirstNonTopWindow = function() {
+ var names = this.getNonTopWindowNames();
+ if (names.length) {
+ this._selectWindowByName(names[0]);
+ }
+};
+
BrowserBot.prototype.selectFrame = function(target) {
if (target.indexOf("index=") == 0) {
target = target.substr(6);
var frame = this.getCurrentWindow().frames[target];
if (frame == null) {
- throw new SeleniumError("Not found: frames["+index+"]");
+ throw new SeleniumError("Not found: frames["+target+"]");
}
if (!frame.document) {
- throw new SeleniumError("frames["+index+"] is not a frame");
+ throw new SeleniumError("frames["+target+"] is not a frame");
}
this.currentWindow = frame;
this.isSubFrameSelected = true;
}
- else if (target == "relative=up") {
+ else if (target == "relative=up" || target == "relative=parent") {
this.currentWindow = this.getCurrentWindow().parent;
this.isSubFrameSelected = (this._getFrameElement(this.currentWindow) != null);
} else if (target == "relative=top") {
@@ -513,6 +558,8 @@ BrowserBot.prototype.getCurrentPage = function() { BrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToModify, browserBot) {
var self = this;
+ windowToModify.seleniumAlert = windowToModify.alert;
+
windowToModify.alert = function(alert) {
browserBot.recordedAlerts.push(alert);
self.relayBotToRC.call(self, "browserbot.recordedAlerts");
@@ -551,6 +598,10 @@ BrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToModify, if (isHTA) {
myOriginalOpen = this[originalOpenReference];
}
+ if (windowName == "" || windowName == "_blank") {
+ windowName = "selenium_blank" + Math.round(100000 * Math.random());
+ LOG.warn("Opening window '_blank', which is not a real window name. Randomizing target to be: " + windowName);
+ }
var openedWindow = myOriginalOpen(url, windowName, windowFeatures, replaceFlag);
LOG.debug("window.open call intercepted; window ID (which you can use with selectWindow()) is \"" + windowName + "\"");
if (windowName!=null) {
@@ -915,7 +966,7 @@ BrowserBot.prototype.getWindowByName = function(windowName, doNotModify) { }
}
if (!targetWindow) {
- throw new SeleniumError("Window does not exist");
+ throw new SeleniumError("Window does not exist. If this looks like a Selenium bug, make sure to read http://selenium-core.openqa.org/reference.html#openWindow for potential workarounds.");
}
if (browserVersion.isHTA) {
try {
@@ -964,6 +1015,19 @@ BrowserBot.prototype.getWindowNameByTitle = function(windowTitle) { throw new SeleniumError("Could not find window with title " + windowTitle);
};
+BrowserBot.prototype.getNonTopWindowNames = function() {
+ var nonTopWindowNames = [];
+
+ for (var windowName in this.openedWindows) {
+ var win = this.openedWindows[windowName];
+ if (! this._windowClosed(win) && win != this.topWindow) {
+ nonTopWindowNames.push(windowName);
+ }
+ }
+
+ return nonTopWindowNames;
+};
+
BrowserBot.prototype.getCurrentWindow = function(doNotModify) {
if (this.proxyInjectionMode) {
return window;
@@ -978,6 +1042,21 @@ BrowserBot.prototype.getCurrentWindow = function(doNotModify) { return testWindow;
};
+/**
+ * Offer a method the end-user can reliably use to retrieve the current window.
+ * This should work even for windows with an XPCNativeWrapper. Returns the
+ * current window object.
+ */
+BrowserBot.prototype.getUserWindow = function() {
+ var userWindow = this.getCurrentWindow(true);
+
+ if (userWindow.wrappedJSObject) {
+ userWindow = userWindow.wrappedJSObject;
+ }
+
+ return userWindow;
+};
+
BrowserBot.prototype._handleClosedSubFrame = function(testWindow, doNotModify) {
if (this.proxyInjectionMode) {
return testWindow;
@@ -1077,6 +1156,103 @@ BrowserBot.prototype.getTitle = function() { return t;
}
+BrowserBot.prototype.getCookieByName = function(cookieName, doc) {
+ if (!doc) doc = this.getDocument();
+ var ck = doc.cookie;
+ if (!ck) return null;
+ var ckPairs = ck.split(/;/);
+ for (var i = 0; i < ckPairs.length; i++) {
+ var ckPair = ckPairs[i].trim();
+ var ckNameValue = ckPair.split(/=/);
+ var ckName = decodeURIComponent(ckNameValue[0]);
+ if (ckName === cookieName) {
+ return decodeURIComponent(ckNameValue[1]);
+ }
+ }
+ return null;
+}
+
+BrowserBot.prototype.getAllCookieNames = function(doc) {
+ if (!doc) doc = this.getDocument();
+ var ck = doc.cookie;
+ if (!ck) return [];
+ var cookieNames = [];
+ var ckPairs = ck.split(/;/);
+ for (var i = 0; i < ckPairs.length; i++) {
+ var ckPair = ckPairs[i].trim();
+ var ckNameValue = ckPair.split(/=/);
+ var ckName = decodeURIComponent(ckNameValue[0]);
+ cookieNames.push(ckName);
+ }
+ return cookieNames;
+}
+
+BrowserBot.prototype.deleteCookie = function(cookieName, domain, path, doc) {
+ if (!doc) doc = this.getDocument();
+ var expireDateInMilliseconds = (new Date()).getTime() + (-1 * 1000);
+ var cookie = cookieName + "=deleted; ";
+ if (path) {
+ cookie += "path=" + path + "; ";
+ }
+ if (domain) {
+ cookie += "domain=" + domain + "; ";
+ }
+ cookie += "expires=" + new Date(expireDateInMilliseconds).toGMTString();
+ LOG.debug("Setting cookie to: " + cookie);
+ doc.cookie = cookie;
+}
+
+/** Try to delete cookie, return false if it didn't work */
+BrowserBot.prototype._maybeDeleteCookie = function(cookieName, domain, path, doc) {
+ this.deleteCookie(cookieName, domain, path, doc);
+ return (!this.getCookieByName(cookieName, doc));
+}
+
+
+BrowserBot.prototype._recursivelyDeleteCookieDomains = function(cookieName, domain, path, doc) {
+ var deleted = this._maybeDeleteCookie(cookieName, domain, path, doc);
+ if (deleted) return true;
+ var dotIndex = domain.indexOf(".");
+ if (dotIndex == 0) {
+ return this._recursivelyDeleteCookieDomains(cookieName, domain.substring(1), path, doc);
+ } else if (dotIndex != -1) {
+ return this._recursivelyDeleteCookieDomains(cookieName, domain.substring(dotIndex), path, doc);
+ } else {
+ // No more dots; try just not passing in a domain at all
+ return this._maybeDeleteCookie(cookieName, null, path, doc);
+ }
+}
+
+BrowserBot.prototype._recursivelyDeleteCookie = function(cookieName, domain, path, doc) {
+ var slashIndex = path.lastIndexOf("/");
+ var finalIndex = path.length-1;
+ if (slashIndex == finalIndex) {
+ slashIndex--;
+ }
+ if (slashIndex != -1) {
+ deleted = this._recursivelyDeleteCookie(cookieName, domain, path.substring(0, slashIndex+1), doc);
+ if (deleted) return true;
+ }
+ return this._recursivelyDeleteCookieDomains(cookieName, domain, path, doc);
+}
+
+BrowserBot.prototype.recursivelyDeleteCookie = function(cookieName, domain, path, win) {
+ if (!win) win = this.getCurrentWindow();
+ var doc = win.document;
+ if (!domain) {
+ domain = doc.domain;
+ }
+ if (!path) {
+ path = win.location.pathname;
+ }
+ var deleted = this._recursivelyDeleteCookie(cookieName, "." + domain, path, doc);
+ if (deleted) return;
+ // Finally try a null path (Try it last because it's uncommon)
+ deleted = this._recursivelyDeleteCookieDomains(cookieName, "." + domain, null, doc);
+ if (deleted) return;
+ throw new SeleniumError("Couldn't delete cookie " + cookieName);
+}
+
/*
* Finds an element recursively in frames and nested frames
* in the specified document, using various lookup protocols
@@ -1089,10 +1265,14 @@ BrowserBot.prototype.findElementRecursive = function(locatorType, locatorString, }
for (var i = 0; i < inWindow.frames.length; i++) {
- element = this.findElementRecursive(locatorType, locatorString, inWindow.frames[i].document, inWindow.frames[i]);
+ // On some browsers, the document object is undefined for third-party
+ // frames. Make sure the document is valid before continuing.
+ if (inWindow.frames[i].document) {
+ element = this.findElementRecursive(locatorType, locatorString, inWindow.frames[i].document, inWindow.frames[i]);
- if (element != null) {
- return element;
+ if (element != null) {
+ return element;
+ }
}
}
};
@@ -1101,20 +1281,12 @@ BrowserBot.prototype.findElementRecursive = function(locatorType, locatorString, * Finds an element on the current page, using various lookup protocols
*/
BrowserBot.prototype.findElementOrNull = function(locator, win) {
- var locatorType = 'implicit';
- var locatorString = locator;
-
- // If there is a locator prefix, use the specified strategy
- var result = locator.match(/^([A-Za-z]+)=(.+)/);
- if (result) {
- locatorType = result[1].toLowerCase();
- locatorString = result[2];
- }
+ locator = parse_locator(locator);
if (win == null) {
win = this.getCurrentWindow();
}
- var element = this.findElementRecursive(locatorType, locatorString, win.document, win);
+ var element = this.findElementRecursive(locator.type, locator.string, win.document, win);
if (element != null) {
return this.browserbot.highlight(element);
@@ -1145,9 +1317,15 @@ BrowserBot.prototype.locateElementByIdentifier = function(identifier, inDocument */
BrowserBot.prototype.locateElementById = function(identifier, inDocument, inWindow) {
var element = inDocument.getElementById(identifier);
- if (element && element.id === identifier) {
+ if (element && element.getAttribute('id') === identifier) {
return element;
}
+ else if (browserVersion.isIE || browserVersion.isOpera) {
+ // SEL-484
+ var xpath = '/descendant::*[@id=' + identifier.quoteForXPath() + ']';
+ return BrowserBot.prototype
+ .locateElementByXPath(xpath, inDocument, inWindow);
+ }
else {
return null;
}
@@ -1200,79 +1378,14 @@ BrowserBot.prototype.locateElementByDomTraversal.prefix = "dom"; * begin with "//".
*/
BrowserBot.prototype.locateElementByXPath = function(xpath, inDocument, inWindow) {
- // Trim any trailing "/": not valid xpath, and remains from attribute
- // locator.
- if (xpath.charAt(xpath.length - 1) == '/') {
- xpath = xpath.slice(0, -1);
- }
-
- // Handle //tag
- var match = xpath.match(/^\/\/(\w+|\*)$/);
- if (match) {
- var elements = inDocument.getElementsByTagName(match[1].toUpperCase());
- if (elements == null) return null;
- return elements[0];
- }
-
- // Handle //tag[@attr='value']
- var match = xpath.match(/^\/\/(\w+|\*)\[@(\w+)=('([^\']+)'|"([^\"]+)")\]$/);
- if (match) {
- // We don't return the value without checking if it is null first.
- // This is beacuse in some rare cases, this shortcut actually WONT work
- // but that the full XPath WILL. A known case, for example, is in IE
- // when the attribute is onclick/onblur/onsubmit/etc. Due to a bug in IE
- // this shortcut won't work because the actual function is returned
- // by getAttribute() rather than the text of the attribute.
- var val = this._findElementByTagNameAndAttributeValue(
- inDocument,
- match[1].toUpperCase(),
- match[2].toLowerCase(),
- match[3].slice(1, -1)
- );
- if (val) {
- return val;
- }
- }
-
- // 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._findElementUsingFullXPath(xpath, inDocument);
-};
-
-BrowserBot.prototype._findElementByTagNameAndAttributeValue = function(
- inDocument, tagName, attributeName, attributeValue
- ) {
- if (browserVersion.isIE && attributeName == "class") {
- attributeName = "className";
- }
- var elements = inDocument.getElementsByTagName(tagName);
- for (var i = 0; i < elements.length; i++) {
- var elementAttr = elements[i].getAttribute(attributeName);
- if (elementAttr == attributeValue) {
- return elements[i];
- }
- }
- return null;
-};
-
-BrowserBot.prototype._findElementByTagNameAndText = function(
- inDocument, tagName, text
- ) {
- var elements = inDocument.getElementsByTagName(tagName);
- for (var i = 0; i < elements.length; i++) {
- if (getText(elements[i]) == text) {
- return elements[i];
- }
- }
- return null;
+ var results = eval_xpath(xpath, inDocument, {
+ returnOnFirstMatch : true,
+ ignoreAttributesWithoutValue: this.ignoreAttributesWithoutValue,
+ allowNativeXpath : this.allowNativeXpath,
+ xpathLibrary : this.xpathLibrary,
+ namespaceResolver : this._namespaceResolver
+ });
+ return (results.length > 0) ? results[0] : null;
};
BrowserBot.prototype._namespaceResolver = function(prefix) {
@@ -1285,65 +1398,17 @@ BrowserBot.prototype._namespaceResolver = function(prefix) { }
}
-BrowserBot.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 (this.allowNativeXpath && inDocument.evaluate) {
- return inDocument.evaluate(xpath, inDocument, this._namespaceResolver, 0, null).iterateNext();
- }
-
- // If not, fall back to slower JavaScript implementation
- // DGF set xpathdebug = true (using getEval, if you like) to turn on JS XPath debugging
- //xpathdebug = true;
- var context = new ExprContext(inDocument);
- var xpathObj = xpathParse(xpath);
- var xpathResult = xpathObj.evaluate(context);
- if (xpathResult && xpathResult.value) {
- return xpathResult.value[0];
- }
- return null;
-
-};
-
-// DGF this may LOOK identical to _findElementUsingFullXPath, but
-// fEUFX pops the first element off the resulting nodelist; this function
-// wraps the xpath in a count() operator and returns the numeric value directly
+/**
+ * Returns the number of xpath results.
+ */
BrowserBot.prototype.evaluateXPathCount = function(xpath, inDocument) {
- // HUGE hack - remove namespace from xpath for IE
- if (browserVersion.isIE) {
- xpath = xpath.replace(/x:/g, '')
- }
- xpath = new String(xpath);
- if (xpath.indexOf("xpath=") == 0) {
- xpath = xpath.substring(6);
- }
- if (xpath.indexOf("count(") == 0) {
- // DGF we COULD just fix this up for the user, but we might get it wrong (parens?)
- throw new SeleniumError("XPath count expressions must not be wrapped in count() function: " + xpath);
- }
-
- xpath="count("+xpath+")";
-
- // Use document.evaluate() if it's available
- if (this.allowNativeXpath && inDocument.evaluate) {
- var result = inDocument.evaluate(xpath, inDocument, this._namespaceResolver, XPathResult.NUMBER_TYPE, null);
- return result.numberValue;
- }
-
- // If not, fall back to slower JavaScript implementation
- // DGF set xpathdebug = true (using getEval, if you like) to turn on JS XPath debugging
- //xpathdebug = true;
- var context = new ExprContext(inDocument);
- var xpathObj = xpathParse(xpath);
- var xpathResult = xpathObj.evaluate(context);
- if (xpathResult && xpathResult.value) {
- return xpathResult.value;
- }
- return 0;
+ var results = eval_xpath(xpath, inDocument, {
+ ignoreAttributesWithoutValue: this.ignoreAttributesWithoutValue,
+ allowNativeXpath : this.allowNativeXpath,
+ xpathLibrary : this.xpathLibrary,
+ namespaceResolver : this._namespaceResolver
+ });
+ return results.length;
};
/**
@@ -1382,6 +1447,11 @@ BrowserBot.prototype.findAttribute = function(locator) { // Get the attribute value.
var attributeValue = element.getAttribute(attributeName);
+
+ // IE returns an object for the "style" attribute
+ if (attributeName == 'style' && typeof(attributeValue) != 'string') {
+ attributeValue = attributeValue.cssText;
+ }
return attributeValue ? attributeValue.toString() : null;
};
@@ -1515,6 +1585,11 @@ BrowserBot.prototype.doubleClickElement = function(element, clientX, clientY) { this._fireEventOnElement("dblclick", element, clientX, clientY);
};
+// The contextmenu event is fired when the user right-clicks to open the context menu
+BrowserBot.prototype.contextMenuOnElement = function(element, clientX, clientY) {
+ this._fireEventOnElement("contextmenu", element, clientX, clientY);
+};
+
BrowserBot.prototype._modifyElementTarget = function(element) {
if (element.target) {
if (element.target == "_blank" || /^selenium_blank/.test(element.target) ) {
@@ -1551,6 +1626,9 @@ BrowserBot.prototype._getTargetWindow = function(element) { BrowserBot.prototype._getFrameFromGlobal = function(target) {
+ if (target == "_self") {
+ return this.getCurrentWindow();
+ }
if (target == "_top") {
return this.topFrame;
} else if (target == "_parent") {
@@ -1627,10 +1705,21 @@ BrowserBot.prototype.goForward = function() { };
BrowserBot.prototype.close = function() {
+ if (browserVersion.isIE) {
+ // fix "do you want to close this window" warning in IE
+ // You can only close windows that you have opened.
+ // So, let's "open" it.
+ try {
+ this.topFrame.name=new Date().getTime();
+ window.open("", this.topFrame.name, "");
+ this.topFrame.close();
+ return;
+ } catch (e) {}
+ }
if (browserVersion.isChrome || browserVersion.isSafari || browserVersion.isOpera) {
- this.getCurrentWindow().close();
+ this.topFrame.close();
} else {
- this.getCurrentWindow().eval("window.close();");
+ this.getCurrentWindow().eval("window.top.close();");
}
};
@@ -1723,12 +1812,75 @@ BrowserBot.prototype.locateElementByAlt = function(locator, document) { * Find an element by css selector
*/
BrowserBot.prototype.locateElementByCss = function(locator, document) {
- var elements = cssQuery(locator, document);
+ var elements = eval_css(locator, document);
if (elements.length != 0)
return elements[0];
return null;
}
+/**
+ * This function is responsible for mapping a UI specifier string to an element
+ * on the page, and returning it. If no element is found, null is returned.
+ * Returning null on failure to locate the element is part of the undocumented
+ * API for locator strategies.
+ */
+BrowserBot.prototype.locateElementByUIElement = function(locator, inDocument) {
+ // offset locators are delimited by "->", which is much simpler than the
+ // previous scheme involving detecting the close-paren.
+ var locators = locator.split(/->/, 2);
+
+ var locatedElement = null;
+ var pageElements = UIMap.getInstance()
+ .getPageElements(locators[0], inDocument);
+
+ if (locators.length > 1) {
+ for (var i = 0; i < pageElements.length; ++i) {
+ var locatedElements = eval_locator(locators[1], inDocument,
+ pageElements[i]);
+ if (locatedElements.length) {
+ locatedElement = locatedElements[0];
+ break;
+ }
+ }
+ }
+ else if (pageElements.length) {
+ locatedElement = pageElements[0];
+ }
+
+ return locatedElement;
+}
+
+BrowserBot.prototype.locateElementByUIElement.prefix = 'ui';
+
+// define a function used to compare the result of a close UI element
+// match with the actual interacted element. If they are close enough
+// according to the heuristic, consider them a match.
+/**
+ * A heuristic function for comparing a node with a target node. Typically the
+ * node is specified in a UI element definition, while the target node is
+ * returned by the recorder as the leaf element which had some event enacted
+ * upon it. This particular heuristic covers the case where the anchor element
+ * contains other inline tags, such as "em" or "img".
+ *
+ * @param node the node being compared to the target node
+ * @param target the target node
+ * @return true if node equals target, or if node is a link
+ * element and target is its descendant, or if node has
+ * an onclick attribute and target is its descendant.
+ * False otherwise.
+ */
+BrowserBot.prototype.locateElementByUIElement.is_fuzzy_match = function(node, target) {
+ try {
+ var isMatch = (
+ (node == target) ||
+ ((node.nodeName == 'A' || node.onclick) && is_ancestor(node, target))
+ );
+ return isMatch;
+ }
+ catch (e) {
+ return false;
+ }
+};
/*****************************************************************/
/* BROWSER-SPECIFIC FUNCTIONS ONLY AFTER THIS LINE */
@@ -1855,7 +2007,7 @@ IEBrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToModif var runInterval = '';
// Only set run interval if options is defined
- if (typeof(window.runOptions) != undefined) {
+ if (typeof(window.runOptions) != 'undefined') {
runInterval = "&runInterval=" + runOptions.runInterval;
}
@@ -1990,28 +2142,6 @@ IEBrowserBot.prototype.locateElementByIdentifer = function(identifier, inDocumen return inDocument.getElementById(identifier);
};
-IEBrowserBot.prototype._findElementByTagNameAndAttributeValue = function(
- inDocument, tagName, attributeName, attributeValue
- ) {
- if (attributeName == "class") {
- attributeName = "className";
- }
- var elements = inDocument.getElementsByTagName(tagName);
- for (var i = 0; i < elements.length; i++) {
- var elementAttr = elements[i].getAttribute(attributeName);
- if (elementAttr == attributeValue) {
- return elements[i];
- }
- // DGF SEL-347, IE6 URL-escapes javascript href attribute
- if (!elementAttr) continue;
- elementAttr = unescape(new String(elementAttr));
- if (elementAttr == attributeValue) {
- return elements[i];
- }
- }
- return null;
-};
-
SafariBrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToModify, browserBot) {
BrowserBot.prototype.modifyWindowToRecordPopUpDialogs(windowToModify, browserBot);
diff --git a/tests/selenium/selenium-lib/core/scripts/selenium-browserdetect.js b/tests/selenium/selenium-lib/core/scripts/selenium-browserdetect.js index 07e09031..825521f0 100644 --- a/tests/selenium/selenium-lib/core/scripts/selenium-browserdetect.js +++ b/tests/selenium/selenium-lib/core/scripts/selenium-browserdetect.js @@ -93,9 +93,12 @@ var BrowserVersion = function() { self.isHTA = false; } } + if (navigator.appVersion.match(/MSIE 6.0/)) { + this.isIE6 = true; + } if ("0" == navigator.appMinorVersion) { this.preSV1 = true; - if (navigator.appVersion.match(/MSIE 6.0/)) { + if (this.isIE6) { this.appearsToBeBrokenInitialIE6 = true; } } diff --git a/tests/selenium/selenium-lib/core/scripts/selenium-commandhandlers.js b/tests/selenium/selenium-lib/core/scripts/selenium-commandhandlers.js index 90694f31..f073e2bc 100644 --- a/tests/selenium/selenium-lib/core/scripts/selenium-commandhandlers.js +++ b/tests/selenium/selenium-lib/core/scripts/selenium-commandhandlers.js @@ -306,12 +306,14 @@ ActionHandler.prototype.execute = function(seleniumApi, command) { // todo: this conditional logic is ugly
seleniumApi.ensureNoUnhandledPopups();
}
- var terminationCondition = this.actionBlock(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 (terminationCondition == undefined && this.wait) {
- terminationCondition = seleniumApi.makePageLoadCondition();
- }
+
+ var handlerCondition = this.actionBlock(command.target, command.value);
+
+ // page load waiting takes precedence over any wait condition returned by
+ // the action handler.
+ var terminationCondition = (this.wait)
+ ? seleniumApi.makePageLoadCondition() : handlerCondition;
+
return new ActionResult(terminationCondition);
};
diff --git a/tests/selenium/selenium-lib/core/scripts/selenium-executionloop.js b/tests/selenium/selenium-lib/core/scripts/selenium-executionloop.js index 4e55110c..dceae35c 100644 --- a/tests/selenium/selenium-lib/core/scripts/selenium-executionloop.js +++ b/tests/selenium/selenium-lib/core/scripts/selenium-executionloop.js @@ -119,7 +119,7 @@ TestLoop.prototype = { _handleCommandError : function(e) {
if (!e.isSeleniumError) {
LOG.exception(e);
- var msg = "Selenium failure. Please report to the Selenium Users forum at http://forums.openqa.org, with error details from the log window.";
+ var msg = "Command execution failure. Please search the forum at http://clearspace.openqa.org for error details from the log window.";
msg += " The error message is: " + extractExceptionMessage(e);
return this.commandError(msg);
} else {
diff --git a/tests/selenium/selenium-lib/core/scripts/selenium-logging.js b/tests/selenium/selenium-lib/core/scripts/selenium-logging.js index d6d65b36..8e4dc053 100644 --- a/tests/selenium/selenium-lib/core/scripts/selenium-logging.js +++ b/tests/selenium/selenium-lib/core/scripts/selenium-logging.js @@ -87,14 +87,14 @@ Logger.prototype = { logWindow.enableButtons();
}
if (this.pendingMessages.length > 0) {
- logWindow.append("info: Appending missed logging messages", "info");
+ logWindow.append("info("+(new Date().getTime())+"): 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(msg.type + "("+msg.timestamp+"): " + msg.msg, msg.type);
}
- logWindow.append("info: Done appending missed logging messages", "info");
+ logWindow.append("info("+(new Date().getTime())+"): Done appending missed logging messages", "info");
}
- logWindow.append(logLevel + ": " + message, logLevel);
+ logWindow.append(logLevel + "("+(new Date().getTime())+"): " + message, logLevel);
}
} else {
// TODO these logging messages are never flushed, which creates
@@ -144,4 +144,5 @@ var LOG = new Logger(); var LogMessage = function(type, msg) {
this.type = type;
this.msg = msg;
+ this.timestamp = (new Date().getTime());
}
diff --git a/tests/selenium/selenium-lib/core/scripts/selenium-remoterunner.js b/tests/selenium/selenium-lib/core/scripts/selenium-remoterunner.js index 7f01e152..0fa03506 100644 --- a/tests/selenium/selenium-lib/core/scripts/selenium-remoterunner.js +++ b/tests/selenium/selenium-lib/core/scripts/selenium-remoterunner.js @@ -22,10 +22,6 @@ workingColor = "#DEE7EC"; doneColor = "#FFFFCC";
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;
@@ -33,6 +29,16 @@ var relayToRC = null; var proxyInjectionMode = false;
var uniqueId = 'sel_' + Math.round(100000 * Math.random());
var seleniumSequenceNumber = 0;
+var cmd8 = "";
+var cmd7 = "";
+var cmd6 = "";
+var cmd5 = "";
+var cmd4 = "";
+var cmd3 = "";
+var cmd2 = "";
+var cmd1 = "";
+var lastCmd = "";
+var lastCmdTime = new Date();
var RemoteRunnerOptions = classCreate();
objectExtend(RemoteRunnerOptions.prototype, URLConfiguration.prototype);
@@ -52,8 +58,12 @@ objectExtend(RemoteRunnerOptions.prototype, { return this._getQueryParameter("driverUrl");
},
+ // requires per-session extension Javascript as soon as this Selenium
+ // instance becomes aware of the session identifier
getSessionId: function() {
- return this._getQueryParameter("sessionId");
+ var sessionId = this._getQueryParameter("sessionId");
+ requireExtensionJs(sessionId);
+ return sessionId;
},
_acquireQueryString: function () {
@@ -109,13 +119,6 @@ function runSeleniumTest() { currentTest = new RemoteRunner(commandFactory);
- 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";
@@ -209,26 +212,37 @@ objectExtend(RemoteRunner.prototype, { cmdText += ', ' + command.value;
}
}
- cmdText += ")";
- if (cmdText.length >40) {
- cmdText = cmdText.substring(0,40);
- cmdText += "...";
+ if (cmdText.length > 70) {
+ cmdText = cmdText.substring(0, 70) + "...\n";
+ } else {
+ cmdText += ")\n";
}
- this.commandNode.appendChild(document.createTextNode(cmdText));
- 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);
+
+ if (cmdText == lastCmd) {
+ var rightNow = new Date();
+ var msSinceStart = rightNow.getTime() - lastCmdTime.getTime();
+ var sinceStart = msSinceStart + "ms";
+ if (msSinceStart > 1000) {
+ sinceStart = Math.round(msSinceStart / 1000) + "s";
+ }
+ cmd1 = "Same command (" + sinceStart + "): " + lastCmd;
+ } else {
+ lastCmdTime = new Date();
+ cmd8 = cmd7;
+ cmd7 = cmd6;
+ cmd6 = cmd5;
+ cmd5 = 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);
+ cmd1 = cmdText;
+ }
+ lastCmd = cmdText;
+
+ if (! proxyInjectionMode) {
+ var commandList = document.commands.commandList;
+ commandList.value = cmd8 + cmd7 + cmd6 + cmd5 + cmd4 + cmd3 + cmd2 + cmd1;
+ commandList.scrollTop = commandList.scrollHeight;
}
},
@@ -259,7 +273,7 @@ objectExtend(RemoteRunner.prototype, { commandError : function(message) {
postResult = "ERROR: " + message;
this.commandNode.style.backgroundColor = errorColor;
- this.commandNode.title = message;
+ this.commandNode.titcle = message;
},
testComplete : function() {
@@ -279,8 +293,14 @@ objectExtend(RemoteRunner.prototype, { return;
}
var command = this._extractCommand(this.xmlHttpForCommandsAndResults);
- this.currentCommand = command;
- this.continueTestAtCurrentCommand();
+ if (command.command == 'retryLast') {
+ setTimeout(fnBind(function() {
+ sendToRC("RETRY", "retry=true", fnBind(this._HandleHttpResponse, this), this.xmlHttpForCommandsAndResults, true);
+ }, this), 1000);
+ } else {
+ this.currentCommand = command;
+ this.continueTestAtCurrentCommand();
+ }
}
// Not OK
else {
@@ -294,7 +314,15 @@ objectExtend(RemoteRunner.prototype, { },
_extractCommand : function(xmlHttp) {
- var command;
+ var command, text, json;
+ text = command = xmlHttp.responseText;
+ if (/^json=/.test(text)) {
+ eval(text);
+ if (json.rest) {
+ eval(json.rest);
+ }
+ return json;
+ }
try {
var re = new RegExp("^(.*?)\n((.|[\r\n])*)");
if (re.exec(xmlHttp.responseText)) {
@@ -371,39 +399,11 @@ function sendToRC(dataToBePosted, urlParms, callback, xmlHttpObject, async) { url = addUrlParams(url);
url += "&sequenceNumber=" + seleniumSequenceNumber++;
- var wrappingCallback;
- if (callback == null) {
- callback = function() {};
- wrappingCallback = callback;
- } else {
- wrappingCallback = function() {
- if (xmlHttpObject.readyState == 4) {
- if (xmlHttpObject.status == 200) {
- var retry = false;
- if (typeof currentTest != 'undefined') {
- var command = currentTest._extractCommand(xmlHttpObject);
- //console.log("*********** " + command.command + " | " + command.target + " | " + command.value);
- if (command.command == 'retryLast') {
- retry = true;
- }
- }
- if (retry) {
- setTimeout(fnBind(function() {
- sendToRC("RETRY", "retry=true", callback, xmlHttpObject, async);
- }, this), 1000);
- } else {
- callback();
- }
- }
- }
- }
- }
-
var postedData = "postedData=" + encodeURIComponent(dataToBePosted);
//xmlHttpObject.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlHttpObject.open("POST", url, async);
- xmlHttpObject.onreadystatechange = wrappingCallback;
+ if (callback) xmlHttpObject.onreadystatechange = callback;
xmlHttpObject.send(postedData);
return null;
}
@@ -561,6 +561,44 @@ Selenium.prototype.doSetContext = function(context) { }
};
+/**
+ * Adds a script tag referencing a specially-named user extensions "file". The
+ * resource handler for this special file (which won't actually exist) will use
+ * the session ID embedded in its name to retrieve per-session specified user
+ * extension javascript.
+ *
+ * @param sessionId
+ */
+function requireExtensionJs(sessionId) {
+ var src = 'scripts/user-extensions.js[' + sessionId + ']';
+ if (document.getElementById(src) == null) {
+ var scriptTag = document.createElement('script');
+ scriptTag.language = 'JavaScript';
+ scriptTag.type = 'text/javascript';
+ scriptTag.src = src;
+ scriptTag.id = src;
+ var headTag = document.getElementsByTagName('head')[0];
+ headTag.appendChild(scriptTag);
+ }
+}
+
+Selenium.prototype.doAttachFile = function(fieldLocator,fileLocator) {
+ /**
+ * Sets a file input (upload) field to the file listed in fileLocator
+ *
+ * @param fieldLocator an <a href="#locators">element locator</a>
+ * @param fileLocator a URL pointing to the specified file. Before the file
+ * can be set in the input field (fieldLocator), Selenium RC may need to transfer the file
+ * to the local machine before attaching the file in a web page form. This is common in selenium
+ * grid configurations where the RC server driving the browser is not the same
+ * machine that started the test.
+ *
+ * Supported Browsers: Firefox ("*chrome") only.
+ *
+ */
+ // This doesn't really do anything on the JS side; we let the Selenium Server take care of this for us!
+};
+
Selenium.prototype.doCaptureScreenshot = function(filename) {
/**
* Captures a PNG screenshot to the specified file.
@@ -568,4 +606,90 @@ Selenium.prototype.doCaptureScreenshot = function(filename) { * @param filename the absolute path to the file to be written, e.g. "c:\blah\screenshot.png"
*/
// This doesn't really do anything on the JS side; we let the Selenium Server take care of this for us!
-};
\ No newline at end of file +};
+
+Selenium.prototype.doCaptureScreenshotToString = function() {
+ /**
+ * Capture a PNG screenshot. It then returns the file as a base 64 encoded string.
+ *
+ * @return string The base 64 encoded string of the screen shot (PNG file)
+ */
+ // This doesn't really do anything on the JS side; we let the Selenium Server take care of this for us!
+};
+
+Selenium.prototype.doCaptureEntirePageScreenshotToString = function(kwargs) {
+ /**
+ * Downloads a screenshot of the browser current window canvas to a
+ * based 64 encoded PNG file. The <em>entire</em> windows canvas is captured,
+ * including parts rendered outside of the current view port.
+ *
+ * Currently this only works in Mozilla and when running in chrome mode.
+ *
+ * @param kwargs A kwargs string that modifies the way the screenshot is captured. Example: "background=#CCFFDD". This may be useful to set for capturing screenshots of less-than-ideal layouts, for example where absolute positioning causes the calculation of the canvas dimension to fail and a black background is exposed (possibly obscuring black text).
+ *
+ * @return string The base 64 encoded string of the page screenshot (PNG file)
+ */
+ // This doesn't really do anything on the JS side; we let the Selenium Server take care of this for us!
+};
+
+Selenium.prototype.doShutDownSeleniumServer = function(keycode) {
+ /**
+ * Kills the running Selenium Server and all browser sessions. After you run this command, you will no longer be able to send
+ * commands to the server; you can't remotely start the server once it has been stopped. Normally
+ * you should prefer to run the "stop" command, which terminates the current browser session, rather than
+ * shutting down the entire server.
+ *
+ */
+ // This doesn't really do anything on the JS side; we let the Selenium Server take care of this for us!
+};
+
+Selenium.prototype.doRetrieveLastRemoteControlLogs = function() {
+ /**
+ * Retrieve the last messages logged on a specific remote control. Useful for error reports, especially
+ * when running multiple remote controls in a distributed environment. The maximum number of log messages
+ * that can be retrieve is configured on remote control startup.
+ *
+ * @return string The last N log messages as a multi-line string.
+ */
+ // This doesn't really do anything on the JS side; we let the Selenium Server take care of this for us!
+};
+
+Selenium.prototype.doKeyDownNative = function(keycode) {
+ /**
+ * Simulates a user pressing a key (without releasing it yet) by sending a native operating system keystroke.
+ * This function uses the java.awt.Robot class to send a keystroke; this more accurately simulates typing
+ * a key on the keyboard. It does not honor settings from the shiftKeyDown, controlKeyDown, altKeyDown and
+ * metaKeyDown commands, and does not target any particular HTML element. To send a keystroke to a particular
+ * element, focus on the element first before running this command.
+ *
+ * @param keycode an integer keycode number corresponding to a java.awt.event.KeyEvent; note that Java keycodes are NOT the same thing as JavaScript keycodes!
+ */
+ // This doesn't really do anything on the JS side; we let the Selenium Server take care of this for us!
+};
+
+Selenium.prototype.doKeyUpNative = function(keycode) {
+ /**
+ * Simulates a user releasing a key by sending a native operating system keystroke.
+ * This function uses the java.awt.Robot class to send a keystroke; this more accurately simulates typing
+ * a key on the keyboard. It does not honor settings from the shiftKeyDown, controlKeyDown, altKeyDown and
+ * metaKeyDown commands, and does not target any particular HTML element. To send a keystroke to a particular
+ * element, focus on the element first before running this command.
+ *
+ * @param keycode an integer keycode number corresponding to a java.awt.event.KeyEvent; note that Java keycodes are NOT the same thing as JavaScript keycodes!
+ */
+ // This doesn't really do anything on the JS side; we let the Selenium Server take care of this for us!
+};
+
+Selenium.prototype.doKeyPressNative = function(keycode) {
+ /**
+ * Simulates a user pressing and releasing a key by sending a native operating system keystroke.
+ * This function uses the java.awt.Robot class to send a keystroke; this more accurately simulates typing
+ * a key on the keyboard. It does not honor settings from the shiftKeyDown, controlKeyDown, altKeyDown and
+ * metaKeyDown commands, and does not target any particular HTML element. To send a keystroke to a particular
+ * element, focus on the element first before running this command.
+ *
+ * @param keycode an integer keycode number corresponding to a java.awt.event.KeyEvent; note that Java keycodes are NOT the same thing as JavaScript keycodes!
+ */
+ // This doesn't really do anything on the JS side; we let the Selenium Server take care of this for us!
+};
+
diff --git a/tests/selenium/selenium-lib/core/scripts/selenium-testrunner.js b/tests/selenium/selenium-lib/core/scripts/selenium-testrunner.js index 89cb80e9..e3e83ff3 100644 --- a/tests/selenium/selenium-lib/core/scripts/selenium-testrunner.js +++ b/tests/selenium/selenium-lib/core/scripts/selenium-testrunner.js @@ -91,6 +91,7 @@ objectExtend(HtmlTestRunner.prototype, { },
_onloadTestSuite:function () {
+ suiteFrame = new HtmlTestSuiteFrame(getSuiteFrame());
if (! this.getTestSuite().isAvailable()) {
return;
}
@@ -102,7 +103,11 @@ objectExtend(HtmlTestRunner.prototype, { this._getApplicationWindow().src = this.controlPanel.getAutoUrl();
} else {
var testCaseLoaded = fnBind(function(){this.testCaseLoaded=true;},this);
- this.getTestSuite().getSuiteRows()[0].loadTestCase(testCaseLoaded);
+ var testNumber = 0;
+ if (this.controlPanel.getTestNumber() != null){
+ var testNumber = this.controlPanel.getTestNumber() - 1;
+ }
+ this.getTestSuite().getSuiteRows()[testNumber].loadTestCase(testCaseLoaded);
}
},
@@ -139,17 +144,13 @@ objectExtend(HtmlTestRunner.prototype, { //todo: move testFailed and storedVars to TestCase
this.testFailed = false;
storedVars = new Object();
+ storedVars.nbsp = String.fromCharCode(160);
+ storedVars.space = ' ';
this.currentTest = new HtmlRunnerTestLoop(testFrame.getCurrentTestCase(), this.metrics, this.commandFactory);
currentTest = this.currentTest;
this.currentTest.start();
},
- runRemainTests:function() {
- this.runAllTests = true;
- this.metrics.resetMetrics();
- this.startTest();
- },
-
runSingleTest:function() {
this.runAllTests = false;
this.metrics.resetMetrics();
@@ -168,6 +169,10 @@ objectExtend(SeleniumFrame.prototype, { addLoadListener(this.frame, fnBind(this._handleLoad, this));
},
+ getWindow : function() {
+ return this.frame.contentWindow;
+ },
+
getDocument : function() {
return this.frame.contentWindow.document;
},
@@ -177,7 +182,6 @@ objectExtend(SeleniumFrame.prototype, { this._onLoad();
if (this.loadCallback) {
this.loadCallback();
- this.loadCallback = null;
}
},
@@ -260,7 +264,7 @@ objectExtend(HtmlTestFrame.prototype, SeleniumFrame.prototype); objectExtend(HtmlTestFrame.prototype, {
_onLoad: function() {
- this.currentTestCase = new HtmlTestCase(this.getDocument(), htmlTestRunner.getTestSuite().getCurrentRow());
+ this.currentTestCase = new HtmlTestCase(this.getWindow(), htmlTestRunner.getTestSuite().getCurrentRow());
},
getCurrentTestCase: function() {
@@ -392,6 +396,10 @@ objectExtend(HtmlTestRunnerControlPanel.prototype, { return this._getQueryParameter("test");
},
+ getTestNumber: function() {
+ return this._getQueryParameter("testNumber");
+ },
+
getSingleTestName: function() {
return this._getQueryParameter("singletest");
},
@@ -592,7 +600,9 @@ objectExtend(HtmlTestSuite.prototype, { initialize: function(suiteDocument) {
this.suiteDocument = suiteDocument;
this.suiteRows = this._collectSuiteRows();
- this.titleRow = new TitleRow(this.getTestTable().rows[0]);
+ var testTable = this.getTestTable();
+ if (!testTable) return;
+ this.titleRow = new TitleRow(testTable.rows[0]);
this.reset();
},
@@ -623,6 +633,7 @@ objectExtend(HtmlTestSuite.prototype, { var result = [];
var tables = sel$A(this.suiteDocument.getElementsByTagName("table"));
var testTable = tables[0];
+ if (!testTable) return;
for (rowNum = 1; rowNum < testTable.rows.length; rowNum++) {
var rowElement = testTable.rows[rowNum];
result.push(new HtmlTestSuiteRow(rowElement, testFrame, this));
@@ -670,7 +681,7 @@ objectExtend(HtmlTestSuite.prototype, { _onTestSuiteComplete: function() {
this.markDone();
- new TestResult(this.failed, this.getTestTable()).post();
+ new SeleniumTestResult(this.failed, this.getTestTable()).post();
},
updateSuiteWithResultOfPreviousTest: function() {
@@ -694,8 +705,8 @@ objectExtend(HtmlTestSuite.prototype, { });
-var TestResult = classCreate();
-objectExtend(TestResult.prototype, {
+var SeleniumTestResult = classCreate();
+objectExtend(SeleniumTestResult.prototype, {
// Post the results to a servlet, CGI-script, etc. The URL of the
// results-handler defaults to "/postResults", but an alternative location
@@ -869,14 +880,22 @@ objectExtend(TestResult.prototype, { var HtmlTestCase = classCreate();
objectExtend(HtmlTestCase.prototype, {
- initialize: function(testDocument, htmlTestSuiteRow) {
- if (testDocument == null) {
- throw "testDocument should not be null";
+ initialize: function(testWindow, htmlTestSuiteRow) {
+ if (testWindow == null) {
+ throw "testWindow should not be null";
}
if (htmlTestSuiteRow == null) {
throw "htmlTestSuiteRow should not be null";
}
- this.testDocument = testDocument;
+ this.testWindow = testWindow;
+ this.testDocument = testWindow.document;
+ this.pathname = "'unknown'";
+ try {
+ if (this.testWindow.location) {
+ this.pathname = this.testWindow.location.pathname;
+ }
+ } catch (e) {}
+
this.htmlTestSuiteRow = htmlTestSuiteRow;
this.headerRow = new TitleRow(this.testDocument.getElementsByTagName("tr")[0]);
this.commandRows = this._collectCommandRows();
@@ -1077,10 +1096,7 @@ objectExtend(HtmlRunnerTestLoop.prototype, { this.metrics = metrics;
this.htmlTestCase = htmlTestCase;
- LOG.info("Starting test " + htmlTestCase.testDocument.location.pathname);
-
- se = selenium;
- global.se = selenium;
+ LOG.info("Starting test " + htmlTestCase.pathname);
this.currentRow = null;
this.currentRowIndex = 0;
@@ -1092,17 +1108,6 @@ objectExtend(HtmlRunnerTestLoop.prototype, { this.expectedFailureType = null;
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() {
@@ -1289,6 +1294,24 @@ Selenium.prototype.doEcho = function(message) { currentTest.currentRow.setMessage(message);
}
+/*
+ * doSetSpeed and getSpeed are already defined in selenium-api.js,
+ * so we're defining these functions in a tricky way so that doc.js doesn't
+ * try to read API doc from the function definitions here.
+ */
+Selenium.prototype._doSetSpeed = function(value) {
+ var milliseconds = parseInt(value);
+ if (milliseconds < 0) milliseconds = 0;
+ htmlTestRunner.controlPanel.speedController.setValue(milliseconds);
+ htmlTestRunner.controlPanel.setRunInterval(milliseconds);
+}
+Selenium.prototype.doSetSpeed = Selenium.prototype._doSetSpeed;
+
+Selenium.prototype._getSpeed = function() {
+ return htmlTestRunner.controlPanel.runInterval;
+}
+Selenium.prototype.getSpeed = Selenium.prototype._getSpeed;
+
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>
diff --git a/tests/selenium/selenium-lib/core/scripts/selenium-version.js b/tests/selenium/selenium-lib/core/scripts/selenium-version.js index 81a359c0..1fee837b 100644 --- a/tests/selenium/selenium-lib/core/scripts/selenium-version.js +++ b/tests/selenium/selenium-lib/core/scripts/selenium-version.js @@ -1,5 +1,5 @@ -Selenium.version = "0.8.3"; -Selenium.revision = "1879"; +Selenium.version = "@VERSION@"; +Selenium.revision = "@REVISION@"; window.top.document.title += " v" + Selenium.version + " [" + Selenium.revision + "]"; diff --git a/tests/selenium/selenium-lib/core/scripts/ui-doc.html b/tests/selenium/selenium-lib/core/scripts/ui-doc.html new file mode 100644 index 00000000..f88c447a --- /dev/null +++ b/tests/selenium/selenium-lib/core/scripts/ui-doc.html @@ -0,0 +1,803 @@ +<html> +<head> +<title>Selenium UI-Element Reference</title> +<style type="text/css"> +body { + margin-left: 5%; + margin-right: 5%; +} +dt { + font-weight: bolder; +} +dd { + margin-top: 10px; + margin-bottom: 10px; +} +pre { + margin: 10px; + padding: 10px; + background-color: #eaeaea; +} +code { + padding: 2px; + background-color: #dfd; +} +table { + border-collapse: collapse; + border: solid 1px black; +} +th, td { + border: solid 1px grey; + padding: 5px; +} +.highlight { + background-color: #eea; +} +.deprecated { + font-style: italic; + color: #c99; +} +</style> +</head> + +<body> + +<h1>Selenium UI-Element Reference</h1> + +<h2>Introduction</h2> + +<p>UI-Element is a Selenium feature that makes it possible to define a mapping between semantically meaningful names of elements on webpages, and the elements themselves. The mapping is defined using <a href="http://en.wikipedia.org/wiki/JSON">JavaScript Object Notation</a>, and may be shared both by the IDE and tests run via Selenium RC. It also offers a single point of update should the user interface of the application under test change.</p> + +<h2>Terminology</h2> + +<dl> +<dt>Page</dt> +<dd>A unique URL, and the contents available by accessing that URL. A page typically consists of several interactive page elements. A page may also be considered a DOM document object, complete with URL information.</dd> +<dt>Page element</dt> +<dd>An element on the actual webpage. Generally speaking, an element is anything the user might interact with, or anything that contains meaningful content. More specifically, an element is realized as a <a href="http://en.wikipedia.org/wiki/Document_Object_Model">Document Object Model (DOM)</a> node and its contents. So when we refer to a page element, we mean both of the following, at the same time: +<ul> +<li>something on the page</li> +<li>its DOM representation, including its relationship with other page elements</li> +</ul> +</dd> +<dt>Pageset</dt> +<dd>A set of pages that share some set of common page elements. For example, I might be able to log into my application from several different pages. If certain page elements on each of those pages appear similarly (i.e. their DOM representations are identical), those pages can be grouped into a pageset with respect to these page elements. There is no restriction on how many pagesets a given page can be a member of. Similarly, a UI element belong to multiple pagesets. A pageset is commonly represented by a <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> which matches the URL's that uniquely identify pages; however, there are cases when the page content must be considered to determine pageset membership. A pageset also has a name.</dd> +<dt>UI element</dt> +<dd>A mapping between a meaningful name for a page element, and the means to locate that page element's DOM node. The page element is located via a locator. UI elements belong to pagesets.</dd> +<dt>UI argument</dt> +<dd>An optional piece of logic that determines how the locator is generated by a UI element. Typically used when similar page elements appear multiple times on the same page, and you want to address them all with a single UI element. For example, if a page presents 20 clickable search results, the index of the search result might be a UI argument.</dd> +<dt>UI map</dt> +<dd>A collection of pagesets, which in turn contain UI elements. The UI map is the medium for translating between UI specifier strings, page elements, and UI elements.</dd> +<dt>UI specifier string</dt> +<dd>A bit of text containing a pageset name, a UI element name, and optionally arguments that modify the way a locator is constructed by the UI element. UI specifier strings are intended to be the human-readable identifier for page elements.</dd> +<dt>Rollup rule</dt> +<dd>Logic that describes how one or more Selenium commands can be grouped into a single command, and how that single command may be expanded into its component Selenium commands. The single command is referred to simply as a "rollup".</dd> +<dt>Command matcher</dt> +<dd>Typically folded into a rollup rule, it matches one or more Selenium commands and optionally sets values for rollup arguments based on the matched commands. A rollup rule usually has one or more command matchers.</dd> +<dt>Rollup argument</dt> +<dd>An optional piece of logic that modifies the command expansion of a rollup.</dd> +</dl> + +<h2>The Basics</h2> + +<h3>Getting Motivated</h3> + +<p>Question: Why use UI-Element? Answer: So your testcases can look like this (boilerplate code omitted):</p> + +<pre> +<tr> + <td>open</td> + <td>/</td> + <td></td> +</tr> +<tr> + <td>clickAndWait</td> + <td><span class="highlight">ui=allPages::section(section=topics)</span></td> + <td></td> +</tr> +<tr> + <td>clickAndWait</td> + <td><span class="highlight">ui=topicListingPages::topic(topic=Process)</span></td> + <td></td> +</tr> +<tr> + <td>clickAndWait</td> + <td><span class="highlight">ui=subtopicListingPages::subtopic(subtopic=Creativity)</span></td> + <td></td> +</tr> +<tr> + <td>click</td> + <td><span class="highlight">ui=subtopicArticleListingPages::article(index=2)</span></td> + <td></td> +</tr> +</pre> + +<h3>Including the Right Files</h3> + +<p>UI-Element is now fully integrated with Selenium. The only additional file that needs to be specified is your map definitions file. In the IDE, add it to the comma-delimited <em>Selenium Core extensions</em> field of the IDE options. A sample definition file created for the website <a href="http://alistapart.com">alistapart.com</a> is included in the distribution and is available here:</p> + +<pre><a href="chrome://selenium-ide/content/selenium/scripts/ui-map-sample.js">chrome://selenium-ide/content/selenium/scripts/ui-map-sample.js</a></pre> + +<p>You might want to experiment with the sample map to get a feel for UI-Element. For the Selenium RC, you have two options. The map file may be included in the <code>user-extensions.js</code> file specified at startup with the <code>-userExtensions</code> switch. Or, you may load it dynamically with the variant of the <code>setUserExtensionJs</code> command in your driver language, before the browser is started.</p> + +<h3>Map Definitions File Syntax</h3> + +<p>This is the general format of a map file:</p> + +<pre> +var map = new UIMap(); + +map.addPageset({ + name: 'aPageset' + , ... +}); +map.addElement('aPageset', { ... }); +map.addElement('aPageset', { ... }); +... + +map.addPageset({ + name: 'anotherPageset' + , ... +}); +... +</pre> + +<p>The map object is initialized by creating a new <code>UIMap</code> object. Next, a pageset is defined. Then one or more UI elements are defined for that pageset. More pagesets are defined, each with corresponding UI elements. That's it!</p> + +<h3>Pageset Shorthand</h3> + +<p>The method signature of <code>addPageset()</code> is <em>(pagesetShorthand)</em>. <em>pagesetShorthand</em> is a JSON description of the pageset. Here's a minimal example:</p> + +<pre> +map.addPageset({ + name: 'allPages' + , description: 'contains elements common to all pages' + , pathRegexp: '.*' +}); +</pre> + +<p>Here's a table containing information about the attributes of the Pageset object. The conditionally required or unrequired items are for IDE recording support only.</p> + +<table> +<tr><th>Name</th> + <th>Required?</th> + <th>Description</th> + <th>Example</th> +</tr> +<tr><td>name</td> + <td>Yes</td> + <td>(String) the name of the pageset. This should be unique within the map.</td> + <td><pre>name: 'shopPages'</pre></td> +</tr> +<tr><td>description</td> + <td>Yes</td> + <td>(String) a description of the pageset. Ideally, this will give the reader an idea of what types of UI elements the pageset will have.</td> + <td><pre>description: 'all pages displaying product'</pre></td> +</tr> +<tr><td>pathPrefix</td> + <td>No</td> + <td>(String) the path of the URL of all included pages in this pageset will contain this prefix. For example, if all pages are of the form http://www.example.com/<span class="highlight">gallery/</span>light-show/, the page prefix might be <code>gallery/</code> .</td> + <td><pre>pathPrefix: 'gallery/'</pre></td> +</tr> +<tr><td>paths<br />pathRegexp</td> + <td>Conditional</td> + <td>(Array | String) either a list of path strings, or a string that represents a regular expression. One or the other should be defined, but not both. If an array, it enumerates pages that are included in the pageset. If a regular expression, any pages whose URL paths match the expression are considered part of the pageset. In either case, the part of the URL being matched (called the <em>path</em>) is the part following the domain, less any trailing slash, and not including the CGI parameters. For example: + <ul> + <li>http://www.example.com/<span class="highlight">articles/index.php</span></li> + <li>http://www.example.com/<span class="highlight">articles/2579</span>?lang=en_US</li> + <li>http://www.example.com/<span class="highlight">articles/selenium</span>/</li> + </ul> + The entire path must match (however, <code>pathPrefix</code> is taken into account if specified). If specified as a regular expression, the two regular expression characters <code>^</code> and <code>$</code> marking the start and end of the matched string are included implicitly, and should not be specified in this string. Please notice too that backslashes must be backslash-escaped in javascript strings.</td> + <td><pre>paths: [ + 'gotoHome.do' + , 'gotoAbout.do' + , 'gotoFaq.do' +]</pre> + <pre>pathRegexp: 'goto(Home|About|Faq)\\.do'</pre> +</tr> +<tr><td>paramRegexps</td> + <td>No</td> + <td>(Object) a mapping from URL parameter names to regular expression strings which must match their values. If specified, the set of pages potentially included in this pageset will be further filtered by URL parameter values. There is no filtering by parameter value by default.</td> + <td><pre>paramRegexps: { + dept: '^[abcd]$' + , team: 'marketing' +}</pre></td> +</tr> +<tr><td>pageContent</td> + <td>Conditional</td> + <td><p>(Function) a function that tests whether a page, represented by its document object, is contained in the pageset, and returns true if and only if this is the case. If specified, the set of pages potentially included in this pageset will be further filtered by content, after URL and URL parameter filtering.</p> + <p>Since the URL is available from the document object (<code>document.location.href</code>), you may encode the logic used for the <code>paths</code> and <code>pathRegexp</code> attributes all into the definition of <code>pageContent</code>. Thus, you may choose to omit the former if and only if using <code>pageContent</code>. Of course, you may continue to use them for clarity.</td> + <td><pre>pageContent: function(doc) { + var id = 'address-tab'; + return doc.getElementById(id) != null; +}</pre></td> +</tr> +</table> + +<h3><a name="ui-element-shorthand">UI-Element Shorthand</a></h3> + +<p>The method signature of <code>addElement()</code> is <em>(pagesetName, uiElementShorthand)</em>. <em>pagesetName</em> is the name of the pageset the UI element is being added to. <em>uiElementShorthand</em> is a complete JSON description of the UI element object in shorthand notation.</p> + +<p>In its simplest form, a UI element object looks like this:</p> + +<pre> +map.addElement('allPages', { + name: 'about_link' + , description: 'link to the about page' + , locator: "//a[contains(@href, 'about.php')]" +}); +</pre> + +<p>Here's a table containing information about the attributes of the UI element object. The asterisk (<code>*</code>) means any string:</p> + +<table> +<tr><th>Name</th> + <th>Required?</th> + <th>Description</th> + <th>Example</th> +</tr> +<tr><td>name</td> + <td>Yes</td> + <td>(String) the name of the UI element</td> + <td><pre>name: 'article'</pre></td> +</tr> +<tr><td>description</td> + <td>Yes</td> + <td>(String) a description of the UI element. This is the main documentation for this UI element, so the more detailed, the better.</td> + <td><pre>description: 'front or issue page link to article'</pre></td> +</tr> +<tr><td>args</td> + <td>No</td> + <td>(Array) a list of arguments that modify the <code>getLocator()</code> method. If unspecified, it will be treated as an empty list.</td> + <td><pre>[ + { name: 'index' + , description: 'the index of the author, by article' + , defaultValues: range(1, 5) } +]</pre><em>See section below elaborating on attributes of argument objects.</em></td> +</tr> +<tr><td>locator<br />getLocator()<br /><span class="deprecated">xpath</span<br /><span class="deprecated">getXPath()</span></td> + <td>Yes</td> + <td><p>(String | Function) either a fixed locator string, or a function that returns a locator string given a set of arguments. One or the other should be defined, but not both. Under the sheets, the <code>locator</code> attribute eventually gets transcripted as a <code>getLocator()</code> function.</p><p><span class="deprecated">As of ui0.7, <code>xpath</code> and <code>getXPath()</code> have been deprecated. They are still supported for backward compatibility.</span></p></td> + <td><pre>locator: 'submit'</pre> + <pre>getLocator: function(args) { + return 'css=div.item:nth-child(' + args.index + ')' + + ' > h5 > a'; +}</pre> + <pre>getLocator: function(args) { + var label = args.label; + var id = this._idMap[label]; + return '//input[@id=' + id.quoteForXPath() + ']'; +}</pre></td> +<tr><td>genericLocator<br />getGenericLocator</td> + <td>No</td> + <td><p>(String | Function) either a fixed locator string, or a function that returns a locator string. If a function, it should take no arguments.</p><p>You may experience some slowdown when recording on pages where individual UI elements have many default locators (due to many permutations of default values over multiple arguments). This is because each default locator is potentially evaluated and matched against the interacted page element. This becomes especially problematic if several UI elements have this characteristic.</p><p>By specifying a generic locator, you give the matching engine a chance to skip over UI elements that definitely don't match. The default locators for skipped elements will not be evaluated unless the generic locator matches the interacted page element..</p></td> + <td><pre>genericLocator: "//table[@class='ctrl']" + + "/descendant::input"</pre> + <pre>getGenericLocator: function() { + return this._xpathPrefix + '/descendant::a'; +}</pre> +</tr> +<tr><td>getOffsetLocator</td> + <td>No</td> + <td><p>(Function) a function that returns an offset locator. The locator is offset from the element identified by a UI specifier string. The function should take this element, and the interacted page element, as arguments, and have the method signature <code>getOffsetLocator(locatedElement, pageElement)</code>. If an offset locator can't be found, a value that evaluates to <code>false</code> must be returned.</p><p>A convenient default function <code>UIElement.defaultOffsetLocatorStrategy</code> is provided so you don't have to define your own. It uses several typical strategies also employed by the IDE recording when recording normally. See the Advanced Topics section below for more information on offset locators.</p></td> + <td><pre>getOffsetLocator: <span class="highlight">UIElement.defaultOffsetLocatorStrategy</span></pre> + <pre>getOffsetLocator: + function(locatedElement, pageElement) { + if (pageElement.parentNode == locatedElement) { + return '/child::' + pageElement.nodeName; + } + return null; +}</pre></td> +<tr><td>testcase*</td> + <td>No</td> + <td>(Object) a testcase for testing the implementation of the <code>getLocator()</code> method. As many testcases as desired may be defined for each UI element. They must all start with the string "testcase".</td> + <td><pre>testcase1: { + xhtml: '<div class="item"><h5>' + + '<a expected-result="1" /></h5></div>' +}</pre><em>See section below elaborating on testcases.</em></td> +</tr> +<tr><td>_*</td> + <td>No</td> + <td>(Any data type) a "local variable" declared for the UI element. This variable will be available both within the <code>getLocator()</code> method of the UI element, and any <code>getDefaultValues()</code> methods of the arguments via the <code>this</code> keyword. They must all start with an underscore "_".</td> + <td><pre>_labelMap: { + 'Name': 'user' + , 'Email': 'em' + , 'Phone': 'tel' +}</pre></td> +</tr> +</table> + +<h3>UI-Argument Shorthand</h3> + +<p>UI arguments are defined as part of UI elements, and help determine how an XPath is generated. A list of arguments may be defined within the UI element JSON shorthand. Here's an example of how that might look:</p> + +<pre> +map.addElement('searchPages', { + name: 'result' + , description: 'link to a result page' + , args: [ + { + name: 'index' + , description: 'the index of the search result' + , defaultValues: range(1, 21) + } + , { + name: 'type' + , description: 'the type of result page' + , defaultValues: [ 'summary', 'detail' ] + } + ] + , getLocator: function(args) { + var index = args['index']; + var type = args['type']; + return "//div[@class='result'][" + index + "]" + + "/descendant::a[@class='" + type + "']"; + } +}); +</pre> + +<p>In the above example, two arguments are defined, <code>index</code> and <code>type</code>. Metadata is provided to describe them, and default values are also specified. FInally, the <code>getLocator()</code> method is defined. The behavior of the method depends on the values of the arguments that are passed in.</p> + +<p>Default values come into play when recording tests using the Selenium IDE. When you interact with a page element in recording mode, the IDE uses all the locator strategies at its disposal to deduce an appropriate locator string for that element. UI-Element introduces a new <code>ui</code> locator strategy. When applying this strategy using a particular UI element, Selenium generates a list of XPaths to try by permuting the arguments of that UI element over all default values. Here, the default values <em>{ 1, 2, 3, 4 .. 20 }</em> are given for the <code>index</code> argument using the special <code>range()</code> function, and the values <em>{ summary, detail }</em> are given for the <code>type</code> argument in standard javascript array notation. If you don't intend to use the IDE, go ahead and set the default values to the empty array <code>[]</code>.</p> + +<p>Here's a table containing information about the attributes of the UI argument object.</p> + +<table> +<tr><th>Name</th> + <th>Required?</th> + <th>Description</th> + <th>Example</th> +</tr> +<tr><td>name</td> + <td>Yes</td> + <td>(String) the name of the argument. This will be the name of the property of the object passed into the parent UI element's <code>getLocator()</code> method containing the argument value.</td> + <td><pre>name: 'index'</pre></td> +</tr> +<tr><td>description</td> + <td>Yes</td> + <td>(String) a description for the argument.</td> + <td><pre>description: 'the index of the article'</pre></td> +</tr> +<tr><td>defaultValues<br/>getDefaultValues()</td> + <td>Yes</td> + <td><p>(Array | Function) either an array of string or numerical values, or a function that returns an array of string or numerical values. One or the other should be defined, but not both. Under the sheets, the <code>defaultValues</code> attribute eventually gets transcripted as a <code>getDefaultValues()</code> function.</p><p>The method signature of the function is <code>getDefaultValues(inDocument)</code>. <code>inDocument</code> is the current document object of the page at time of recording. In cases where the default values are known a priori, <code>inDocument</code> need not be used. If the default values of all arguments of a UI element are known a priori, the list of default locators for the element may be precalculated, resulting in better performance. If <code>inDocument</code> is used, in cases where the current document is inspected for valid values, the element's default locators are calculated once for every recordable event.</p></td> + <td><pre>defaultValues: [ 'alpha', 'beta', 'unlimited' ]</pre> + <pre>getDefaultValues: function() { + return keys(this._idMap); +}</pre> + <pre>getDefaultValues: function(inDocument) { + var defaultValues = []; + var links = inDocument + .getElementsByTagName('a'); + for (var i = 0; i < links.length; ++i) { + var link = links[i]; + if (link.className == 'category') { + defaultValues.push(link.innerHTML); + } + } + return defaultValues; +}</pre></td> +</tr> +</table> + +<h3>About <code>this</code></h3> + +<p>You may have noticed usage of the <code>this</code> keyword in the examples above, specifically in the <code>getLocator()</code> and <code>getDefaultValues()</code> methods. Well, what exactly is <code>this</code>?</p> + +<p>The answer is: it depends. The object referred to by <code>this</code> changes depending on the context in which it is being evaluated. At the time of object creation using <code>addPageset()</code> or <code>addElement()</code>, it refers to the <code>window</code> object of the Selenium IDE, which isn't useful at all. However, subsequently any time <code>getLocator()</code> is called, its <code>this</code> references the UI element object it's attached too. Thus, using <code>this</code>, any "local variables" defined for the UI element may be accessed. Similarly, when <code>getDefaultValues()</code> is called, its <code>this</code> references the UI argument object it's attached too. But ... what "local variables" are accessible by the from <code>getDefaultValues()</code>?</p> + +<p>There's a little magic here. If you defined your local variables as prescribed in the above <a href="#ui-element-shorthand">UI-Element Shorthand</a> section, starting with an underscore, those variable are automatically made available to the UI argument via its <code>this</code> keyword. Note that this isn't standard javascript behavior. It's implemented this way to clear out clutter in the method definitions and to avoid the use of global variables for lists and maps used within the methods. It is sometimes useful, for example, to define an object that maps human-friendly argument values to DOM element <code>id</code>'s. In such a case, <code>getDefaultValues()</code> can be made to simply return the <code>keys()</code> (or property names) of the map, while the <code>getLocator()</code> method uses the map to retrieve the associated <code>id</code> to involve in the locator.</p> + +<p>Also note that <code>this</code> only behaves this way in the two mentioned methods, <code>getLocator()</code> and <code>getDefaultValues()</code>; in other words you can't reference the UI element's local variables using <code>this</code> outside of methods.</p> + +<p>If you're interested, here's some <a href="http://www.digital-web.com/articles/scope_in_javascript/">additional reading on javascript scope</a>.</p> + +<h2>Advanced Topics</h2> + +<h3>Testcases</h3> + +<p>You can write testcases for your UI element implementations that are run every time the Selenium IDE is started. Any testcases that fail are reported on. The dual purpose of writing testcases is to both validate the <code>getLocator()</code> method against a representation of the real page under test, and to give a visual example of what the DOM context of a page element is expected to be.</p> + +<p>A testcase is an object with a required <code>xhtml</code> property (String), and a required <code>args</code> property (Object). An example is due:</p> + +<pre> +testcase1: { + args: { line: 2, column: 3 } + , xhtml: '<table id="scorecard">' + + '<tr class="line" />' + + '<tr class="line"><td /><td /><td expected-result="1" /></tr>' + + '</table>' +} +</pre> + +<p>The <code>args</code> property specifies the object to be passed into the UI element's <code>getLocator()</code> method to generate the test locator, which is then applied to the <code>xhtml</code>. If evaluating the locator on the XHTML document returns a DOM node with the <code>expected-result</code> attribute, the testcase is considered to have passed.</p> + +<p>The <code>xhtml</code> property must represent a complete XML document, sans <code><html></code> tags, which are automatically added. The reason this is necessary is that the text is being converted into an XPath evaluable DOM tree via Mozilla's native XML parser. Unfortunately, there is no way to generate a simple HTML document, only XML documents. This means that the content of the <code>xhtml</code> must be well-formed. <span class="highlight">Tags should also be specified in lowercase.</span></p> + +<h3>Fuzzy Matching</h3> + +<p>Here's a real-world example of where fuzzy matching is important:</p> + +<pre> +<table> +<tr onclick="showDetails(0)"> + <td>Brahms</td> + <td>Viola Quintet</td> +</tr> +<tr onclick="showDetails(1)"> + <td>Saegusa</td> + <td>Cello 88</td> +</tr> +</table> +</pre> + +<p>Imagine I'm recording in the IDE. Let's say I click on "Cello 88". The IDE would create locator for this action like <code>//table/tr[2]/td[2]</code>. Does that mean that my <code>getLocator()</code> method should return the same XPath?</p> + +<p>Clearly not. Clicking on either of the table cells for the second row has the same result. I would like my UI element generated XPath to be <code>//table/tr[2]</code> . However, when recording, the page element that was identified as being acted upon was the table cell, which doesn't match my UI element XPath, so the <code>ui</code> locator strategy will fail to auto-populate. What to do?</p> + +<p>Fuzzy matching to the rescue! Fuzzy matching is realized as a fuzzy matcher function that returns true if a target DOM element is considered to be equivalent to a reference DOM element. The reference DOM element would be the element specified by the UI element's generated XPath. Currently, the fuzzy matcher considers it a match if: + +<ul> +<li>the elements are the same element,</li> +<li>the reference element is an anchor (<code><a></code>) element, and the target element is a descendant of it; or</li> +<li>the reference element has an <code>onclick</code> attribute, and the target element is a descendant of it.</li> +</ul> + +<p>This logic may or may not be sufficient for you. The good news is, it's very easy to modify. Look for the definition of <code>BrowserBot.prototype.locateElementByUIElement.is_fuzzy_match</code> in <code>ui-element.js</code> .</p> + +<h3>Offset Locators</h3> + +<p>Offset locators are locators that are appended to UI specifier strings to form composite locators. They can be automatically deduced by the IDE recorder for UI elements that have specified a <code>getOffsetLocator</code> function. This feature may be useful if your pages contain too many elements to write UI elements for. In this case, offset locators allow you to define UI elements that "anchor" other elements. Given the following markup:</p> + +<pre><form name="contact_info"> + <input type="text" name="foo" /> + <textarea name="bar"></textarea> + <input type="submit" value="baz" /> +</form></pre> + +<p>Assume that a UI element has been defined for the form element. Then the following locators containing offset locators and "anchored" off this element would be recorded using the default offset locator function (<code>UIElement.defaultOffsetLocatorStrategy</code>):</p> + +<pre>ui=contactPages::contact_form()<span class="highlight">->//input[@name='foo']</span> +ui=contactPages::contact_form()<span class="highlight">->//textarea[@name='bar']</span> +ui=contactPages::contact_form()<span class="highlight">->//input[@value='baz']</span></pre> + +<p>The character sequence <code>-></code> serves to delimit the offset locator from the main locator. For this reason, the sequence should not appear in the main locator, or else ambiguity will result.</p> + +<p>When recording with the IDE, no preference is given to matching plain vanilla UI specifier strings over ones that have offset locators. In other words, if a page element could be specified both by a UI specifier string for one UI element, and by one augmented by an offset locator for a different UI element, there is no guarantee that one or the other locator will be recorded.</p> + +<p>Currently, <span class="highlight">only XPath is supported as an offset locator type</span>, as it is the only locator for which a context node can be specified at evaluation time. Other locator strategies may be supported in the future.</p> + +<h3>Rollup Rules</h3> + +<p>Question: Why use rollup rules? Answer: Remember the testcase from the "Getting Motivated" section above? With rollups, that testcase can be condensed into this:</p> + +<pre> +<tr> + <td>open</td> + <td>/</td> + <td></td> +</tr> +<tr> + <td>rollup</td> + <td><span class="highlight">navigate_to_subtopic_article</span></td> + <td><span class="highlight">index=2, subtopic=Creativity</span></td> +</tr> +</pre> + +<p>It's inevitable that certain sequences of Selenium commands will appear in testcases over and over again. When this happens, you might wish to group several fine-grained commands into a single coarser, more semantically meaningful action. In doing so, you would abstract out the execution details for the action, such that if they were to change at some point, you would have a single point of update. In UI-Element, such actions are given their own command, called <code>rollup</code>. In a sense, rollups are a natural extension of the <code>ui</code> locator.</p> + +<p>UI-Element is designed with the belief that the IDE can be a useful tool for writing testcases, and need not be shunned for lack of functionality. A corollary belief is that you should be able to drive your RC test in the language of your choice. The execution of the <code>rollup</code> command by the Selenium testrunner produces the component commands, which are executed in a new context, like a function call. The logic of the rollup "expansion" is written in javascript. Corresponding logic for inferring a rollup from a list of commands is also written in javascript. Thus, the logic can be incorporated into any of the family of Selenium products as a user extension. Most notably, the IDE is made viable as a testcase creation tool that understands both how rollups expand to commands, and also how rollup rules can be "applied" to commands to reduce them to rollups.</p> + +<p>Rollup rule definitions appear in this general format:</p> + +<pre> +var manager = new RollupManager(); + +manager.addRollupRule({ ... }); +manager.addRollupRule({ ... }); +... +</pre> + +<p>In a relatively simple form, a rollup rule looks like this:</p> + +<pre> +manager.addRollupRule({ + name: 'do_search' + , description: 'performs a search' + , args: [ + name: 'term' + , description: 'the search term' + ] + , commandMatchers: [ + { + command: 'type' + , target: 'ui=searchPages::search_box\\(.+' + , updateArgs: function(command, args) { + var uiSpecifier = new UISpecifier(command.target); + args.term = uiSpecifier.args.term; + return args; + } + } + , { + command: 'click.+' + , target: 'ui=searchPages::search_go\\(.+' + } + ] + , getExpandedCommands: function(args) { + var commands = []; + var uiSpecifier = new UISpecifier( + 'searchPages' + , 'search_box' + , { term: args.term }); + commands.push({ + command: 'type' + , target: 'ui=' + uiSpecifier.toString() + }); + commands.push({ + command: 'clickAndWait' + , target: 'ui=searchPages::search_go()' + }); + return commands; + } +}); +</pre> + +<p>In the above example, a rollup rule is defined for performing a search. The rule can be "applied" to two consecutive commands that match the <code>commandMatchers</code>. The rollup takes one argument, <code>term</code>, and expands back to the original two commands. One thing to note is that the second command matcher will match all commands starting with <code>click</code>. The rollup will expand that command to a <code>clickAndWait</code>.</p> + +<p>Here's a table containing information about the attributes of the rollup rule object.</p> + +<table> +<tr><th>Name</th> + <th>Required?</th> + <th>Description</th> + <th>Example</th> +</tr> +<tr><td>name</td> + <td>Yes</td> + <td>(String) the name of the rollup rule. This will be the target of the resulting <code>rollup</code> command.</td> + <td><pre>name: 'do_login'</pre></td> +</tr> +<tr><td>description</td> + <td>Yes</td> + <td>(String) a description for the rollup rule.</td> + <td><pre>description: 'logs into the application'</pre></td> +</tr> +<tr><td>alternateCommand</td> + <td>No</td> + <td>(String) specifies an alternate usage of rollup rules to replace commands. This string is used to replace the command name of the first matched command.</td> + <td><pre>alternateCommand: 'clickAndWait'</pre></td> +</tr> +<tr><td>pre</td> + <td>No</td> + <td>(String) a detailed summary of the preconditions that must be satisfied for the rollup to execute successfully. This metadata is easily viewable in the IDE when the rollup command is selected.</td> + <td><pre>pre: 'the page contains login widgets'</pre></td> +</tr> +<tr><td>post</td> + <td>No</td> + <td>(String) a detailed summary of the postconditions that will exist after the rollup has been executed.</td> + <td><pre>post: 'the user is logged in, or is \ +directed to a login error page'</pre></td> +</tr> +<tr><td>args</td> + <td>Conditional</td> + <td><p>(Array) a list of arguments that are used to modify the rollup expansion. These are similar to UI arguments, with the exception that <code>exampleValues</code> are provided, instead of <code>defaultValues</code>. Here, example values are used for reference purposes only; they are displayed in the rollup pane in the IDE.</p><p>This attribute may be omitted if no <code>updateArgs()</code> functions are defined for any command matchers.</td> + <td><pre>args: { + name: 'user' + , description: 'the username to login as' + , exampleValues: [ + 'John Doe' + , 'Jane Doe' + ] +}</pre></td> +</tr> +<tr><td>commandMatchers<br />getRollup()</td> + <td>Yes</td> + <td><p>(Array | Function) a list of command matcher definitions, or a function that, given a list of commands, returns either 1) a rollup command if the rule is considered to match the commands (starting at the first command); or 2) false. If a function, it should have the method signature <code>getRollup(commands)</code>, and the returned rollup command (if any) must have the <code>replacementIndexes</code> attribute, which is a list of array indexes indicating which commands in the <code>commands</code> parameter are to be replaced.</p> + <p>If you don't intend on using the IDE with your rollups, go ahead and set this to the empty array <code>[]</code>.</p></td> + <td><pre>commandMatchers: [ + { + command: 'type' + , target: 'ui=loginPages::user\\(.+' + , value: '.+' + , minMatches: 1 + , maxMatches: 1 + , updateArgs: + function(command, args) { + args.user = command.value; + return args; + } + } +]</pre> + <pre>// this is a simplistic example, roughy +// equivalent to the commandMatchers +// example above. The <span class="highlight">to_kwargs()</span> function +// is used to turn an arguments object into +// a keyword-arguments string. +getRollup: function(commands) { + var command = commands[0]; + var re = /^ui=loginPages::user\(.+/; + if (command.command == 'type' && + re.test(command.target) && + command.value) { + var args = { user: command.value }; + return { + command: 'rollup' + , target: this.name + , value: to_kwargs(args) + , replacementIndexes: [ 0 ] + }; + } + return false; +}</pre><em>See section below elaborating on command matcher objects.</em></td> +</tr> +<tr><td>expandedCommands<br />getExpandedCommands()</td> + <td>Yes</td> + <td><p>(Array | Function) a list of commands the rollup command expands into, or a function that, given an argument object mapping argument names to values, returns a list of expanded commands. If a function, it should have the method signature <code>getExpandedCommands(args)</code>.</p><p>Each command in the list of expanded commands should contain a <code>command</code> attribute, which is the name of the command, and optionally <code>target</code> and <code>value</code> attributes, depending on the type of command.</p><p>It is expected that providing a fixed list of expanded commands will be of limited use, as rollups will typically contain commands that have arguments, requiring more sophisticated logic to expand.</td> + <td><pre>expandedCommands: [ + { + command: 'check' + , target: 'ui=termsPages::agree\\(.+' + } + , { + command: 'clickAndWait' + , target: 'ui=termsPages::submit\\(.+' + } +]</pre> + <pre>getExpandedCommands: function(args) { + var commands = []; + commands.push({ + command: 'type' + , target: 'ui=loginPages::user()' + , value: args.user + }); + commands.push({ + command: 'type' + , target: 'ui=loginPages::pass()' + , value: args.pass + }); + commands.push({ + command: 'clickAndWait' + , target: 'ui=loginPages::submit()' + }); + commands.push({ + command: 'verifyLocation' + , target: 'regexp:.+/home' + }); + return commands; +}</pre> + <pre>// if using alternateCommand +expandedCommands: [] +</pre></td> +</tr> +</table> + +<p>The user should be able to freely record commands in the IDE, which can be collapsed into rollups at any point by applying the defined rollup rules. Healthy usage of the <code>ui</code> locator makes commands easy to match using command matcher definitions. Command matchers simplify the specification of a command match. In basic usage, for a rollup rule, you might specify 3 command matchers: <em>M1</em>, <em>M2</em>, and <em>M3</em>, that you intend to match 3 corresponding commands, <em>C1</em>, <em>C2</em>, and <em>C3</em>. In more complex usage, a single command matcher might match more than one command. For example, <em>M1</em> matches <em>C1</em> and <em>C2</em>, <em>M2</em> matches <em>C3</em>, and <em>M3</em> matches <em>C4</em>, <em>C5</em>, and <em>C6</em>. In the latter case, you would want to track the matches by updating argument values in the command matchers' <code>updateArgs()</code> methods.</p> + +<p>Here are the required and optional fields for command matcher objects:</p> + +<table> +<tr><th>Name</th> + <th>Required?</th> + <th>Description</th> + <th>Example</th> +</tr> +<tr><td>command</td> + <td>Yes</td> + <td>(String) a simplified regular expression string that matches the command name of a command. The special regexp characters <code>^</code> and <code>$</code> are automatically included at the beginning and end of the string, and therefore should not be explicitly provided.</td> + <td><pre>command: 'click.+'</pre> + <pre>command: 'rollup'</pre></td> +</tr> +<tr><td>target</td> + <td>Yes</td> + <td>(String) a simplified regular expression string that matches the target of a command. Same rules as for the <code>command</code> attribute.</td> + <td><pre>target: 'btnG'</pre> + <pre>// special regexp characters must be +// escaped in regexp strings. Backslashes +// always need to be escaped in javascript +// strings. +target: 'ui=loginPages::user\\(.+' + </pre></td> +</tr> +<tr><td>value</td> + <td>No</td> + <td>(String) a simplified regular expression string that matches the value of a command. Same rules as for the <code>command</code> attribute.</td> + <td><pre>value: '\\d+'</pre> + <pre>value: (?:foo|bar)</pre></td> +</tr> +<tr><td>minMatches</td> + <td>No</td> + <td>(Number) the minimum number of times this command matcher must match consecutive commands for the rollup rule to match a set of commands. If unspecified, the default is 1. If <code>maxMatches</code> is also specified, <code>minMatches</code> must be less than or equal to it.</td> + <td><pre>minMatches: 2</pre></td> +</tr> +<tr><td>maxMatches</td> + <td>No</td> + <td>(Number) the maximum number of times this command matcher is allowed to match consecutive commands for the rollup rule. If unspecified, the default is 1.</td> + <td><pre>maxMatches: 2</pre></td> +</tr> +<tr><td>updateArgs()</td> + <td>No</td> + <td><p>(Function) updates an arguments object when a match has been found, and returns the updated arguments object. This method is used to keep track of the way in which one or more commands were matched. When a rollup rule is successfully applied, any argument name-value pairs are stored as the rollup command's value. At time of expanding the rollup command, the command's value is converted back to an arguments object, which is passed to the rollup rule's <code>getExpandedCommands()</code> method.</p><p>This method must have the following method signature: <code>updateArgs(command, args)</code>, where <code>command</code> is the command object that was just matched, and <code>args</code> is the arguments object for the current trial application of the parent rollup rule.</td> + <td><pre>// reused from above +updateArgs: function(command, args) { + args.user = command.value; + return args; +}</pre> + <pre>// for multiple matches +updateArgs: function(command, args) { + if (!args.clickCount) { + args.clickCount = 0; + } + ++args.clickCount; + return args; +}</pre> + <pre>// another example from above (modified). +// If you need to parse a UI specifier, +// instantiate a new <span class="highlight">UISpecifier</span> object with the +// locator. To do it by the book, you should +// first strip off the "ui=" prefix. If you just +// want to inspect the UI specifier's args, you +// can safely skip this step. +updateArgs: function(command, args) { + var s = command.target.replace(/^ui=/, ''); + var uiSpecifier = new UISpecifier(s); + args.term = uiSpecifier.args.term; + return args; +}</pre> + <pre>// example from sample map file. If you're +// matching a rollup command that has arguments, +// you'll want to parse them. The easiest way +// to do this is with the <span class="highlight">parse_kwargs()</span> +// function, which has its roots in python. +updateArgs: function(command, args) { + var args1 = parse_kwargs(command.value); + args.subtopic = args1.subtopic; + return args; +}</pre></td> +</tr> +</table> + +<p>Too much mumbo jumbo?</p> + +<p>To see rollup rules in action in the IDE, use the included sample map with UI-Element (see instructions above in the "Including the Right Files" section), and grab the listing of 4 commands from the "Getting Motivated" section, above. Under the <em>Source</em> tab of the IDE, paste the commands in between the <code><tbody></code> and <code></tbody></code> tags. Now switch back to the <em>Table</em> tab, and click the new <span class="highlight">purple spiral button</span>; this is the "Apply rollup rules" button. If done correctly, you should be prompted when rollup rule matches are found. Go ahead - go to the <a href="http://alistapart.com">alistapart.com</a> site and try executing the rollups!</p> + +<h2>Release Notes</h2> + +<h3>Core-1.0</h3> + +<ul> +<li>UI-Element is now completely integrated into Selenium Core.</li> +<li>Added offset locators. Modified the delimiter to be <code>-></code>.</li> +<li><code>getDefaultValues()</code> can dynamically construct a list of values and assume that a <code>document</code> object is being passed in.</li> +<li>Arguments in UI specifier strings are presented in the same order as they are defined in the mapping file (no longer alphabetically).</li> +<li>Allow generic locators to be specified, potentially improving recording performance when there are many UI arguments.</li> +<li>Updated documentation.</li> +<li>Many other fixes.</li> +</ul> + +<h3>ui0.7</h3> + +<ul> +<li>Changed extensions id and homepage to avoid conflicting with standard Selenium IDE distribution.</li> +<li>Added rollup button.</li> +<li>Added UI-Element and Rollup panes, with beautiful colors and formatting.</li> +<li>Updated referenced version of Selenium Core to 0.8.3, and RC to 0.9.2 .</li> +<li>Added <code>quoteForXPath()</code> to <code>String</code> prototype.</li> +<li>Made XPath uppercasing much more robust. It is now backed by the ajaxslt library. Uppercasing is no longer required by UI-Element. It is used to demonstrate how XPath transformations may be used to improve XPath evaluation performance when using the javascript engine. See <a href="http://groups.google.com/group/google-ajax-discuss/browse_thread/thread/f7a7a2014a6415d4">this thread on XPath performance</a>. +<li>Added new <code>rollup</code> Selenium command and associated functionality with the RollupManager object.</li> +<li>Deprecated <code>getXPath()</code> and <code>xpath</code> in favor of <code>getLocator()</code> and <code>locator</code> in the UI-Element shorthand. Testcases now work with locator types other than XPath (implicit, CSS, etc.).</li> +<li>UI element testcases are now truly XHTML. All content is considered inner HTML of the html element, which is automatically generated. This supports the use of alternate locator types described in the above bullet.</li> +<li>Global variables introduced by UI-Element are now properties of the variable <code>GLOBAL</code>. You can now name your map <code>uiMap</code> if you wish.</li> +<li>Updated the sample map, including demonstration of implicit and CSS locators, Rollup Rules, and usage of <code>quoteForXPath()</code>.</li> +<li>Improved auto-population of target dropdown with UI element locators and rollup names. The population logic is as follows: when a command is loaded from a file or inserted without having been recorded, all UI element locators are shown. After a command recorded or executed, only UI element locators for pagesets that match the page at time of recording or execution will be shown.</li> +<li>Made UI element testcase args mandatory. This reduces the startup time for the IDE, and improves testcase readability.</li> +<li>Updated documentation. Added this release notes section.</li> +</ul> + +<h2>Final Thoughts</h2> + +<p>Catch UI-Element news in the <a href="http://ttwhy.org/home/blog/category/selenium/">Selenium category of my blog</a>. You can also find me on the <a href="http://clearspace.openqa.org/people/gyrm">OpenQA Forums</a>.</p> + +<p><em>- Haw-Bin Chai</em></p> + +</body> +</html> diff --git a/tests/selenium/selenium-lib/core/scripts/ui-element.js b/tests/selenium/selenium-lib/core/scripts/ui-element.js new file mode 100644 index 00000000..4076a259 --- /dev/null +++ b/tests/selenium/selenium-lib/core/scripts/ui-element.js @@ -0,0 +1,1627 @@ +//******************************************************************************
+// Globals, including constants
+
+var UI_GLOBAL = {
+ UI_PREFIX: 'ui'
+ , XHTML_DOCTYPE: '<!DOCTYPE html PUBLIC '
+ + '"-//W3C//DTD XHTML 1.0 Strict//EN" '
+ + '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
+ , XHTML_XMLNS: 'http://www.w3.org/1999/xhtml'
+};
+
+//*****************************************************************************
+// Exceptions
+
+function UIElementException(message)
+{
+ this.message = message;
+ this.name = 'UIElementException';
+}
+
+function UIArgumentException(message)
+{
+ this.message = message;
+ this.name = 'UIArgumentException';
+}
+
+function PagesetException(message)
+{
+ this.message = message;
+ this.name = 'PagesetException';
+}
+
+function UISpecifierException(message)
+{
+ this.message = message;
+ this.name = 'UISpecifierException';
+}
+
+function CommandMatcherException(message)
+{
+ this.message = message;
+ this.name = 'CommandMatcherException';
+}
+
+//*****************************************************************************
+// UI-Element core
+
+/**
+ * The UIElement object. This has been crafted along with UIMap to make
+ * specifying UI elements using JSON as simple as possible. Object construction
+ * will fail if 1) a proper name isn't provided, 2) a faulty args argument is
+ * given, or 3) getLocator() returns undefined for a valid permutation of
+ * default argument values. See ui-doc.html for the documentation on the
+ * builder syntax.
+ *
+ * @param uiElementShorthand an object whose contents conform to the
+ * UI-Element builder syntax.
+ *
+ * @return a new UIElement object
+ * @throws UIElementException
+ */
+function UIElement(uiElementShorthand)
+{
+ // a shorthand object might look like:
+ //
+ // {
+ // name: 'topic'
+ // , description: 'sidebar links to topic categories'
+ // , args: [
+ // {
+ // name: 'name'
+ // , description: 'the name of the topic'
+ // , defaultValues: topLevelTopics
+ // }
+ // ]
+ // , getLocator: function(args) {
+ // return this._listXPath +
+ // "/a[text()=" + args.name.quoteForXPath() + "]";
+ // }
+ // , getGenericLocator: function() {
+ // return this._listXPath + '/a';
+ // }
+ // // maintain testcases for getLocator()
+ // , testcase1: {
+ // // defaultValues used if args not specified
+ // args: { name: 'foo' }
+ // , xhtml: '<div id="topiclist">'
+ // + '<ul><li><a expected-result="1">foo</a></li></ul>'
+ // + '</div>'
+ // }
+ // // set a local element variable
+ // , _listXPath: "//div[@id='topiclist']/ul/li"
+ // }
+ //
+ // name cannot be null or an empty string. Enforce the same requirement for
+ // the description.
+
+ /**
+ * Recursively returns all permutations of argument-value pairs, given
+ * a list of argument definitions. Each argument definition will have
+ * a set of default values to use in generating said pairs. If an argument
+ * has no default values defined, it will not be included among the
+ * permutations.
+ *
+ * @param args a list of UIArguments
+ * @param opt_inDocument (optional)
+ * @return a list of associative arrays containing key value pairs
+ */
+ this.permuteArgs = function(args, opt_inDocument) {
+ var permutations = [];
+ for (var i = 0; i < args.length; ++i) {
+ var arg = args[i];
+ var defaultValues = (arguments.length > 1)
+ ? arg.getDefaultValues(opt_inDocument)
+ : arg.getDefaultValues();
+
+ // skip arguments for which no default values are defined
+ if (defaultValues.length == 0) {
+ continue;
+ }
+ for (var j = 0; j < defaultValues.length; ++j) {
+ var value = defaultValues[j];
+ var nextPermutations = this.permuteArgs(args.slice(i+1));
+ if (nextPermutations.length == 0) {
+ var permutation = {};
+ permutation[arg.name] = value + ''; // make into string
+ permutations.push(permutation);
+ }
+ else {
+ for (var k = 0; k < nextPermutations.length; ++k) {
+ nextPermutations[k][arg.name] = value + '';
+ permutations.push(nextPermutations[k]);
+ }
+ }
+ }
+ break;
+ }
+ return permutations;
+ }
+
+
+
+ /**
+ * Returns a list of all testcases for this UIElement.
+ */
+ this.getTestcases = function()
+ {
+ return this.testcases;
+ }
+
+
+
+ /**
+ * Run all unit tests, stopping at the first failure, if any. Return true
+ * if no failures encountered, false otherwise. See the following thread
+ * regarding use of getElementById() on XML documents created by parsing
+ * text via the DOMParser:
+ *
+ * http://groups.google.com/group/comp.lang.javascript/browse_thread/thread/2b1b82b3c53a1282/
+ */
+ this.test = function()
+ {
+ var parser = new DOMParser();
+ var testcases = this.getTestcases();
+ testcaseLoop: for (var i = 0; i < testcases.length; ++i) {
+ var testcase = testcases[i];
+ var xhtml = UI_GLOBAL.XHTML_DOCTYPE + '<html xmlns="'
+ + UI_GLOBAL.XHTML_XMLNS + '">' + testcase.xhtml + '</html>';
+ var doc = parser.parseFromString(xhtml, "text/xml");
+ if (doc.firstChild.nodeName == 'parsererror') {
+ safe_alert('Error parsing XHTML in testcase "' + testcase.name
+ + '" for UI element "' + this.name + '": ' + "\n"
+ + doc.firstChild.firstChild.nodeValue);
+ }
+
+ // we're no longer using the default locators when testing, because
+ // args is now required
+ var locator = parse_locator(this.getLocator(testcase.args));
+ var results;
+ if (locator.type == 'xpath' || (locator.type == 'implicit' &&
+ locator.string.substring(0, 2) == '//')) {
+ // try using the javascript xpath engine to avoid namespace
+ // issues. The xpath does have to be lowercase however, it
+ // seems.
+ results = eval_xpath(locator.string, doc,
+ { allowNativeXpath: false, returnOnFirstMatch: true });
+ }
+ else {
+ // piece the locator back together
+ locator = (locator.type == 'implicit')
+ ? locator.string
+ : locator.type + '=' + locator.string;
+ results = eval_locator(locator, doc);
+ }
+ if (results.length && results[0].hasAttribute('expected-result')) {
+ continue testcaseLoop;
+ }
+
+ // testcase failed
+ if (is_IDE()) {
+ var msg = 'Testcase "' + testcase.name
+ + '" failed for UI element "' + this.name + '":';
+ if (!results.length) {
+ msg += '\n"' + locator + '" did not match any elements!';
+ }
+ else {
+ msg += '\n' + results[0] + ' was not the expected result!';
+ }
+ safe_alert(msg);
+ }
+ return false;
+ }
+ return true;
+ };
+
+
+
+ /**
+ * Creates a set of locators using permutations of default values for
+ * arguments used in the locator construction. The set is returned as an
+ * object mapping locators to key-value arguments objects containing the
+ * values passed to getLocator() to create the locator.
+ *
+ * @param opt_inDocument (optional) the document object of the "current"
+ * page when this method is invoked. Some arguments
+ * may have default value lists that are calculated
+ * based on the contents of the page.
+ *
+ * @return a list of locator strings
+ * @throws UIElementException
+ */
+ this.getDefaultLocators = function(opt_inDocument) {
+ var defaultLocators = {};
+ if (this.args.length == 0) {
+ defaultLocators[this.getLocator({})] = {};
+ }
+ else {
+ var permutations = this.permuteArgs(this.args, opt_inDocument);
+ if (permutations.length != 0) {
+ for (var i = 0; i < permutations.length; ++i) {
+ var args = permutations[i];
+ var locator = this.getLocator(args);
+ if (!locator) {
+ throw new UIElementException('Error in UIElement(): '
+ + 'no getLocator return value for element "' + name
+ + '"');
+ }
+ defaultLocators[locator] = args;
+ }
+ }
+ else {
+ // try using no arguments. If it doesn't work, fine.
+ try {
+ var locator = this.getLocator();
+ defaultLocators[locator] = {};
+ }
+ catch (e) {
+ safe_log('debug', e.message);
+ }
+ }
+ }
+ return defaultLocators;
+ };
+
+
+
+ /**
+ * Validate the structure of the shorthand notation this object is being
+ * initialized with. Throws an exception if there's a validation error.
+ *
+ * @param uiElementShorthand
+ *
+ * @throws UIElementException
+ */
+ this.validate = function(uiElementShorthand)
+ {
+ var msg = "UIElement validation error:\n" + print_r(uiElementShorthand);
+ if (!uiElementShorthand.name) {
+ throw new UIElementException(msg + 'no name specified!');
+ }
+ if (!uiElementShorthand.description) {
+ throw new UIElementException(msg + 'no description specified!');
+ }
+ if (!uiElementShorthand.locator
+ && !uiElementShorthand.getLocator
+ && !uiElementShorthand.xpath
+ && !uiElementShorthand.getXPath) {
+ throw new UIElementException(msg + 'no locator specified!');
+ }
+ };
+
+
+
+ this.init = function(uiElementShorthand)
+ {
+ this.validate(uiElementShorthand);
+
+ this.name = uiElementShorthand.name;
+ this.description = uiElementShorthand.description;
+
+ // construct a new getLocator() method based on the locator property,
+ // or use the provided function. We're deprecating the xpath property
+ // and getXPath() function, but still allow for them for backwards
+ // compatability.
+ if (uiElementShorthand.locator) {
+ this.getLocator = function(args) {
+ return uiElementShorthand.locator;
+ };
+ }
+ else if (uiElementShorthand.getLocator) {
+ this.getLocator = uiElementShorthand.getLocator;
+ }
+ else if (uiElementShorthand.xpath) {
+ this.getLocator = function(args) {
+ return uiElementShorthand.xpath;
+ };
+ }
+ else {
+ this.getLocator = uiElementShorthand.getXPath;
+ }
+
+ if (uiElementShorthand.genericLocator) {
+ this.getGenericLocator = function() {
+ return uiElementShorthand.genericLocator;
+ };
+ }
+ else if (uiElementShorthand.getGenericLocator) {
+ this.getGenericLocator = uiElementShorthand.getGenericLocator;
+ }
+
+ if (uiElementShorthand.getOffsetLocator) {
+ this.getOffsetLocator = uiElementShorthand.getOffsetLocator;
+ }
+
+ // get the testcases and local variables
+ this.testcases = [];
+ var localVars = {};
+ for (var attr in uiElementShorthand) {
+ if (attr.match(/^testcase/)) {
+ var testcase = uiElementShorthand[attr];
+ if (uiElementShorthand.args &&
+ uiElementShorthand.args.length && !testcase.args) {
+ safe_alert('No args defined in ' + attr + ' for UI element '
+ + this.name + '! Skipping testcase.');
+ continue;
+ }
+ testcase.name = attr;
+ this.testcases.push(testcase);
+ }
+ else if (attr.match(/^_/)) {
+ this[attr] = uiElementShorthand[attr];
+ localVars[attr] = uiElementShorthand[attr];
+ }
+ }
+
+ // create the arguments
+ this.args = []
+ this.argsOrder = [];
+ if (uiElementShorthand.args) {
+ for (var i = 0; i < uiElementShorthand.args.length; ++i) {
+ var arg = new UIArgument(uiElementShorthand.args[i], localVars);
+ this.args.push(arg);
+ this.argsOrder.push(arg.name);
+
+ // if an exception is thrown when invoking getDefaultValues()
+ // with no parameters passed in, assume the method requires an
+ // inDocument parameter, and thus may only be invoked at run
+ // time. Mark the UI element object accordingly.
+ try {
+ arg.getDefaultValues();
+ }
+ catch (e) {
+ this.isDefaultLocatorConstructionDeferred = true;
+ }
+ }
+
+ }
+
+ if (!this.isDefaultLocatorConstructionDeferred) {
+ this.defaultLocators = this.getDefaultLocators();
+ }
+ };
+
+
+
+ this.init(uiElementShorthand);
+}
+
+// hang this off the UIElement "namespace". This is a composite strategy.
+UIElement.defaultOffsetLocatorStrategy = function(locatedElement, pageElement) {
+ var strategies = [
+ UIElement.linkXPathOffsetLocatorStrategy
+ , UIElement.preferredAttributeXPathOffsetLocatorStrategy
+ , UIElement.simpleXPathOffsetLocatorStrategy
+ ];
+
+ for (var i = 0; i < strategies.length; ++i) {
+ var strategy = strategies[i];
+ var offsetLocator = strategy(locatedElement, pageElement);
+
+ if (offsetLocator) {
+ return offsetLocator;
+ }
+ }
+
+ return null;
+};
+
+UIElement.simpleXPathOffsetLocatorStrategy = function(locatedElement,
+ pageElement)
+{
+ if (is_ancestor(locatedElement, pageElement)) {
+ var xpath = "";
+ var recorder = Recorder.get(locatedElement.ownerDocument.defaultView);
+ var locatorBuilders = recorder.locatorBuilders;
+ var currentNode = pageElement;
+
+ while (currentNode != null && currentNode != locatedElement) {
+ xpath = locatorBuilders.relativeXPathFromParent(currentNode)
+ + xpath;
+ currentNode = currentNode.parentNode;
+ }
+
+ var results = eval_xpath(xpath, locatedElement.ownerDocument,
+ { contextNode: locatedElement });
+
+ if (results.length > 0 && results[0] == pageElement) {
+ return xpath;
+ }
+ }
+
+ return null;
+};
+
+UIElement.linkXPathOffsetLocatorStrategy = function(locatedElement, pageElement)
+{
+ if (pageElement.nodeName == 'A' && is_ancestor(locatedElement, pageElement))
+ {
+ var text = pageElement.textContent
+ .replace(/^\s+/, "")
+ .replace(/\s+$/, "");
+
+ if (text) {
+ var xpath = '/descendant::a[normalize-space()='
+ + text.quoteForXPath() + ']';
+
+ var results = eval_xpath(xpath, locatedElement.ownerDocument,
+ { contextNode: locatedElement });
+
+ if (results.length > 0 && results[0] == pageElement) {
+ return xpath;
+ }
+ }
+ }
+
+ return null;
+};
+
+// compare to the "xpath:attributes" locator strategy defined in the IDE source
+UIElement.preferredAttributeXPathOffsetLocatorStrategy =
+ function(locatedElement, pageElement)
+{
+ // this is an ordered listing of single attributes
+ var preferredAttributes = [
+ 'name'
+ , 'value'
+ , 'type'
+ , 'action'
+ , 'alt'
+ , 'title'
+ , 'class'
+ , 'src'
+ , 'href'
+ , 'onclick'
+ ];
+
+ if (is_ancestor(locatedElement, pageElement)) {
+ var xpathBase = '/descendant::' + pageElement.nodeName.toLowerCase();
+
+ for (var i = 0; i < preferredAttributes.length; ++i) {
+ var name = preferredAttributes[i];
+ var value = pageElement.getAttribute(name);
+
+ if (value) {
+ var xpath = xpathBase + '[@' + name + '='
+ + value.quoteForXPath() + ']';
+
+ var results = eval_xpath(xpath, locatedElement.ownerDocument,
+ { contextNode: locatedElement });
+
+ if (results.length > 0 && results[0] == pageElement) {
+ return xpath;
+ }
+ }
+ }
+ }
+
+ return null;
+};
+
+
+
+/**
+ * Constructs a UIArgument. This is mostly for checking that the values are
+ * valid.
+ *
+ * @param uiArgumentShorthand
+ * @param localVars
+ *
+ * @throws UIArgumentException
+ */
+function UIArgument(uiArgumentShorthand, localVars)
+{
+ /**
+ * @param uiArgumentShorthand
+ *
+ * @throws UIArgumentException
+ */
+ this.validate = function(uiArgumentShorthand)
+ {
+ var msg = "UIArgument validation error:\n"
+ + print_r(uiArgumentShorthand);
+
+ // try really hard to throw an exception!
+ if (!uiArgumentShorthand.name) {
+ throw new UIArgumentException(msg + 'no name specified!');
+ }
+ if (!uiArgumentShorthand.description) {
+ throw new UIArgumentException(msg + 'no description specified!');
+ }
+ if (!uiArgumentShorthand.defaultValues &&
+ !uiArgumentShorthand.getDefaultValues) {
+ throw new UIArgumentException(msg + 'no default values specified!');
+ }
+ };
+
+
+
+ /**
+ * @param uiArgumentShorthand
+ * @param localVars a list of local variables
+ */
+ this.init = function(uiArgumentShorthand, localVars)
+ {
+ this.validate(uiArgumentShorthand);
+
+ this.name = uiArgumentShorthand.name;
+ this.description = uiArgumentShorthand.description;
+
+ if (uiArgumentShorthand.defaultValues) {
+ var defaultValues = uiArgumentShorthand.defaultValues;
+ this.getDefaultValues =
+ function() { return defaultValues; }
+ }
+ else {
+ this.getDefaultValues = uiArgumentShorthand.getDefaultValues;
+ }
+
+ for (var name in localVars) {
+ this[name] = localVars[name];
+ }
+ }
+
+
+
+ this.init(uiArgumentShorthand, localVars);
+}
+
+
+
+/**
+ * The UISpecifier constructor is overloaded. If less than three arguments are
+ * provided, the first argument will be considered a UI specifier string, and
+ * will be split out accordingly. Otherwise, the first argument will be
+ * considered the path.
+ *
+ * @param uiSpecifierStringOrPagesetName a UI specifier string, or the pageset
+ * name of the UI specifier
+ * @param elementName the name of the element
+ * @param args an object associating keys to values
+ *
+ * @return new UISpecifier object
+ */
+function UISpecifier(uiSpecifierStringOrPagesetName, elementName, args)
+{
+ /**
+ * Initializes this object from a UI specifier string of the form:
+ *
+ * pagesetName::elementName(arg1=value1, arg2=value2, ...)
+ *
+ * into its component parts, and returns them as an object.
+ *
+ * @return an object containing the components of the UI specifier
+ * @throws UISpecifierException
+ */
+ this._initFromUISpecifierString = function(uiSpecifierString) {
+ var matches = /^(.*)::([^\(]+)\((.*)\)$/.exec(uiSpecifierString);
+ if (matches == null) {
+ throw new UISpecifierException('Error in '
+ + 'UISpecifier._initFromUISpecifierString(): "'
+ + this.string + '" is not a valid UI specifier string');
+ }
+ this.pagesetName = matches[1];
+ this.elementName = matches[2];
+ this.args = (matches[3]) ? parse_kwargs(matches[3]) : {};
+ };
+
+
+
+ /**
+ * Override the toString() method to return the UI specifier string when
+ * evaluated in a string context. Combines the UI specifier components into
+ * a canonical UI specifier string and returns it.
+ *
+ * @return a UI specifier string
+ */
+ this.toString = function() {
+ // empty string is acceptable for the path, but it must be defined
+ if (this.pagesetName == undefined) {
+ throw new UISpecifierException('Error in UISpecifier.toString(): "'
+ + this.pagesetName + '" is not a valid UI specifier pageset '
+ + 'name');
+ }
+ if (!this.elementName) {
+ throw new UISpecifierException('Error in UISpecifier.unparse(): "'
+ + this.elementName + '" is not a valid UI specifier element '
+ + 'name');
+ }
+ if (!this.args) {
+ throw new UISpecifierException('Error in UISpecifier.unparse(): "'
+ + this.args + '" are not valid UI specifier args');
+ }
+
+ uiElement = UIMap.getInstance()
+ .getUIElement(this.pagesetName, this.elementName);
+ if (uiElement != null) {
+ var kwargs = to_kwargs(this.args, uiElement.argsOrder);
+ }
+ else {
+ // probably under unit test
+ var kwargs = to_kwargs(this.args);
+ }
+
+ return this.pagesetName + '::' + this.elementName + '(' + kwargs + ')';
+ };
+
+ // construct the object
+ if (arguments.length < 2) {
+ this._initFromUISpecifierString(uiSpecifierStringOrPagesetName);
+ }
+ else {
+ this.pagesetName = uiSpecifierStringOrPagesetName;
+ this.elementName = elementName;
+ this.args = (args) ? clone(args) : {};
+ }
+}
+
+
+
+function Pageset(pagesetShorthand)
+{
+ /**
+ * Returns true if the page is included in this pageset, false otherwise.
+ * The page is specified by a document object.
+ *
+ * @param inDocument the document object representing the page
+ */
+ this.contains = function(inDocument)
+ {
+ var urlParts = parseUri(unescape(inDocument.location.href));
+ var path = urlParts.path
+ .replace(/^\//, "")
+ .replace(/\/$/, "");
+ if (!this.pathRegexp.test(path)) {
+ return false;
+ }
+ for (var paramName in this.paramRegexps) {
+ var paramRegexp = this.paramRegexps[paramName];
+ if (!paramRegexp.test(urlParts.queryKey[paramName])) {
+ return false;
+ }
+ }
+ if (!this.pageContent(inDocument)) {
+ return false;
+ }
+
+ return true;
+ }
+
+
+
+ this.getUIElements = function()
+ {
+ var uiElements = [];
+ for (var uiElementName in this.uiElements) {
+ uiElements.push(this.uiElements[uiElementName]);
+ }
+ return uiElements;
+ };
+
+
+
+ /**
+ * Returns a list of UI specifier string stubs representing all UI elements
+ * for this pageset. Stubs contain all required arguments, but leave
+ * argument values blank. Each element stub is paired with the element's
+ * description.
+ *
+ * @return a list of UI specifier string stubs
+ */
+ this.getUISpecifierStringStubs = function()
+ {
+ var stubs = [];
+ for (var name in this.uiElements) {
+ var uiElement = this.uiElements[name];
+ var args = {};
+ for (var i = 0; i < uiElement.args.length; ++i) {
+ args[uiElement.args[i].name] = '';
+ }
+ var uiSpecifier = new UISpecifier(this.name, uiElement.name, args);
+ stubs.push([
+ UI_GLOBAL.UI_PREFIX + '=' + uiSpecifier.toString()
+ , uiElement.description
+ ]);
+ }
+ return stubs;
+ }
+
+
+
+ /**
+ * Throws an exception on validation failure.
+ */
+ this._validate = function(pagesetShorthand)
+ {
+ var msg = "Pageset validation error:\n"
+ + print_r(pagesetShorthand);
+ if (!pagesetShorthand.name) {
+ throw new PagesetException(msg + 'no name specified!');
+ }
+ if (!pagesetShorthand.description) {
+ throw new PagesetException(msg + 'no description specified!');
+ }
+ if (!pagesetShorthand.paths &&
+ !pagesetShorthand.pathRegexp &&
+ !pagesetShorthand.pageContent) {
+ throw new PagesetException(msg
+ + 'no path, pathRegexp, or pageContent specified!');
+ }
+ };
+
+
+
+ this.init = function(pagesetShorthand)
+ {
+ this._validate(pagesetShorthand);
+
+ this.name = pagesetShorthand.name;
+ this.description = pagesetShorthand.description;
+
+ var pathPrefixRegexp = pagesetShorthand.pathPrefix
+ ? RegExp.escape(pagesetShorthand.pathPrefix) : "";
+ var pathRegexp = '^' + pathPrefixRegexp;
+
+ if (pagesetShorthand.paths != undefined) {
+ pathRegexp += '(?:';
+ for (var i = 0; i < pagesetShorthand.paths.length; ++i) {
+ if (i > 0) {
+ pathRegexp += '|';
+ }
+ pathRegexp += RegExp.escape(pagesetShorthand.paths[i]);
+ }
+ pathRegexp += ')$';
+ }
+ else if (pagesetShorthand.pathRegexp) {
+ pathRegexp += '(?:' + pagesetShorthand.pathRegexp + ')$';
+ }
+
+ this.pathRegexp = new RegExp(pathRegexp);
+ this.paramRegexps = {};
+ for (var paramName in pagesetShorthand.paramRegexps) {
+ this.paramRegexps[paramName] =
+ new RegExp(pagesetShorthand.paramRegexps[paramName]);
+ }
+ this.pageContent = pagesetShorthand.pageContent ||
+ function() { return true; };
+ this.uiElements = {};
+ };
+
+
+
+ this.init(pagesetShorthand);
+}
+
+
+
+/**
+ * Construct the UI map object, and return it. Once the object is instantiated,
+ * it binds to a global variable and will not leave scope.
+ *
+ * @return new UIMap object
+ */
+function UIMap()
+{
+ // the singleton pattern, split into two parts so that "new" can still
+ // be used, in addition to "getInstance()"
+ UIMap.self = this;
+
+ // need to attach variables directly to the Editor object in order for them
+ // to be in scope for Editor methods
+ if (is_IDE()) {
+ Editor.uiMap = this;
+ Editor.UI_PREFIX = UI_GLOBAL.UI_PREFIX;
+ }
+
+ this.pagesets = new Object();
+
+
+
+ /**
+ * pageset[pagesetName]
+ * regexp
+ * elements[elementName]
+ * UIElement
+ */
+ this.addPageset = function(pagesetShorthand)
+ {
+ try {
+ var pageset = new Pageset(pagesetShorthand);
+ }
+ catch (e) {
+ safe_alert("Could not create pageset from shorthand:\n"
+ + print_r(pagesetShorthand) + "\n" + e.message);
+ return false;
+ }
+
+ if (this.pagesets[pageset.name]) {
+ safe_alert('Could not add pageset "' + pageset.name
+ + '": a pageset with that name already exists!');
+ return false;
+ }
+
+ this.pagesets[pageset.name] = pageset;
+ return true;
+ };
+
+
+
+ /**
+ * @param pagesetName
+ * @param uiElementShorthand a representation of a UIElement object in
+ * shorthand JSON.
+ */
+ this.addElement = function(pagesetName, uiElementShorthand)
+ {
+ try {
+ var uiElement = new UIElement(uiElementShorthand);
+ }
+ catch (e) {
+ safe_alert("Could not create UI element from shorthand:\n"
+ + print_r(uiElementShorthand) + "\n" + e.message);
+ return false;
+ }
+
+ // run the element's unit tests only for the IDE, and only when the
+ // IDE is starting. Make a rough guess as to the latter condition.
+ if (is_IDE() && !editor.selDebugger && !uiElement.test()) {
+ safe_alert('Could not add UI element "' + uiElement.name
+ + '": failed testcases!');
+ return false;
+ }
+
+ try {
+ this.pagesets[pagesetName].uiElements[uiElement.name] = uiElement;
+ }
+ catch (e) {
+ safe_alert("Could not add UI element '" + uiElement.name
+ + "' to pageset '" + pagesetName + "':\n" + e.message);
+ return false;
+ }
+
+ return true;
+ };
+
+
+
+ /**
+ * Returns the pageset for a given UI specifier string.
+ *
+ * @param uiSpecifierString
+ * @return a pageset object
+ */
+ this.getPageset = function(uiSpecifierString)
+ {
+ try {
+ var uiSpecifier = new UISpecifier(uiSpecifierString);
+ return this.pagesets[uiSpecifier.pagesetName];
+ }
+ catch (e) {
+ return null;
+ }
+ }
+
+
+
+ /**
+ * Returns the UIElement that a UISpecifierString or pageset and element
+ * pair refer to.
+ *
+ * @param pagesetNameOrUISpecifierString
+ * @return a UIElement, or null if none is found associated with
+ * uiSpecifierString
+ */
+ this.getUIElement = function(pagesetNameOrUISpecifierString, uiElementName)
+ {
+ var pagesetName = pagesetNameOrUISpecifierString;
+ if (arguments.length == 1) {
+ var uiSpecifierString = pagesetNameOrUISpecifierString;
+ try {
+ var uiSpecifier = new UISpecifier(uiSpecifierString);
+ pagesetName = uiSpecifier.pagesetName;
+ var uiElementName = uiSpecifier.elementName;
+ }
+ catch (e) {
+ return null;
+ }
+ }
+ try {
+ return this.pagesets[pagesetName].uiElements[uiElementName];
+ }
+ catch (e) {
+ return null;
+ }
+ };
+
+
+
+ /**
+ * Returns a list of pagesets that "contains" the provided page,
+ * represented as a document object. Containership is defined by the
+ * Pageset object's contain() method.
+ *
+ * @param inDocument the page to get pagesets for
+ * @return a list of pagesets
+ */
+ this.getPagesetsForPage = function(inDocument)
+ {
+ var pagesets = [];
+ for (var pagesetName in this.pagesets) {
+ var pageset = this.pagesets[pagesetName];
+ if (pageset.contains(inDocument)) {
+ pagesets.push(pageset);
+ }
+ }
+ return pagesets;
+ };
+
+
+
+ /**
+ * Returns a list of all pagesets.
+ *
+ * @return a list of pagesets
+ */
+ this.getPagesets = function()
+ {
+ var pagesets = [];
+ for (var pagesetName in this.pagesets) {
+ pagesets.push(this.pagesets[pagesetName]);
+ }
+ return pagesets;
+ };
+
+
+
+ /**
+ * Returns a list of elements on a page that a given UI specifier string,
+ * maps to. If no elements are mapped to, returns an empty list..
+ *
+ * @param uiSpecifierString a String that specifies a UI element with
+ * attendant argument values
+ * @param inDocument the document object the specified UI element
+ * appears in
+ * @return a potentially-empty list of elements
+ * specified by uiSpecifierString
+ */
+ this.getPageElements = function(uiSpecifierString, inDocument)
+ {
+ var locator = this.getLocator(uiSpecifierString);
+ var results = locator ? eval_locator(locator, inDocument) : [];
+ return results;
+ };
+
+
+
+ /**
+ * Returns the locator string that a given UI specifier string maps to, or
+ * null if it cannot be mapped.
+ *
+ * @param uiSpecifierString
+ */
+ this.getLocator = function(uiSpecifierString)
+ {
+ try {
+ var uiSpecifier = new UISpecifier(uiSpecifierString);
+ }
+ catch (e) {
+ safe_alert('Could not create UISpecifier for string "'
+ + uiSpecifierString + '": ' + e.message);
+ return null;
+ }
+
+ var uiElement = this.getUIElement(uiSpecifier.pagesetName,
+ uiSpecifier.elementName);
+ try {
+ return uiElement.getLocator(uiSpecifier.args);
+ }
+ catch (e) {
+ return null;
+ }
+ }
+
+
+
+ /**
+ * Finds and returns a UI specifier string given an element and the page
+ * that it appears on.
+ *
+ * @param pageElement the document element to map to a UI specifier
+ * @param inDocument the document the element appears in
+ * @return a UI specifier string, or false if one cannot be
+ * constructed
+ */
+ this.getUISpecifierString = function(pageElement, inDocument)
+ {
+ var is_fuzzy_match =
+ BrowserBot.prototype.locateElementByUIElement.is_fuzzy_match;
+ var pagesets = this.getPagesetsForPage(inDocument);
+
+ for (var i = 0; i < pagesets.length; ++i) {
+ var pageset = pagesets[i];
+ var uiElements = pageset.getUIElements();
+
+ for (var j = 0; j < uiElements.length; ++j) {
+ var uiElement = uiElements[j];
+
+ // first test against the generic locator, if there is one.
+ // This should net some performance benefit when recording on
+ // more complicated pages.
+ if (uiElement.getGenericLocator) {
+ var passedTest = false;
+ var results =
+ eval_locator(uiElement.getGenericLocator(), inDocument);
+ for (var i = 0; i < results.length; ++i) {
+ if (results[i] == pageElement) {
+ passedTest = true;
+ break;
+ }
+ }
+ if (!passedTest) {
+ continue;
+ }
+ }
+
+ var defaultLocators;
+ if (uiElement.isDefaultLocatorConstructionDeferred) {
+ defaultLocators = uiElement.getDefaultLocators(inDocument);
+ }
+ else {
+ defaultLocators = uiElement.defaultLocators;
+ }
+
+ //safe_alert(print_r(uiElement.defaultLocators));
+ for (var locator in defaultLocators) {
+ var locatedElements = eval_locator(locator, inDocument);
+ if (locatedElements.length) {
+ var locatedElement = locatedElements[0];
+ }
+ else {
+ continue;
+ }
+
+ // use a heuristic to determine whether the element
+ // specified is the "same" as the element we're matching
+ if (is_fuzzy_match) {
+ if (is_fuzzy_match(locatedElement, pageElement)) {
+ return UI_GLOBAL.UI_PREFIX + '=' +
+ new UISpecifier(pageset.name, uiElement.name,
+ defaultLocators[locator]);
+ }
+ }
+ else {
+ if (locatedElement == pageElement) {
+ return UI_GLOBAL.UI_PREFIX + '=' +
+ new UISpecifier(pageset.name, uiElement.name,
+ defaultLocators[locator]);
+ }
+ }
+
+ // ok, matching the element failed. See if an offset
+ // locator can complete the match.
+ if (uiElement.getOffsetLocator) {
+ for (var k = 0; k < locatedElements.length; ++k) {
+ var offsetLocator = uiElement
+ .getOffsetLocator(locatedElements[k], pageElement);
+ if (offsetLocator) {
+ return UI_GLOBAL.UI_PREFIX + '=' +
+ new UISpecifier(pageset.name,
+ uiElement.name,
+ defaultLocators[locator])
+ + '->' + offsetLocator;
+ }
+ }
+ }
+ }
+ }
+ }
+ return false;
+ };
+
+
+
+ /**
+ * Returns a sorted list of UI specifier string stubs representing possible
+ * UI elements for all pagesets, paired the their descriptions. Stubs
+ * contain all required arguments, but leave argument values blank.
+ *
+ * @return a list of UI specifier string stubs
+ */
+ this.getUISpecifierStringStubs = function() {
+ var stubs = [];
+ var pagesets = this.getPagesets();
+ for (var i = 0; i < pagesets.length; ++i) {
+ stubs = stubs.concat(pagesets[i].getUISpecifierStringStubs());
+ }
+ stubs.sort(function(a, b) {
+ if (a[0] < b[0]) {
+ return -1;
+ }
+ return a[0] == b[0] ? 0 : 1;
+ });
+ return stubs;
+ }
+}
+
+UIMap.getInstance = function() {
+ return (UIMap.self == null) ? new UIMap() : UIMap.self;
+}
+
+//******************************************************************************
+// Rollups
+
+/**
+ * The Command object isn't available in the Selenium RC. We introduce an
+ * object with the identical constructor. In the IDE, this will be redefined,
+ * which is just fine.
+ *
+ * @param command
+ * @param target
+ * @param value
+ */
+if (typeof(Command) == 'undefined') {
+ function Command(command, target, value) {
+ this.command = command != null ? command : '';
+ this.target = target != null ? target : '';
+ this.value = value != null ? value : '';
+ }
+}
+
+
+
+/**
+ * A CommandMatcher object matches commands during the application of a
+ * RollupRule. It's specified with a shorthand format, for example:
+ *
+ * new CommandMatcher({
+ * command: 'click'
+ * , target: 'ui=allPages::.+'
+ * })
+ *
+ * which is intended to match click commands whose target is an element in the
+ * allPages PageSet. The matching expressions are given as regular expressions;
+ * in the example above, the command must be "click"; "clickAndWait" would be
+ * acceptable if 'click.*' were used. Here's a more complete example:
+ *
+ * new CommandMatcher({
+ * command: 'type'
+ * , target: 'ui=loginPages::username()'
+ * , value: '.+_test'
+ * , updateArgs: function(command, args) {
+ * args.username = command.value;
+ * }
+ * })
+ *
+ * Here, the command and target are fixed, but there is variability in the
+ * value of the command. When a command matches, the username is saved to the
+ * arguments object.
+ */
+function CommandMatcher(commandMatcherShorthand)
+{
+ /**
+ * Ensure the shorthand notation used to initialize the CommandMatcher has
+ * all required values.
+ *
+ * @param commandMatcherShorthand an object containing information about
+ * the CommandMatcher
+ */
+ this.validate = function(commandMatcherShorthand) {
+ var msg = "CommandMatcher validation error:\n"
+ + print_r(commandMatcherShorthand);
+ if (!commandMatcherShorthand.command) {
+ throw new CommandMatcherException(msg + 'no command specified!');
+ }
+ if (!commandMatcherShorthand.target) {
+ throw new CommandMatcherException(msg + 'no target specified!');
+ }
+ if (commandMatcherShorthand.minMatches &&
+ commandMatcherShorthand.maxMatches &&
+ commandMatcherShorthand.minMatches >
+ commandMatcherShorthand.maxMatches) {
+ throw new CommandMatcherException(msg + 'minMatches > maxMatches!');
+ }
+ };
+
+ /**
+ * Initialize this object.
+ *
+ * @param commandMatcherShorthand an object containing information used to
+ * initialize the CommandMatcher
+ */
+ this.init = function(commandMatcherShorthand) {
+ this.validate(commandMatcherShorthand);
+
+ this.command = commandMatcherShorthand.command;
+ this.target = commandMatcherShorthand.target;
+ this.value = commandMatcherShorthand.value || null;
+ this.minMatches = commandMatcherShorthand.minMatches || 1;
+ this.maxMatches = commandMatcherShorthand.maxMatches || 1;
+ this.updateArgs = commandMatcherShorthand.updateArgs ||
+ function(command, args) { return args; };
+ };
+
+ /**
+ * Determines whether a given command matches. Updates args by "reference"
+ * and returns true if it does; return false otherwise.
+ *
+ * @param command the command to attempt to match
+ */
+ this.isMatch = function(command) {
+ var re = new RegExp('^' + this.command + '$');
+ if (! re.test(command.command)) {
+ return false;
+ }
+ re = new RegExp('^' + this.target + '$');
+ if (! re.test(command.target)) {
+ return false;
+ }
+ if (this.value != null) {
+ re = new RegExp('^' + this.value + '$');
+ if (! re.test(command.value)) {
+ return false;
+ }
+ }
+
+ // okay, the command matches
+ return true;
+ };
+
+ // initialization
+ this.init(commandMatcherShorthand);
+}
+
+
+
+function RollupRuleException(message)
+{
+ this.message = message;
+ this.name = 'RollupRuleException';
+}
+
+function RollupRule(rollupRuleShorthand)
+{
+ /**
+ * Ensure the shorthand notation used to initialize the RollupRule has all
+ * required values.
+ *
+ * @param rollupRuleShorthand an object containing information about the
+ * RollupRule
+ */
+ this.validate = function(rollupRuleShorthand) {
+ var msg = "RollupRule validation error:\n"
+ + print_r(rollupRuleShorthand);
+ if (!rollupRuleShorthand.name) {
+ throw new RollupRuleException(msg + 'no name specified!');
+ }
+ if (!rollupRuleShorthand.description) {
+ throw new RollupRuleException(msg + 'no description specified!');
+ }
+ // rollupRuleShorthand.args is optional
+ if (!rollupRuleShorthand.commandMatchers &&
+ !rollupRuleShorthand.getRollup) {
+ throw new RollupRuleException(msg
+ + 'no command matchers specified!');
+ }
+ if (!rollupRuleShorthand.expandedCommands &&
+ !rollupRuleShorthand.getExpandedCommands) {
+ throw new RollupRuleException(msg
+ + 'no expanded commands specified!');
+ }
+
+ return true;
+ };
+
+ /**
+ * Initialize this object.
+ *
+ * @param rollupRuleShorthand an object containing information used to
+ * initialize the RollupRule
+ */
+ this.init = function(rollupRuleShorthand) {
+ this.validate(rollupRuleShorthand);
+
+ this.name = rollupRuleShorthand.name;
+ this.description = rollupRuleShorthand.description;
+ this.pre = rollupRuleShorthand.pre || '';
+ this.post = rollupRuleShorthand.post || '';
+ this.alternateCommand = rollupRuleShorthand.alternateCommand;
+ this.args = rollupRuleShorthand.args || [];
+
+ if (rollupRuleShorthand.commandMatchers) {
+ // construct the rule from the list of CommandMatchers
+ this.commandMatchers = [];
+ var matchers = rollupRuleShorthand.commandMatchers;
+ for (var i = 0; i < matchers.length; ++i) {
+ if (matchers[i].updateArgs && this.args.length == 0) {
+ // enforce metadata for arguments
+ var msg = "RollupRule validation error:\n"
+ + print_r(rollupRuleShorthand)
+ + 'no argument metadata provided!';
+ throw new RollupRuleException(msg);
+ }
+ this.commandMatchers.push(new CommandMatcher(matchers[i]));
+ }
+
+ // returns false if the rollup doesn't match, or a rollup command
+ // if it does. If returned, the command contains the
+ // replacementIndexes property, which indicates which commands it
+ // substitutes for.
+ this.getRollup = function(commands) {
+ // this is a greedy matching algorithm
+ var replacementIndexes = [];
+ var commandMatcherQueue = this.commandMatchers;
+ var matchCount = 0;
+ var args = {};
+ for (var i = 0, j = 0; i < commandMatcherQueue.length;) {
+ var matcher = commandMatcherQueue[i];
+ if (j >= commands.length) {
+ // we've run out of commands! If the remaining matchers
+ // do not have minMatches requirements, this is a
+ // match. Otherwise, it's not.
+ if (matcher.minMatches > 0) {
+ return false;
+ }
+ ++i;
+ matchCount = 0; // unnecessary, but let's be consistent
+ }
+ else {
+ if (matcher.isMatch(commands[j])) {
+ ++matchCount;
+ if (matchCount == matcher.maxMatches) {
+ // exhausted this matcher's matches ... move on
+ // to next matcher
+ ++i;
+ matchCount = 0;
+ }
+ args = matcher.updateArgs(commands[j], args);
+ replacementIndexes.push(j);
+ ++j; // move on to next command
+ }
+ else {
+ //alert(matchCount + ', ' + matcher.minMatches);
+ if (matchCount < matcher.minMatches) {
+ return false;
+ }
+ // didn't match this time, but we've satisfied the
+ // requirements already ... move on to next matcher
+ ++i;
+ matchCount = 0;
+ // still gonna look at same command
+ }
+ }
+ }
+
+ var rollup;
+ if (this.alternateCommand) {
+ rollup = new Command(this.alternateCommand,
+ commands[0].target, commands[0].value);
+ }
+ else {
+ rollup = new Command('rollup', this.name);
+ rollup.value = to_kwargs(args);
+ }
+ rollup.replacementIndexes = replacementIndexes;
+ return rollup;
+ };
+ }
+ else {
+ this.getRollup = function(commands) {
+ var result = rollupRuleShorthand.getRollup(commands);
+ if (result) {
+ var rollup = new Command(
+ result.command
+ , result.target
+ , result.value
+ );
+ rollup.replacementIndexes = result.replacementIndexes;
+ return rollup;
+ }
+ return false;
+ };
+ }
+
+ this.getExpandedCommands = function(kwargs) {
+ var commands = [];
+ var expandedCommands = (rollupRuleShorthand.expandedCommands
+ ? rollupRuleShorthand.expandedCommands
+ : rollupRuleShorthand.getExpandedCommands(
+ parse_kwargs(kwargs)));
+ for (var i = 0; i < expandedCommands.length; ++i) {
+ var command = expandedCommands[i];
+ commands.push(new Command(
+ command.command
+ , command.target
+ , command.value
+ ));
+ }
+ return commands;
+ };
+ };
+
+ this.init(rollupRuleShorthand);
+}
+
+
+
+/**
+ *
+ */
+function RollupManager()
+{
+ // singleton pattern
+ RollupManager.self = this;
+
+ this.init = function()
+ {
+ this.rollupRules = {};
+ if (is_IDE()) {
+ Editor.rollupManager = this;
+ }
+ };
+
+ /**
+ * Adds a new RollupRule to the repository. Returns true on success, or
+ * false if the rule couldn't be added.
+ *
+ * @param rollupRuleShorthand shorthand JSON specification of the new
+ * RollupRule, possibly including CommandMatcher
+ * shorthand too.
+ * @return true if the rule was added successfully,
+ * false otherwise.
+ */
+ this.addRollupRule = function(rollupRuleShorthand)
+ {
+ try {
+ var rule = new RollupRule(rollupRuleShorthand);
+ this.rollupRules[rule.name] = rule;
+ }
+ catch(e) {
+ smart_alert("Could not create RollupRule from shorthand:\n\n"
+ + e.message);
+ return false;
+ }
+ return true;
+ };
+
+ /**
+ * Returns a RollupRule by name.
+ *
+ * @param rollupName the name of the rule to fetch
+ * @return the RollupRule, or null if it isn't found.
+ */
+ this.getRollupRule = function(rollupName)
+ {
+ return (this.rollupRules[rollupName] || null);
+ };
+
+ /**
+ * Returns a list of name-description pairs for use in populating the
+ * auto-populated target dropdown in the IDE. Rules that have an alternate
+ * command defined are not included in the list, as they are not bona-fide
+ * rollups.
+ *
+ * @return a list of name-description pairs
+ */
+ this.getRollupRulesForDropdown = function()
+ {
+ var targets = [];
+ var names = keys(this.rollupRules).sort();
+ for (var i = 0; i < names.length; ++i) {
+ var name = names[i];
+ if (this.rollupRules[name].alternateCommand) {
+ continue;
+ }
+ targets.push([ name, this.rollupRules[name].description ]);
+ }
+ return targets;
+ };
+
+ /**
+ * Applies all rules to the current editor commands, asking the user in
+ * each case if it's okay to perform the replacement. The rules are applied
+ * repeatedly until there are no more matches. The algorithm should
+ * remember when the user has declined a replacement, and not ask to do it
+ * again.
+ *
+ * @return the list of commands with rollup replacements performed
+ */
+ this.applyRollupRules = function()
+ {
+ var commands = editor.getTestCase().commands;
+ var blacklistedRollups = {};
+
+ // so long as rollups were performed, we need to keep iterating through
+ // the commands starting at the beginning, because further rollups may
+ // potentially be applied on the newly created ones.
+ while (true) {
+ var performedRollup = false;
+ for (var i = 0; i < commands.length; ++i) {
+ // iterate through commands
+ for (var rollupName in this.rollupRules) {
+ var rule = this.rollupRules[rollupName];
+ var rollup = rule.getRollup(commands.slice(i));
+ if (rollup) {
+ // since we passed in a sliced version of the commands
+ // array to the getRollup() method, we need to re-add
+ // the offset to the replacementIndexes
+ var k = 0;
+ for (; k < rollup.replacementIndexes.length; ++k) {
+ rollup.replacementIndexes[k] += i;
+ }
+
+ // build the confirmation message
+ var msg = "Perform the following command rollup?\n\n";
+ for (k = 0; k < rollup.replacementIndexes.length; ++k) {
+ var replacementIndex = rollup.replacementIndexes[k];
+ var command = commands[replacementIndex];
+ msg += '[' + replacementIndex + ']: ';
+ msg += command + "\n";
+ }
+ msg += "\n";
+ msg += rollup;
+
+ // check against blacklisted rollups
+ if (blacklistedRollups[msg]) {
+ continue;
+ }
+
+ // highlight the potentially replaced rows
+ for (k = 0; k < commands.length; ++k) {
+ var command = commands[k];
+ command.result = '';
+ if (rollup.replacementIndexes.indexOf(k) != -1) {
+ command.selectedForReplacement = true;
+ }
+ editor.view.rowUpdated(replacementIndex);
+ }
+
+ // get confirmation from user
+ if (confirm(msg)) {
+ // perform rollup
+ var deleteRanges = [];
+ var replacementIndexes = rollup.replacementIndexes;
+ for (k = 0; k < replacementIndexes.length; ++k) {
+ // this is expected to be list of ranges. A
+ // range has a start, and a list of commands.
+ // The deletion only checks the length of the
+ // command list.
+ deleteRanges.push({
+ start: replacementIndexes[k]
+ , commands: [ 1 ]
+ });
+ }
+ editor.view.executeAction(new TreeView
+ .DeleteCommandAction(editor.view,deleteRanges));
+ editor.view.insertAt(i, rollup);
+
+ performedRollup = true;
+ }
+ else {
+ // cleverly remember not to try this rollup again
+ blacklistedRollups[msg] = true;
+ }
+
+ // unhighlight
+ for (k = 0; k < commands.length; ++k) {
+ commands[k].selectedForReplacement = false;
+ editor.view.rowUpdated(k);
+ }
+ }
+ }
+ }
+ if (!performedRollup) {
+ break;
+ }
+ }
+ return commands;
+ };
+
+ this.init();
+}
+
+RollupManager.getInstance = function() {
+ return (RollupManager.self == null)
+ ? new RollupManager()
+ : RollupManager.self;
+}
+
diff --git a/tests/selenium/selenium-lib/core/scripts/ui-map-sample.js b/tests/selenium/selenium-lib/core/scripts/ui-map-sample.js new file mode 100644 index 00000000..37ab84b7 --- /dev/null +++ b/tests/selenium/selenium-lib/core/scripts/ui-map-sample.js @@ -0,0 +1,979 @@ +// sample UI element mapping definition. This is for http://alistapart.com/, +// a particularly well structured site on web design principles. + + + +// in general, the map should capture structural aspects of the system, instead +// of "content". In other words, interactive elements / assertible elements +// that can be counted on to always exist should be defined here. Content - +// for example text or a link that appears in a blog entry - is always liable +// to change, and will not be fun to represent in this way. You probably don't +// want to be testing specific content anyway. + +// create the UI mapping object. THIS IS THE MOST IMPORTANT PART - DON'T FORGET +// TO DO THIS! In order for it to come into play, a user extension must +// construct the map in this way. +var myMap = new UIMap(); + + + + +// any values which may appear multiple times can be defined as variables here. +// For example, here we're enumerating a list of top level topics that will be +// used as default argument values for several UI elements. Check out how +// this variable is referenced further down. +var topics = [ + 'Code', + 'Content', + 'Culture', + 'Design', + 'Process', + 'User Science' +]; + +// map subtopics to their parent topics +var subtopics = { + 'Browsers': 'Code' + , 'CSS': 'Code' + , 'Flash': 'Code' + , 'HTML and XHTML': 'Code' + , 'Scripting': 'Code' + , 'Server Side': 'Code' + , 'XML': 'Code' + , 'Brand Arts': 'Content' + , 'Community': 'Content' + , 'Writing': 'Content' + , 'Industry': 'Culture' + , 'Politics and Money': 'Culture' + , 'State of the Web': 'Culture' + , 'Graphic Design': 'Design' + , 'User Interface Design': 'Design' + , 'Typography': 'Design' + , 'Layout': 'Design' + , 'Business': 'Process' + , 'Creativity': 'Process' + , 'Project Management and Workflow': 'Process' + , 'Accessibility': 'User Science' + , 'Information Architecture': 'User Science' + , 'Usability': 'User Science' +}; + + + +// define UI elements common for all pages. This regular expression does the +// trick. '^' is automatically prepended, and '$' is automatically postpended. +// Please note that because the regular expression is being represented as a +// string, all backslashes must be escaped with an additional backslash. Also +// note that the URL being matched will always have any trailing forward slash +// stripped. +myMap.addPageset({ + name: 'allPages' + , description: 'all alistapart.com pages' + , pathRegexp: '.*' +}); +myMap.addElement('allPages', { + name: 'masthead' + // the description should be short and to the point, usually no longer than + // a single line + , description: 'top level image link to site homepage' + // make sure the function returns the XPath ... it's easy to leave out the + // "return" statement by accident! + , locator: "xpath=//*[@id='masthead']/a/img" + , testcase1: { + xhtml: '<h1 id="masthead"><a><img expected-result="1" /></a></h1>' + } +}); +myMap.addElement('allPages', { + // be VERY CAREFUL to include commas in the correct place. Missing commas + // and extra commas can cause lots of headaches when debugging map + // definition files!!! + name: 'current_issue' + , description: 'top level link to issue currently being browsed' + , locator: "//div[@id='ish']/a" + , testcase1: { + xhtml: '<div id="ish"><a expected-result="1"></a></div>' + } +}); +myMap.addElement('allPages', { + name: 'section' + , description: 'top level link to articles section' + , args: [ + { + name: 'section' + , description: 'the name of the section' + , defaultValues: [ + 'articles' + , 'topics' + , 'about' + , 'contact' + , 'contribute' + , 'feed' + ] + } + ] + // getXPath has been deprecated by getLocator, but verify backward + // compatability here + , getXPath: function(args) { + return "//li[@id=" + args.section.quoteForXPath() + "]/a"; + } + , testcase1: { + args: { section: 'feed' } + , xhtml: '<ul><li id="feed"><a expected-result="1" /></li></ul>' + } +}); +myMap.addElement('allPages', { + name: 'search_box' + , description: 'site search input field' + // xpath has been deprecated by locator, but verify backward compatability + , xpath: "//input[@id='search']" + , testcase1: { + xhtml: '<input id="search" expected-result="1" />' + } +}); +myMap.addElement('allPages', { + name: 'search_discussions' + , description: 'site search include discussions checkbox' + , locator: 'incdisc' + , testcase1: { + xhtml: '<input id="incdisc" expected-result="1" />' + } +}); +myMap.addElement('allPages', { + name: 'search_submit' + , description: 'site search submission button' + , locator: 'submit' + , testcase1: { + xhtml: '<input id="submit" expected-result="1" />' + } +}); +myMap.addElement('allPages', { + name: 'topics' + , description: 'sidebar links to topic categories' + , args: [ + { + name: 'topic' + , description: 'the name of the topic' + , defaultValues: topics + } + ] + , getLocator: function(args) { + return "//div[@id='topiclist']/ul/li" + + "/a[text()=" + args.topic.quoteForXPath() + "]"; + } + , testcase1: { + args: { topic: 'foo' } + , xhtml: '<div id="topiclist"><ul><li>' + + '<a expected-result="1">foo</a>' + + '</li></ul></div>' + } +}); +myMap.addElement('allPages', { + name: 'copyright' + , description: 'footer link to copyright page' + , getLocator: function(args) { return "//span[@class='copyright']/a"; } + , testcase1: { + xhtml: '<span class="copyright"><a expected-result="1" /></span>' + } +}); + + + +// define UI elements for the homepage, i.e. "http://alistapart.com/", and +// magazine issue pages, i.e. "http://alistapart.com/issues/234". +myMap.addPageset({ + name: 'issuePages' + , description: 'pages including magazine issues' + , pathRegexp: '(issues/.+)?' +}); +myMap.addElement('issuePages', { + name: 'article' + , description: 'front or issue page link to article' + , args: [ + { + name: 'index' + , description: 'the index of the article' + // an array of default values for the argument. A default + // value is one that is passed to the getXPath() method of + // the container UIElement object when trying to build an + // element locator. + // + // range() may be used to count easily. Remember though that + // the ending value does not include the right extreme; for + // example range(1, 5) counts from 1 to 4 only. + , defaultValues: range(1, 5) + } + ] + , getLocator: function(args) { + return "//div[@class='item'][" + args.index + "]/h4/a"; + } +}); +myMap.addElement('issuePages', { + name: 'author' + , description: 'article author link' + , args: [ + { + name: 'index' + , description: 'the index of the author, by article' + , defaultValues: range(1, 5) + } + ] + , getLocator: function(args) { + return "//div[@class='item'][" + args.index + "]/h5/a"; + } +}); +myMap.addElement('issuePages', { + name: 'store' + , description: 'alistapart.com store link' + , locator: "//ul[@id='banners']/li/a[@title='ALA Store']/img" +}); +myMap.addElement('issuePages', { + name: 'special_article' + , description: "editor's choice article link" + , locator: "//div[@id='choice']/h4/a" +}); +myMap.addElement('issuePages', { + name: 'special_author' + , description: "author link of editor's choice article" + , locator: "//div[@id='choice']/h5/a" +}); + + + +// define UI elements for the articles page, i.e. +// "http://alistapart.com/articles" +myMap.addPageset({ + name: 'articleListPages' + , description: 'page with article listings' + , paths: [ 'articles' ] +}); +myMap.addElement('articleListPages', { + name: 'issue' + , description: 'link to issue' + , args: [ + { + name: 'index' + , description: 'the index of the issue on the page' + , defaultValues: range(1, 10) + } + ] + , getLocator: function(args) { + return "//h2[@class='ishinfo'][" + args.index + ']/a'; + } + , genericLocator: "//h2[@class='ishinfo']/a" +}); +myMap.addElement('articleListPages', { + name: 'article' + , description: 'link to article, by issue and article number' + , args: [ + { + name: 'issue_index' + , description: "the index of the article's issue on the page; " + + 'typically five per page' + , defaultValues: range(1, 6) + } + , { + name: 'article_index' + , description: 'the index of the article within the issue; ' + + 'typically two per issue' + , defaultValues: range(1, 5) + } + ] + , getLocator: function(args) { + var xpath = "//h2[@class='ishinfo'][" + (args.issue_index || 1) + ']' + + "/following-sibling::div[@class='item']" + + '[' + (args.article_index || 1) + "]/h3[@class='title']/a"; + return xpath; + } + , genericLocator: "//h2[@class='ishinfo']" + + "/following-sibling::div[@class='item']/h3[@class='title']/a" +}); +myMap.addElement('articleListPages', { + name: 'author' + , description: 'article author link, by issue and article' + , args: [ + { + name: 'issue_index' + , description: "the index of the article's issue on the page; \ +typically five per page" + , defaultValues: range(1, 6) + } + , { + name: 'article_index' + , description: "the index of the article within the issue; \ +typically two articles per issue" + , defaultValues: range(1, 3) + } + ] + // this XPath uses the "following-sibling" axis. The div elements for + // the articles in an issue are not children, but siblings of the h2 + // element identifying the article. + , getLocator: function(args) { + var xpath = "//h2[@class='ishinfo'][" + (args.issue_index || 1) + ']' + + "/following-sibling::div[@class='item']" + + '[' + (args.article_index || 1) + "]/h4[@class='byline']/a"; + return xpath; + } + , genericLocator: "//h2[@class='ishinfo']" + + "/following-sibling::div[@class='item']/h4[@class='byline']/a" +}); +myMap.addElement('articleListPages', { + name: 'next_page' + , description: 'link to next page of articles (older)' + , locator: "//a[contains(text(),'Next page')]" +}); +myMap.addElement('articleListPages', { + name: 'previous_page' + , description: 'link to previous page of articles (newer)' + , locator: "//a[contains(text(),'Previous page')]" +}); + + + +// define UI elements for specific article pages, i.e. +// "http://alistapart.com/articles/culturalprobe" +myMap.addPageset({ + name: 'articlePages' + , description: 'pages for actual articles' + , pathRegexp: 'articles/.+' +}); +myMap.addElement('articlePages', { + name: 'title' + , description: 'article title loop-link' + , locator: "//div[@id='content']/h1[@class='title']/a" +}); +myMap.addElement('articlePages', { + name: 'author' + , description: 'article author link' + , locator: "//div[@id='content']/h3[@class='byline']/a" +}); +myMap.addElement('articlePages', { + name: 'article_topics' + , description: 'links to topics under which article is published, before \ +article content' + , args: [ + { + name: 'topic' + , description: 'the name of the topic' + , defaultValues: keys(subtopics) + } + ] + , getLocator: function(args) { + return "//ul[@id='metastuff']/li/a" + + "[@title=" + args.topic.quoteForXPath() + "]"; + } +}); +myMap.addElement('articlePages', { + name: 'discuss' + , description: 'link to article discussion area, before article content' + , locator: "//ul[@id='metastuff']/li[@class='discuss']/p/a" +}); +myMap.addElement('articlePages', { + name: 'related_topics' + , description: 'links to topics under which article is published, after \ +article content' + , args: [ + { + name: 'topic' + , description: 'the name of the topic' + , defaultValues: keys(subtopics) + } + ] + , getLocator: function(args) { + return "//div[@id='learnmore']/p/a" + + "[@title=" + args.topic.quoteForXPath() + "]"; + } +}); +myMap.addElement('articlePages', { + name: 'join_discussion' + , description: 'link to article discussion area, after article content' + , locator: "//div[@class='discuss']/p/a" +}); + + + +myMap.addPageset({ + name: 'topicListingPages' + , description: 'top level listing of topics' + , paths: [ 'topics' ] +}); +myMap.addElement('topicListingPages', { + name: 'topic' + , description: 'link to topic category' + , args: [ + { + name: 'topic' + , description: 'the name of the topic' + , defaultValues: topics + } + ] + , getLocator: function(args) { + return "//div[@id='content']/h2/a" + + "[text()=" + args.topic.quoteForXPath() + "]"; + } +}); +myMap.addElement('topicListingPages', { + name: 'subtopic' + , description: 'link to subtopic category' + , args: [ + { + name: 'subtopic' + , description: 'the name of the subtopic' + , defaultValues: keys(subtopics) + } + ] + , getLocator: function(args) { + return "//div[@id='content']" + + "/descendant::a[text()=" + args.subtopic.quoteForXPath() + "]"; + } +}); + +// the following few subtopic page UI elements are very similar. Define UI +// elements for the code page, which is a subpage under topics, i.e. +// "http://alistapart.com/topics/code/" +myMap.addPageset({ + name: 'subtopicListingPages' + , description: 'pages listing subtopics' + , pathPrefix: 'topics/' + , paths: [ + 'code' + , 'content' + , 'culture' + , 'design' + , 'process' + , 'userscience' + ] +}); +myMap.addElement('subtopicListingPages', { + name: 'subtopic' + , description: 'link to a subtopic category' + , args: [ + { + name: 'subtopic' + , description: 'the name of the subtopic' + , defaultValues: keys(subtopics) + } + ] + , getLocator: function(args) { + return "//div[@id='content']/h2" + + "/a[text()=" + args.subtopic.quoteForXPath() + "]"; + } +}); + + + +// subtopic articles page +myMap.addPageset({ + name: 'subtopicArticleListingPages' + , description: 'pages listing the articles for a given subtopic' + , pathRegexp: 'topics/[^/]+/.+' +}); +myMap.addElement('subtopicArticleListingPages', { + name: 'article' + , description: 'link to a subtopic article' + , args: [ + { + name: 'index' + , description: 'the index of the article' + , defaultValues: range(1, 51) // the range seems unlimited ... + } + ] + , getLocator: function(args) { + return "//div[@id='content']/div[@class='item']" + + "[" + args.index + "]/h3/a"; + } + , testcase1: { + args: { index: 2 } + , xhtml: '<div id="content"><div class="item" /><div class="item">' + + '<h3><a expected-result="1" /></h3></div></div>' + } +}); +myMap.addElement('subtopicArticleListingPages', { + name: 'author' + , description: "link to a subtopic article author's page" + , args: [ + { + name: 'article_index' + , description: 'the index of the authored article' + , defaultValues: range(1, 51) + } + , { + name: 'author_index' + , description: 'the index of the author when there are multiple' + , defaultValues: range(1, 4) + } + ] + , getLocator: function(args) { + return "//div[@id='content']/div[@class='item'][" + + args.article_index + "]/h4/a[" + + (args.author_index ? args.author_index : '1') + ']'; + } +}); +myMap.addElement('subtopicArticleListingPages', { + name: 'issue' + , description: 'link to issue a subtopic article appears in' + , args: [ + { + name: 'index' + , description: 'the index of the subtopic article' + , defaultValues: range(1, 51) + } + ] + , getLocator: function(args) { + return "//div[@id='content']/div[@class='item']" + + "[" + args.index + "]/h5/a"; + } +}); + + + +myMap.addPageset({ + name: 'aboutPages' + , description: 'the website about page' + , paths: [ 'about' ] +}); +myMap.addElement('aboutPages', { + name: 'crew' + , description: 'link to site crew member bio or personal website' + , args: [ + { + name: 'role' + , description: 'the role of the crew member' + , defaultValues: [ + 'ALA Crew' + , 'Support' + , 'Emeritus' + ] + } + , { + name: 'role_index' + , description: 'the index of the member within the role' + , defaultValues: range(1, 20) + } + , { + name: 'member_index' + , description: 'the index of the member within the role title' + , defaultValues: range(1, 5) + } + ] + , getLocator: function(args) { + // the first role is kind of funky, and requires a conditional to + // build the XPath correctly. Its header looks like this: + // + // <h3> + // <span class="caps">ALA 4</span>.0 <span class="caps">CREW</span> + // </h3> + // + // This kind of complexity is a little daunting, but you can see + // how the format can handle it relatively easily and concisely. + if (args.role == 'ALA Crew') { + var selector = "descendant::text()='CREW'"; + } + else { + var selector = "text()=" + args.role.quoteForXPath(); + } + var xpath = + "//div[@id='secondary']/h3[" + selector + ']' + + "/following-sibling::dl/dt[" + (args.role_index || 1) + ']' + + '/a[' + (args.member_index || '1') + ']'; + return xpath; + } +}); + + + +myMap.addPageset({ + name: 'searchResultsPages' + , description: 'pages listing search results' + , paths: [ 'search' ] +}); +myMap.addElement('searchResultsPages', { + name: 'result_link' + , description: 'search result link' + , args: [ + { + name: 'index' + , description: 'the index of the search result' + , defaultValues: range(1, 11) + } + ] + , getLocator: function(args) { + return "//div[@id='content']/ul[" + args.index + ']/li/h3/a'; + } +}); +myMap.addElement('searchResultsPages', { + name: 'more_results_link' + , description: 'next or previous results link at top or bottom of page' + , args: [ + { + name: 'direction' + , description: 'next or previous results page' + // demonstrate a method which acquires default values from the + // document object. Such default values may contain EITHER commas + // OR equals signs, but NOT BOTH. + , getDefaultValues: function(inDocument) { + var defaultValues = []; + var divs = inDocument.getElementsByTagName('div'); + for (var i = 0; i < divs.length; ++i) { + if (divs[i].className == 'pages') { + break; + } + } + var links = divs[i].getElementsByTagName('a'); + for (i = 0; i < links.length; ++i) { + defaultValues.push(links[i].innerHTML + .replace(/^\xab\s*/, "") + .replace(/\s*\bb$/, "") + .replace(/\s*\d+$/, "")); + } + return defaultValues; + } + } + , { + name: 'position' + , description: 'position of the link' + , defaultValues: ['top', 'bottom'] + } + ] + , getLocator: function(args) { + return "//div[@id='content']/div[@class='pages'][" + + (args.position == 'top' ? '1' : '2') + ']' + + "/a[contains(text(), " + + (args.direction ? args.direction.quoteForXPath() : undefined) + + ")]"; + } +}); + + + +myMap.addPageset({ + name: 'commentsPages' + , description: 'pages listing comments made to an article' + , pathRegexp: 'comments/.+' +}); +myMap.addElement('commentsPages', { + name: 'article_link' + , description: 'link back to the original article' + , locator: "//div[@id='content']/h1[@class='title']/a" +}); +myMap.addElement('commentsPages', { + name: 'comment_link' + , description: 'same-page link to comment' + , args: [ + { + name: 'index' + , description: 'the index of the comment' + , defaultValues: range(1, 11) + } + ] + , getLocator: function(args) { + return "//div[@class='content']/div[contains(@class, 'comment')]" + + '[' + args.index + ']/h4/a[2]'; + } +}); +myMap.addElement('commentsPages', { + name: 'paging_link' + , description: 'links to more pages of comments' + , args: [ + { + name: 'dest' + , description: 'the destination page' + , defaultValues: ['next', 'prev'].concat(range(1, 16)) + } + , { + name: 'position' + , description: 'position of the link' + , defaultValues: ['top', 'bottom'] + } + ] + , getLocator: function(args) { + var dest = args.dest; + var xpath = "//div[@id='content']/div[@class='pages']" + + '[' + (args.position == 'top' ? '1' : '2') + ']/p'; + if (dest == 'next' || dest == 'prev') { + xpath += "/a[contains(text(), " + dest.quoteForXPath() + ")]"; + } + else { + xpath += "/a[text()=" + dest.quoteForXPath() + "]"; + } + return xpath; + } +}); + + + +myMap.addPageset({ + name: 'authorPages' + , description: 'personal pages for each author' + , pathRegexp: 'authors/[a-z]/.+' +}); +myMap.addElement('authorPages', { + name: 'article' + , description: "link to article written by this author.\n" + + 'This description has a line break.' + , args: [ + { + name: 'index' + , description: 'index of the article on the page' + , defaultValues: range(1, 11) + } + ] + , getLocator: function(args) { + var index = args.index; + // try out the CSS locator! + //return "//h4[@class='title'][" + index + "]/a"; + return 'css=h4.title:nth-child(' + index + ') > a'; + } + , testcase1: { + args: { index: '2' } + , xhtml: '<h4 class="title" /><h4 class="title">' + + '<a expected-result="1" /></h4>' + } +}); + + + +// test the offset locator. Something like the following can be recorded: +// ui=qaPages::content()//a[contains(text(),'May I quote from your articles?')] +myMap.addPageset({ + name: 'qaPages' + , description: 'question and answer pages' + , pathRegexp: 'qa' +}); +myMap.addElement('qaPages', { + name: 'content' + , description: 'the content pane containing the q&a entries' + , locator: "//div[@id='content' and " + + "child::h1[text()='Questions and Answers']]" + , getOffsetLocator: UIElement.defaultOffsetLocatorStrategy +}); +myMap.addElement('qaPages', { + name: 'last_updated' + , description: 'displays the last update date' + // demonstrate calling getLocator() for another UI element within a + // getLocator(). The former must have already been added to the map. And + // obviously, you can't randomly combine different locator types! + , locator: myMap.getUIElement('qaPages', 'content').getLocator() + '/p/em' +}); + + + +//****************************************************************************** + +var myRollupManager = new RollupManager(); + +// though the description element is required, its content is free form. You +// might want to create a documentation policy as given below, where the pre- +// and post-conditions of the rollup are spelled out. +// +// To take advantage of a "heredoc" like syntax for longer descriptions, +// add a backslash to the end of the current line and continue the string on +// the next line. +myRollupManager.addRollupRule({ + name: 'navigate_to_subtopic_article_listing' + , description: 'drill down to the listing of articles for a given subtopic \ +from the section menu, then the topic itself.' + , pre: 'current page contains the section menu (most pages should)' + , post: 'navigated to the page listing all articles for a given subtopic' + , args: [ + { + name: 'subtopic' + , description: 'the subtopic whose article listing to navigate to' + , exampleValues: keys(subtopics) + } + ] + , commandMatchers: [ + { + command: 'clickAndWait' + , target: 'ui=allPages::section\\(section=topics\\)' + // must escape parentheses in the the above target, since the + // string is being used as a regular expression. Again, backslashes + // in strings must be escaped too. + } + , { + command: 'clickAndWait' + , target: 'ui=topicListingPages::topic\\(.+' + } + , { + command: 'clickAndWait' + , target: 'ui=subtopicListingPages::subtopic\\(.+' + , updateArgs: function(command, args) { + // don't bother stripping the "ui=" prefix from the locator + // here; we're just using UISpecifier to parse the args out + var uiSpecifier = new UISpecifier(command.target); + args.subtopic = uiSpecifier.args.subtopic; + return args; + } + } + ] + , getExpandedCommands: function(args) { + var commands = []; + var topic = subtopics[args.subtopic]; + var subtopic = args.subtopic; + commands.push({ + command: 'clickAndWait' + , target: 'ui=allPages::section(section=topics)' + }); + commands.push({ + command: 'clickAndWait' + , target: 'ui=topicListingPages::topic(topic=' + topic + ')' + }); + commands.push({ + command: 'clickAndWait' + , target: 'ui=subtopicListingPages::subtopic(subtopic=' + subtopic + + ')' + }); + commands.push({ + command: 'verifyLocation' + , target: 'regexp:.+/topics/.+/.+' + }); + return commands; + } +}); + + + +myRollupManager.addRollupRule({ + name: 'replace_click_with_clickAndWait' + , description: 'replaces commands where a click was detected with \ +clickAndWait instead' + , alternateCommand: 'clickAndWait' + , commandMatchers: [ + { + command: 'click' + , target: 'ui=subtopicArticleListingPages::article\\(.+' + } + ] + , expandedCommands: [] +}); + + + +myRollupManager.addRollupRule({ + name: 'navigate_to_subtopic_article' + , description: 'navigate to an article listed under a subtopic.' + , pre: 'current page contains the section menu (most pages should)' + , post: 'navigated to an article page' + , args: [ + { + name: 'subtopic' + , description: 'the subtopic whose article listing to navigate to' + , exampleValues: keys(subtopics) + } + , { + name: 'index' + , description: 'the index of the article in the listing' + , exampleValues: range(1, 11) + } + ] + , commandMatchers: [ + { + command: 'rollup' + , target: 'navigate_to_subtopic_article_listing' + , value: 'subtopic\\s*=.+' + , updateArgs: function(command, args) { + var args1 = parse_kwargs(command.value); + args.subtopic = args1.subtopic; + return args; + } + } + , { + command: 'clickAndWait' + , target: 'ui=subtopicArticleListingPages::article\\(.+' + , updateArgs: function(command, args) { + var uiSpecifier = new UISpecifier(command.target); + args.index = uiSpecifier.args.index; + return args; + } + } + ] + /* + // this is pretty much equivalent to the commandMatchers immediately above. + // Seems more verbose and less expressive, doesn't it? But sometimes you + // might prefer the flexibility of a function. + , getRollup: function(commands) { + if (commands.length >= 2) { + command1 = commands[0]; + command2 = commands[1]; + var args1 = parse_kwargs(command1.value); + try { + var uiSpecifier = new UISpecifier(command2.target + .replace(/^ui=/, '')); + } + catch (e) { + return false; + } + if (command1.command == 'rollup' && + command1.target == 'navigate_to_subtopic_article_listing' && + args1.subtopic && + command2.command == 'clickAndWait' && + uiSpecifier.pagesetName == 'subtopicArticleListingPages' && + uiSpecifier.elementName == 'article') { + var args = { + subtopic: args1.subtopic + , index: uiSpecifier.args.index + }; + return { + command: 'rollup' + , target: this.name + , value: to_kwargs(args) + , replacementIndexes: [ 0, 1 ] + }; + } + } + return false; + } + */ + , getExpandedCommands: function(args) { + var commands = []; + commands.push({ + command: 'rollup' + , target: 'navigate_to_subtopic_article_listing' + , value: to_kwargs({ subtopic: args.subtopic }) + }); + var uiSpecifier = new UISpecifier( + 'subtopicArticleListingPages' + , 'article' + , { index: args.index }); + commands.push({ + command: 'clickAndWait' + , target: 'ui=' + uiSpecifier.toString() + }); + commands.push({ + command: 'verifyLocation' + , target: 'regexp:.+/articles/.+' + }); + return commands; + } +}); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/selenium/selenium-lib/core/selenium.css b/tests/selenium/selenium-lib/core/selenium.css index 04a194ac..9d83cb06 100644 --- a/tests/selenium/selenium-lib/core/selenium.css +++ b/tests/selenium/selenium-lib/core/selenium.css @@ -48,7 +48,11 @@ iframe { width: 100%; height: 100%; background: white; - overflow: auto; + /* HBC - this particular property seems to be causing an issue in + conjunction with the native Draw() method in Snapsie. I don't really + see a visual difference from commenting this out, so I'm going ahead + with it. + overflow: auto; */ } /*---( Style )---*/ @@ -67,6 +71,19 @@ body, html { font-size: 90%; } +.remoterunner { + font-size: 10pt; +} + +.remoterunner fieldset { + padding: 0.25em; +} + +.remoterunner button, .remoterunner label { + margin-right: 1em; +} + + #controlPanel { padding: 0.5ex; background: #eee; @@ -109,11 +126,6 @@ body, html { background-image: url("icons/selected.png"); } -#controlPanel #runRemainTests { - width: 32px; - background-image: url("icons/remains.png"); -} - .cssPauseTest { background-image: url("icons/pause.png"); } diff --git a/tests/selenium/selenium-lib/core/xpath/dom.js b/tests/selenium/selenium-lib/core/xpath/dom.js index 5e49748c..9bcc86d4 100644 --- a/tests/selenium/selenium-lib/core/xpath/dom.js +++ b/tests/selenium/selenium-lib/core/xpath/dom.js @@ -1,428 +1,566 @@ -// Copyright 2005 Google Inc.
-// All Rights Reserved
-//
-// An XML parse and a minimal DOM implementation that just supportes
-// the subset of the W3C DOM that is used in the XSLT implementation.
-//
-// References:
-//
-// [DOM] W3C DOM Level 3 Core Specification
-// <http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/>.
-//
-//
-// Author: Steffen Meschkat <[email protected]>
-
-// NOTE: The split() method in IE omits empty result strings. This is
-// utterly annoying. So we don't use it here.
-
-// Resolve entities in XML text fragments. According to the DOM
-// specification, the DOM is supposed to resolve entity references at
-// the API level. I.e. no entity references are passed through the
-// API. See "Entities and the DOM core", p.12, DOM 2 Core
-// Spec. However, different browsers actually pass very different
-// values at the API.
-//
-function xmlResolveEntities(s) {
-
- var parts = stringSplit(s, '&');
-
- var ret = parts[0];
- for (var i = 1; i < parts.length; ++i) {
- var rp = stringSplit(parts[i], ';');
- if (rp.length == 1) {
- // no entity reference: just a & but no ;
- ret += parts[i];
- continue;
- }
-
- var ch;
- switch (rp[0]) {
- case 'lt':
- ch = '<';
- break;
- case 'gt':
- ch = '>';
- break;
- case 'amp':
- ch = '&';
- break;
- case 'quot':
- ch = '"';
- break;
- case 'apos':
- ch = '\'';
- break;
- case 'nbsp':
- ch = String.fromCharCode(160);
- break;
- default:
- // Cool trick: let the DOM do the entity decoding. We assign
- // the entity text through non-W3C DOM properties and read it
- // through the W3C DOM. W3C DOM access is specified to resolve
- // entities.
- var span = window.document.createElement('span');
- span.innerHTML = '&' + rp[0] + '; ';
- ch = span.childNodes[0].nodeValue.charAt(0);
- }
- ret += ch + rp[1];
- }
-
- return ret;
-}
-
-
-// Parses the given XML string with our custom, JavaScript XML parser. Written
-// by Steffen Meschkat ([email protected]).
-function xmlParse(xml) {
- Timer.start('xmlparse');
- var regex_empty = /\/$/;
-
- // See also <http://www.w3.org/TR/REC-xml/#sec-common-syn> for
- // allowed chars in a tag and attribute name. TODO(mesch): the
- // following is still not completely correct.
-
- var regex_tagname = /^([\w:-]*)/;
- var regex_attribute = /([\w:-]+)\s?=\s?('([^\']*)'|"([^\"]*)")/g;
-
- var xmldoc = new XDocument();
- var root = xmldoc;
-
- // For the record: in Safari, we would create native DOM nodes, but
- // in Opera that is not possible, because the DOM only allows HTML
- // element nodes to be created, so we have to do our own DOM nodes.
-
- // xmldoc = document.implementation.createDocument('','',null);
- // root = xmldoc; // .createDocumentFragment();
- // NOTE(mesch): using the DocumentFragment instead of the Document
- // crashes my Safari 1.2.4 (v125.12).
- var stack = [];
-
- var parent = root;
- stack.push(parent);
-
- var x = stringSplit(xml, '<');
- for (var i = 1; i < x.length; ++i) {
- var xx = stringSplit(x[i], '>');
- var tag = xx[0];
- var text = xmlResolveEntities(xx[1] || '');
-
- if (tag.charAt(0) == '/') {
- stack.pop();
- parent = stack[stack.length-1];
-
- } else if (tag.charAt(0) == '?') {
- // Ignore XML declaration and processing instructions
- } else if (tag.charAt(0) == '!') {
- // Ignore notation and comments
- } else {
- var empty = tag.match(regex_empty);
- var tagname = regex_tagname.exec(tag)[1];
- var node = xmldoc.createElement(tagname);
-
- var att;
- while (att = regex_attribute.exec(tag)) {
- var val = xmlResolveEntities(att[3] || att[4] || '');
- node.setAttribute(att[1], val);
- }
-
- if (empty) {
- parent.appendChild(node);
- } else {
- parent.appendChild(node);
- parent = node;
- stack.push(node);
- }
- }
-
- if (text && parent != root) {
- parent.appendChild(xmldoc.createTextNode(text));
- }
- }
-
- Timer.end('xmlparse');
- return root;
-}
-
-
-// Our W3C DOM Node implementation. Note we call it XNode because we
-// can't define the identifier Node. We do this mostly for Opera,
-// where we can't reuse the HTML DOM for parsing our own XML, and for
-// Safari, where it is too expensive to have the template processor
-// operate on native DOM nodes.
-function XNode(type, name, value, owner) {
- this.attributes = [];
- this.childNodes = [];
-
- XNode.init.call(this, type, name, value, owner);
-}
-
-// Don't call as method, use apply() or call().
-XNode.init = function(type, name, value, owner) {
- this.nodeType = type - 0;
- this.nodeName = '' + name;
- this.nodeValue = '' + value;
- this.ownerDocument = owner;
-
- this.firstChild = null;
- this.lastChild = null;
- this.nextSibling = null;
- this.previousSibling = null;
- this.parentNode = null;
-}
-
-XNode.unused_ = [];
-
-XNode.recycle = function(node) {
- if (!node) {
- return;
- }
-
- if (node.constructor == XDocument) {
- XNode.recycle(node.documentElement);
- return;
- }
-
- if (node.constructor != this) {
- return;
- }
-
- XNode.unused_.push(node);
- for (var a = 0; a < node.attributes.length; ++a) {
- XNode.recycle(node.attributes[a]);
- }
- for (var c = 0; c < node.childNodes.length; ++c) {
- XNode.recycle(node.childNodes[c]);
- }
- node.attributes.length = 0;
- node.childNodes.length = 0;
- XNode.init.call(node, 0, '', '', null);
-}
-
-XNode.create = function(type, name, value, owner) {
- if (XNode.unused_.length > 0) {
- var node = XNode.unused_.pop();
- XNode.init.call(node, type, name, value, owner);
- return node;
- } else {
- return new XNode(type, name, value, owner);
- }
-}
-
-XNode.prototype.appendChild = function(node) {
- // firstChild
- if (this.childNodes.length == 0) {
- this.firstChild = node;
- }
-
- // previousSibling
- node.previousSibling = this.lastChild;
-
- // nextSibling
- node.nextSibling = null;
- if (this.lastChild) {
- this.lastChild.nextSibling = node;
- }
-
- // parentNode
- node.parentNode = this;
-
- // lastChild
- this.lastChild = node;
-
- // childNodes
- this.childNodes.push(node);
-}
-
-
-XNode.prototype.replaceChild = function(newNode, oldNode) {
- if (oldNode == newNode) {
- return;
- }
-
- for (var i = 0; i < this.childNodes.length; ++i) {
- if (this.childNodes[i] == oldNode) {
- this.childNodes[i] = newNode;
-
- var p = oldNode.parentNode;
- oldNode.parentNode = null;
- newNode.parentNode = p;
-
- p = oldNode.previousSibling;
- oldNode.previousSibling = null;
- newNode.previousSibling = p;
- if (newNode.previousSibling) {
- newNode.previousSibling.nextSibling = newNode;
- }
-
- p = oldNode.nextSibling;
- oldNode.nextSibling = null;
- newNode.nextSibling = p;
- if (newNode.nextSibling) {
- newNode.nextSibling.previousSibling = newNode;
- }
-
- if (this.firstChild == oldNode) {
- this.firstChild = newNode;
- }
-
- if (this.lastChild == oldNode) {
- this.lastChild = newNode;
- }
-
- break;
- }
- }
-}
-
-XNode.prototype.insertBefore = function(newNode, oldNode) {
- if (oldNode == newNode) {
- return;
- }
-
- if (oldNode.parentNode != this) {
- return;
- }
-
- if (newNode.parentNode) {
- newNode.parentNode.removeChild(newNode);
- }
-
- var newChildren = [];
- for (var i = 0; i < this.childNodes.length; ++i) {
- var c = this.childNodes[i];
- if (c == oldNode) {
- newChildren.push(newNode);
-
- newNode.parentNode = this;
-
- newNode.previousSibling = oldNode.previousSibling;
- oldNode.previousSibling = newNode;
- if (newNode.previousSibling) {
- newNode.previousSibling.nextSibling = newNode;
- }
-
- newNode.nextSibling = oldNode;
-
- if (this.firstChild == oldNode) {
- this.firstChild = newNode;
- }
- }
- newChildren.push(c);
- }
- this.childNodes = newChildren;
-}
-
-XNode.prototype.removeChild = function(node) {
- var newChildren = [];
- for (var i = 0; i < this.childNodes.length; ++i) {
- var c = this.childNodes[i];
- if (c != node) {
- newChildren.push(c);
- } else {
- if (c.previousSibling) {
- c.previousSibling.nextSibling = c.nextSibling;
- }
- if (c.nextSibling) {
- c.nextSibling.previousSibling = c.previousSibling;
- }
- if (this.firstChild == c) {
- this.firstChild = c.nextSibling;
- }
- if (this.lastChild == c) {
- this.lastChild = c.previousSibling;
- }
- }
- }
- this.childNodes = newChildren;
-}
-
-
-XNode.prototype.hasAttributes = function() {
- return this.attributes.length > 0;
-}
-
-
-XNode.prototype.setAttribute = function(name, value) {
- for (var i = 0; i < this.attributes.length; ++i) {
- if (this.attributes[i].nodeName == name) {
- this.attributes[i].nodeValue = '' + value;
- return;
- }
- }
- this.attributes.push(new XNode(DOM_ATTRIBUTE_NODE, name, value));
-}
-
-
-XNode.prototype.getAttribute = function(name) {
- for (var i = 0; i < this.attributes.length; ++i) {
- if (this.attributes[i].nodeName == name) {
- return this.attributes[i].nodeValue;
- }
- }
- return null;
-}
-
-XNode.prototype.removeAttribute = function(name) {
- var a = [];
- for (var i = 0; i < this.attributes.length; ++i) {
- if (this.attributes[i].nodeName != name) {
- a.push(this.attributes[i]);
- }
- }
- this.attributes = a;
-}
-
-
-function XDocument() {
- XNode.call(this, DOM_DOCUMENT_NODE, '#document', null, this);
- this.documentElement = null;
-}
-
-XDocument.prototype = new XNode(DOM_DOCUMENT_NODE, '#document');
-
-XDocument.prototype.clear = function() {
- XNode.recycle(this.documentElement);
- this.documentElement = null;
-}
-
-XDocument.prototype.appendChild = function(node) {
- XNode.prototype.appendChild.call(this, node);
- this.documentElement = this.childNodes[0];
-}
-
-XDocument.prototype.createElement = function(name) {
- return XNode.create(DOM_ELEMENT_NODE, name, null, this);
-}
-
-XDocument.prototype.createDocumentFragment = function() {
- return XNode.create(DOM_DOCUMENT_FRAGMENT_NODE, '#document-fragment',
- null, this);
-}
-
-XDocument.prototype.createTextNode = function(value) {
- return XNode.create(DOM_TEXT_NODE, '#text', value, this);
-}
-
-XDocument.prototype.createAttribute = function(name) {
- return XNode.create(DOM_ATTRIBUTE_NODE, name, null, this);
-}
-
-XDocument.prototype.createComment = function(data) {
- return XNode.create(DOM_COMMENT_NODE, '#comment', data, this);
-}
-
-XNode.prototype.getElementsByTagName = function(name, list) {
- if (!list) {
- list = [];
- }
-
- if (this.nodeName == name) {
- list.push(this);
- }
-
- for (var i = 0; i < this.childNodes.length; ++i) {
- this.childNodes[i].getElementsByTagName(name, list);
- }
-
- return list;
-}
+// Copyright 2005 Google Inc. +// All Rights Reserved +// +// Author: Steffen Meschkat <[email protected]> +// +// An XML parse and a minimal DOM implementation that just supportes +// the subset of the W3C DOM that is used in the XSLT implementation. + +// NOTE: The split() method in IE omits empty result strings. This is +// utterly annoying. So we don't use it here. + +// Resolve entities in XML text fragments. According to the DOM +// specification, the DOM is supposed to resolve entity references at +// the API level. I.e. no entity references are passed through the +// API. See "Entities and the DOM core", p.12, DOM 2 Core +// Spec. However, different browsers actually pass very different +// values at the API. See <http://mesch.nyc/test-xml-quote>. +function xmlResolveEntities(s) { + + var parts = stringSplit(s, '&'); + + var ret = parts[0]; + for (var i = 1; i < parts.length; ++i) { + var rp = parts[i].indexOf(';'); + if (rp == -1) { + // no entity reference: just a & but no ; + ret += parts[i]; + continue; + } + + var entityName = parts[i].substring(0, rp); + var remainderText = parts[i].substring(rp + 1); + + var ch; + switch (entityName) { + case 'lt': + ch = '<'; + break; + case 'gt': + ch = '>'; + break; + case 'amp': + ch = '&'; + break; + case 'quot': + ch = '"'; + break; + case 'apos': + ch = '\''; + break; + case 'nbsp': + ch = String.fromCharCode(160); + break; + default: + // Cool trick: let the DOM do the entity decoding. We assign + // the entity text through non-W3C DOM properties and read it + // through the W3C DOM. W3C DOM access is specified to resolve + // entities. + var span = domCreateElement(window.document, 'span'); + span.innerHTML = '&' + entityName + '; '; + ch = span.childNodes[0].nodeValue.charAt(0); + } + ret += ch + remainderText; + } + + return ret; +} + +var XML10_TAGNAME_REGEXP = new RegExp('^(' + XML10_NAME + ')'); +var XML10_ATTRIBUTE_REGEXP = new RegExp(XML10_ATTRIBUTE, 'g'); + +var XML11_TAGNAME_REGEXP = new RegExp('^(' + XML11_NAME + ')'); +var XML11_ATTRIBUTE_REGEXP = new RegExp(XML11_ATTRIBUTE, 'g'); + +// Parses the given XML string with our custom, JavaScript XML parser. Written +// by Steffen Meschkat ([email protected]). +function xmlParse(xml) { + var regex_empty = /\/$/; + + var regex_tagname; + var regex_attribute; + if (xml.match(/^<\?xml/)) { + // When an XML document begins with an XML declaration + // VersionInfo must appear. + if (xml.search(new RegExp(XML10_VERSION_INFO)) == 5) { + regex_tagname = XML10_TAGNAME_REGEXP; + regex_attribute = XML10_ATTRIBUTE_REGEXP; + } else if (xml.search(new RegExp(XML11_VERSION_INFO)) == 5) { + regex_tagname = XML11_TAGNAME_REGEXP; + regex_attribute = XML11_ATTRIBUTE_REGEXP; + } else { + // VersionInfo is missing, or unknown version number. + // TODO : Fallback to XML 1.0 or XML 1.1, or just return null? + alert('VersionInfo is missing, or unknown version number.'); + } + } else { + // When an XML declaration is missing it's an XML 1.0 document. + regex_tagname = XML10_TAGNAME_REGEXP; + regex_attribute = XML10_ATTRIBUTE_REGEXP; + } + + var xmldoc = new XDocument(); + var root = xmldoc; + + // For the record: in Safari, we would create native DOM nodes, but + // in Opera that is not possible, because the DOM only allows HTML + // element nodes to be created, so we have to do our own DOM nodes. + + // xmldoc = document.implementation.createDocument('','',null); + // root = xmldoc; // .createDocumentFragment(); + // NOTE(mesch): using the DocumentFragment instead of the Document + // crashes my Safari 1.2.4 (v125.12). + var stack = []; + + var parent = root; + stack.push(parent); + + // The token that delimits a section that contains markup as + // content: CDATA or comments. + var slurp = ''; + + var x = stringSplit(xml, '<'); + for (var i = 1; i < x.length; ++i) { + var xx = stringSplit(x[i], '>'); + var tag = xx[0]; + var text = xmlResolveEntities(xx[1] || ''); + + if (slurp) { + // In a "slurp" section (CDATA or comment): only check for the + // end of the section, otherwise append the whole text. + var end = x[i].indexOf(slurp); + if (end != -1) { + var data = x[i].substring(0, end); + parent.nodeValue += '<' + data; + stack.pop(); + parent = stack[stack.length-1]; + text = x[i].substring(end + slurp.length); + slurp = ''; + } else { + parent.nodeValue += '<' + x[i]; + text = null; + } + + } else if (tag.indexOf('![CDATA[') == 0) { + var start = '![CDATA['.length; + var end = x[i].indexOf(']]>'); + if (end != -1) { + var data = x[i].substring(start, end); + var node = domCreateCDATASection(xmldoc, data); + domAppendChild(parent, node); + } else { + var data = x[i].substring(start); + text = null; + var node = domCreateCDATASection(xmldoc, data); + domAppendChild(parent, node); + parent = node; + stack.push(node); + slurp = ']]>'; + } + + } else if (tag.indexOf('!--') == 0) { + var start = '!--'.length; + var end = x[i].indexOf('-->'); + if (end != -1) { + var data = x[i].substring(start, end); + var node = domCreateComment(xmldoc, data); + domAppendChild(parent, node); + } else { + var data = x[i].substring(start); + text = null; + var node = domCreateComment(xmldoc, data); + domAppendChild(parent, node); + parent = node; + stack.push(node); + slurp = '-->'; + } + + } else if (tag.charAt(0) == '/') { + stack.pop(); + parent = stack[stack.length-1]; + + } else if (tag.charAt(0) == '?') { + // Ignore XML declaration and processing instructions + } else if (tag.charAt(0) == '!') { + // Ignore notation and comments + } else { + var empty = tag.match(regex_empty); + var tagname = regex_tagname.exec(tag)[1]; + var node = domCreateElement(xmldoc, tagname); + + var att; + while (att = regex_attribute.exec(tag)) { + var val = xmlResolveEntities(att[5] || att[7] || ''); + domSetAttribute(node, att[1], val); + } + + domAppendChild(parent, node); + if (!empty) { + parent = node; + stack.push(node); + } + } + + if (text && parent != root) { + domAppendChild(parent, domCreateTextNode(xmldoc, text)); + } + } + + return root; +} + +// Based on <http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/ +// core.html#ID-1950641247> +var DOM_ELEMENT_NODE = 1; +var DOM_ATTRIBUTE_NODE = 2; +var DOM_TEXT_NODE = 3; +var DOM_CDATA_SECTION_NODE = 4; +var DOM_ENTITY_REFERENCE_NODE = 5; +var DOM_ENTITY_NODE = 6; +var DOM_PROCESSING_INSTRUCTION_NODE = 7; +var DOM_COMMENT_NODE = 8; +var DOM_DOCUMENT_NODE = 9; +var DOM_DOCUMENT_TYPE_NODE = 10; +var DOM_DOCUMENT_FRAGMENT_NODE = 11; +var DOM_NOTATION_NODE = 12; + +// Traverses the element nodes in the DOM section underneath the given +// node and invokes the given callbacks as methods on every element +// node encountered. Function opt_pre is invoked before a node's +// children are traversed; opt_post is invoked after they are +// traversed. Traversal will not be continued if a callback function +// returns boolean false. NOTE(mesch): copied from +// <//google3/maps/webmaps/javascript/dom.js>. +function domTraverseElements(node, opt_pre, opt_post) { + var ret; + if (opt_pre) { + ret = opt_pre.call(null, node); + if (typeof ret == 'boolean' && !ret) { + return false; + } + } + + for (var c = node.firstChild; c; c = c.nextSibling) { + if (c.nodeType == DOM_ELEMENT_NODE) { + ret = arguments.callee.call(this, c, opt_pre, opt_post); + if (typeof ret == 'boolean' && !ret) { + return false; + } + } + } + + if (opt_post) { + ret = opt_post.call(null, node); + if (typeof ret == 'boolean' && !ret) { + return false; + } + } +} + +// Our W3C DOM Node implementation. Note we call it XNode because we +// can't define the identifier Node. We do this mostly for Opera, +// where we can't reuse the HTML DOM for parsing our own XML, and for +// Safari, where it is too expensive to have the template processor +// operate on native DOM nodes. +function XNode(type, name, opt_value, opt_owner) { + this.attributes = []; + this.childNodes = []; + + XNode.init.call(this, type, name, opt_value, opt_owner); +} + +// Don't call as method, use apply() or call(). +XNode.init = function(type, name, value, owner) { + this.nodeType = type - 0; + this.nodeName = '' + name; + this.nodeValue = '' + value; + this.ownerDocument = owner; + + this.firstChild = null; + this.lastChild = null; + this.nextSibling = null; + this.previousSibling = null; + this.parentNode = null; +} + +XNode.unused_ = []; + +XNode.recycle = function(node) { + if (!node) { + return; + } + + if (node.constructor == XDocument) { + XNode.recycle(node.documentElement); + return; + } + + if (node.constructor != this) { + return; + } + + XNode.unused_.push(node); + for (var a = 0; a < node.attributes.length; ++a) { + XNode.recycle(node.attributes[a]); + } + for (var c = 0; c < node.childNodes.length; ++c) { + XNode.recycle(node.childNodes[c]); + } + node.attributes.length = 0; + node.childNodes.length = 0; + XNode.init.call(node, 0, '', '', null); +} + +XNode.create = function(type, name, value, owner) { + if (XNode.unused_.length > 0) { + var node = XNode.unused_.pop(); + XNode.init.call(node, type, name, value, owner); + return node; + } else { + return new XNode(type, name, value, owner); + } +} + +XNode.prototype.appendChild = function(node) { + // firstChild + if (this.childNodes.length == 0) { + this.firstChild = node; + } + + // previousSibling + node.previousSibling = this.lastChild; + + // nextSibling + node.nextSibling = null; + if (this.lastChild) { + this.lastChild.nextSibling = node; + } + + // parentNode + node.parentNode = this; + + // lastChild + this.lastChild = node; + + // childNodes + this.childNodes.push(node); +} + + +XNode.prototype.replaceChild = function(newNode, oldNode) { + if (oldNode == newNode) { + return; + } + + for (var i = 0; i < this.childNodes.length; ++i) { + if (this.childNodes[i] == oldNode) { + this.childNodes[i] = newNode; + + var p = oldNode.parentNode; + oldNode.parentNode = null; + newNode.parentNode = p; + + p = oldNode.previousSibling; + oldNode.previousSibling = null; + newNode.previousSibling = p; + if (newNode.previousSibling) { + newNode.previousSibling.nextSibling = newNode; + } + + p = oldNode.nextSibling; + oldNode.nextSibling = null; + newNode.nextSibling = p; + if (newNode.nextSibling) { + newNode.nextSibling.previousSibling = newNode; + } + + if (this.firstChild == oldNode) { + this.firstChild = newNode; + } + + if (this.lastChild == oldNode) { + this.lastChild = newNode; + } + + break; + } + } +} + + +XNode.prototype.insertBefore = function(newNode, oldNode) { + if (oldNode == newNode) { + return; + } + + if (oldNode.parentNode != this) { + return; + } + + if (newNode.parentNode) { + newNode.parentNode.removeChild(newNode); + } + + var newChildren = []; + for (var i = 0; i < this.childNodes.length; ++i) { + var c = this.childNodes[i]; + if (c == oldNode) { + newChildren.push(newNode); + + newNode.parentNode = this; + + newNode.previousSibling = oldNode.previousSibling; + oldNode.previousSibling = newNode; + if (newNode.previousSibling) { + newNode.previousSibling.nextSibling = newNode; + } + + newNode.nextSibling = oldNode; + + if (this.firstChild == oldNode) { + this.firstChild = newNode; + } + } + newChildren.push(c); + } + this.childNodes = newChildren; +} + + +XNode.prototype.removeChild = function(node) { + var newChildren = []; + for (var i = 0; i < this.childNodes.length; ++i) { + var c = this.childNodes[i]; + if (c != node) { + newChildren.push(c); + } else { + if (c.previousSibling) { + c.previousSibling.nextSibling = c.nextSibling; + } + if (c.nextSibling) { + c.nextSibling.previousSibling = c.previousSibling; + } + if (this.firstChild == c) { + this.firstChild = c.nextSibling; + } + if (this.lastChild == c) { + this.lastChild = c.previousSibling; + } + } + } + this.childNodes = newChildren; +} + + +XNode.prototype.hasAttributes = function() { + return this.attributes.length > 0; +} + + +XNode.prototype.setAttribute = function(name, value) { + for (var i = 0; i < this.attributes.length; ++i) { + if (this.attributes[i].nodeName == name) { + this.attributes[i].nodeValue = '' + value; + return; + } + } + this.attributes.push(XNode.create(DOM_ATTRIBUTE_NODE, name, value, this)); +} + + +XNode.prototype.getAttribute = function(name) { + for (var i = 0; i < this.attributes.length; ++i) { + if (this.attributes[i].nodeName == name) { + return this.attributes[i].nodeValue; + } + } + return null; +} + + +XNode.prototype.removeAttribute = function(name) { + var a = []; + for (var i = 0; i < this.attributes.length; ++i) { + if (this.attributes[i].nodeName != name) { + a.push(this.attributes[i]); + } + } + this.attributes = a; +} + + +XNode.prototype.getElementsByTagName = function(name) { + var ret = []; + var self = this; + if ("*" == name) { + domTraverseElements(this, function(node) { + if (self == node) return; + ret.push(node); + }, null); + } else { + domTraverseElements(this, function(node) { + if (self == node) return; + if (node.nodeName == name) { + ret.push(node); + } + }, null); + } + return ret; +} + + +XNode.prototype.getElementById = function(id) { + var ret = null; + domTraverseElements(this, function(node) { + if (node.getAttribute('id') == id) { + ret = node; + return false; + } + }, null); + return ret; +} + + +function XDocument() { + // NOTE(mesch): Acocording to the DOM Spec, ownerDocument of a + // document node is null. + XNode.call(this, DOM_DOCUMENT_NODE, '#document', null, null); + this.documentElement = null; +} + +XDocument.prototype = new XNode(DOM_DOCUMENT_NODE, '#document'); + +XDocument.prototype.clear = function() { + XNode.recycle(this.documentElement); + this.documentElement = null; +} + +XDocument.prototype.appendChild = function(node) { + XNode.prototype.appendChild.call(this, node); + this.documentElement = this.childNodes[0]; +} + +XDocument.prototype.createElement = function(name) { + return XNode.create(DOM_ELEMENT_NODE, name, null, this); +} + +XDocument.prototype.createDocumentFragment = function() { + return XNode.create(DOM_DOCUMENT_FRAGMENT_NODE, '#document-fragment', + null, this); +} + +XDocument.prototype.createTextNode = function(value) { + return XNode.create(DOM_TEXT_NODE, '#text', value, this); +} + +XDocument.prototype.createAttribute = function(name) { + return XNode.create(DOM_ATTRIBUTE_NODE, name, null, this); +} + +XDocument.prototype.createComment = function(data) { + return XNode.create(DOM_COMMENT_NODE, '#comment', data, this); +} + +XDocument.prototype.createCDATASection = function(data) { + return XNode.create(DOM_CDATA_SECTION_NODE, '#cdata-section', data, this); +} diff --git a/tests/selenium/selenium-lib/core/xpath/javascript-xpath-0.1.11.js b/tests/selenium/selenium-lib/core/xpath/javascript-xpath-0.1.11.js new file mode 100644 index 00000000..78c42c5d --- /dev/null +++ b/tests/selenium/selenium-lib/core/xpath/javascript-xpath-0.1.11.js @@ -0,0 +1,2816 @@ +/* JavaScript-XPath 0.1.11
+ * (c) 2007 Cybozu Labs, Inc.
+ *
+ * JavaScript-XPath is freely distributable under the terms of an MIT-style license.
+ * For details, see the JavaScript-XPath web site: http://coderepos.org/share/wiki/JavaScript-XPath
+ *
+/*--------------------------------------------------------------------------*/
+
+
+
+(function () {
+
+var undefined = void(0);
+
+var defaultConfig = {
+ targetFrame: undefined,
+ exportInstaller: false,
+ useNative: true,
+ useInnerText: true
+};
+
+var config;
+
+if (window.jsxpath) {
+ config = window.jsxpath;
+}
+else {
+ var scriptElms = document.getElementsByTagName('script');
+ var scriptElm = scriptElms[scriptElms.length - 1];
+ var scriptSrc = scriptElm.src;
+ config = {};
+ var scriptSrcMatchResult = scriptSrc.match(/\?(.*)$/);
+ if (scriptSrcMatchResult) {
+ var configStrings = scriptSrcMatchResult[1].split('&');
+ for (var i = 0, l = configStrings.length; i < l; i ++) {
+ var configString = configStrings[i];
+ var configStringSplited = configString.split('=');
+ var configName = configStringSplited[0];
+ var configValue = configStringSplited[1];
+ if (configValue == undefined) {
+ configValue == true;
+ }
+ else if (configValue == 'false' || /^-?\d+$/.test(configValue)) {
+ configValue = eval(configValue);
+ }
+ config[configName] = configValue;
+ }
+ }
+}
+
+for (var n in defaultConfig) {
+ if (!(n in config)) config[n] = defaultConfig[n];
+}
+
+config.hasNative = !!(document.implementation
+ && document.implementation.hasFeature
+ && document.implementation.hasFeature("XPath", null));
+
+if (config.hasNative && config.useNative && !config.exportInstaller) {
+ return;
+}
+
+
+
+var BinaryExpr;
+var FilterExpr;
+var FunctionCall;
+var Literal;
+var NameTest;
+var NodeSet;
+var NodeType;
+var NodeUtil;
+var Number;
+var PathExpr;
+var Step;
+var UnaryExpr;
+var UnionExpr;
+var VariableReference;
+
+/*
+ * object: user agent identifier
+ */
+var uai = new function() {
+
+ var ua = navigator.userAgent;
+
+ if (RegExp == undefined) {
+ if (ua.indexOf("Opera") >= 0) {
+ this.opera = true;
+ } else if (ua.indexOf("Netscape") >= 0) {
+ this.netscape = true;
+ } else if (ua.indexOf("Mozilla/") == 0) {
+ this.mozilla = true;
+ } else {
+ this.unknown = true;
+ }
+
+ if (ua.indexOf("Gecko/") >= 0) {
+ this.gecko = true;
+ }
+
+ if (ua.indexOf("Win") >= 0) {
+ this.windows = true;
+ } else if (ua.indexOf("Mac") >= 0) {
+ this.mac = true;
+ } else if (ua.indexOf("Linux") >= 0) {
+ this.linux = true;
+ } else if (ua.indexOf("BSD") >= 0) {
+ this.bsd = true;
+ } else if (ua.indexOf("SunOS") >= 0) {
+ this.sunos = true;
+ }
+ }
+ else {
+
+ /* for Trident/Tasman */
+ /*@cc_on
+ @if (@_jscript)
+ function jscriptVersion() {
+ switch (@_jscript_version) {
+ case 3.0: return "4.0";
+ case 5.0: return "5.0";
+ case 5.1: return "5.01";
+ case 5.5: return "5.5";
+ case 5.6:
+ if ("XMLHttpRequest" in window) return "7.0";
+ return "6.0";
+ case 5.7:
+ return "7.0";
+ default: return true;
+ }
+ }
+ if (@_win16 || @_win32 || @_win64) {
+ this.windows = true;
+ this.trident = jscriptVersion();
+ } else if (@_mac || navigator.platform.indexOf("Mac") >= 0) {
+ // '@_mac' may be 'NaN' even if the platform is Mac,
+ // so we check 'navigator.platform', too.
+ this.mac = true;
+ this.tasman = jscriptVersion();
+ }
+ if (/MSIE (\d+\.\d+)b?;/.test(ua)) {
+ this.ie = RegExp.$1;
+ this['ie' + RegExp.$1.charAt(0)] = true;
+ }
+ @else @*/
+
+ /* for AppleWebKit */
+ if (/AppleWebKit\/(\d+(?:\.\d+)*)/.test(ua)) {
+ this.applewebkit = RegExp.$1;
+ if (RegExp.$1.charAt(0) == 4) {
+ this.applewebkit2 = true;
+ }
+ else {
+ this.applewebkit3 = true;
+ }
+ }
+
+ /* for Gecko */
+ else if (typeof Components == "object" &&
+ (/Gecko\/(\d{8})/.test(ua) ||
+ navigator.product == "Gecko" && /^(\d{8})$/.test(navigator.productSub))) {
+ this.gecko = RegExp.$1;
+ }
+
+ /*@end @*/
+
+ if (typeof(opera) == "object" && typeof(opera.version) == "function") {
+ this.opera = opera.version();
+ this['opera' + this.opera[0] + this.opera[2]] = true;
+ } else if (typeof opera == "object"
+ && (/Opera[\/ ](\d+\.\d+)/.test(ua))) {
+ this.opera = RegExp.$1;
+ } else if (this.ie) {
+ } else if (/Safari\/(\d+(?:\.\d+)*)/.test(ua)) {
+ this.safari = RegExp.$1;
+ } else if (/NetFront\/(\d+(?:\.\d+)*)/.test(ua)) {
+ this.netfront = RegExp.$1;
+ } else if (/Konqueror\/(\d+(?:\.\d+)*)/.test(ua)) {
+ this.konqueror = RegExp.$1;
+ } else if (ua.indexOf("(compatible;") < 0
+ && (/^Mozilla\/(\d+\.\d+)/.test(ua))) {
+ this.mozilla = RegExp.$1;
+ if (/\([^(]*rv:(\d+(?:\.\d+)*).*?\)/.test(ua))
+ this.mozillarv = RegExp.$1;
+ if (/Firefox\/(\d+(?:\.\d+)*)/.test(ua)) {
+ this.firefox = RegExp.$1;
+ } else if (/Netscape\d?\/(\d+(?:\.\d+)*)/.test(ua)) {
+ this.netscape = RegExp.$1;
+ }
+ } else {
+ this.unknown = true;
+ }
+
+ if (ua.indexOf("Win 9x 4.90") >= 0) {
+ this.windows = "ME";
+ } else if (/Win(?:dows)? ?(NT ?(\d+\.\d+)?|\d+|ME|Vista|XP)/.test(ua)) {
+ this.windows = RegExp.$1;
+ if (RegExp.$2) {
+ this.winnt = RegExp.$2;
+ } else switch (RegExp.$1) {
+ case "2000": this.winnt = "5.0"; break;
+ case "XP": this.winnt = "5.1"; break;
+ case "Vista": this.winnt = "6.0"; break;
+ }
+ } else if (ua.indexOf("Mac") >= 0) {
+ this.mac = true;
+ } else if (ua.indexOf("Linux") >= 0) {
+ this.linux = true;
+ } else if (/(\w*BSD)/.test(ua)) {
+ this.bsd = RegExp.$1;
+ } else if (ua.indexOf("SunOS") >= 0) {
+ this.sunos = true;
+ }
+ }
+};
+
+
+/**
+ * pseudo class: Lexer
+ */
+var Lexer = function(source) {
+ var proto = Lexer.prototype;
+ var tokens = source.match(proto.regs.token);
+ for (var i = 0, l = tokens.length; i < l; i ++) {
+ if (proto.regs.strip.test(tokens[i])) {
+ tokens.splice(i, 1);
+ }
+ }
+ for (var n in proto) tokens[n] = proto[n];
+ tokens.index = 0;
+ return tokens;
+};
+
+Lexer.prototype.regs = {
+ token: /\$?(?:(?![0-9-])[\w-]+:)?(?![0-9-])[\w-]+|\/\/|\.\.|::|\d+(?:\.\d*)?|\.\d+|"[^"]*"|'[^']*'|[!<>]=|(?![0-9-])[\w-]+:\*|\s+|./g,
+ strip: /^\s/
+};
+
+Lexer.prototype.peek = function(i) {
+ return this[this.index + (i||0)];
+};
+Lexer.prototype.next = function() {
+ return this[this.index++];
+};
+Lexer.prototype.back = function() {
+ this.index--;
+};
+Lexer.prototype.empty = function() {
+ return this.length <= this.index;
+};
+
+
+/**
+ * class: Ctx
+ */
+var Ctx = function(node, position, last) {
+ this.node = node;
+ this.position = position || 1;
+ this.last = last || 1;
+};
+
+
+/**
+ * abstract class: BaseExpr
+ */
+var BaseExpr = function() {};
+
+BaseExpr.prototype.number = function(ctx) {
+ var exrs = this.evaluate(ctx);
+ if (exrs.isNodeSet) return exrs.number();
+ return + exrs;
+};
+
+BaseExpr.prototype.string = function(ctx) {
+ var exrs = this.evaluate(ctx);
+ if (exrs.isNodeSet) return exrs.string();
+ return '' + exrs;
+};
+
+BaseExpr.prototype.bool = function(ctx) {
+ var exrs = this.evaluate(ctx);
+ if (exrs.isNodeSet) return exrs.bool();
+ return !! exrs;
+};
+
+
+/**
+ * abstract class: BaseExprHasPredicates
+ */
+var BaseExprHasPredicates = function() {};
+
+BaseExprHasPredicates.parsePredicates = function(lexer, expr) {
+ while (lexer.peek() == '[') {
+ lexer.next();
+ if (lexer.empty()) {
+ throw Error('missing predicate expr');
+ }
+ var predicate = BinaryExpr.parse(lexer);
+ expr.predicate(predicate);
+ if (lexer.empty()) {
+ throw Error('unclosed predicate expr');
+ }
+ if (lexer.next() != ']') {
+ lexer.back();
+ throw Error('bad token: ' + lexer.next());
+ }
+ }
+};
+
+BaseExprHasPredicates.prototyps = new BaseExpr();
+
+BaseExprHasPredicates.prototype.evaluatePredicates = function(nodeset, start) {
+ var predicates, predicate, nodes, node, nodeset, position, reverse;
+
+ reverse = this.reverse;
+ predicates = this.predicates;
+
+ nodeset.sort();
+
+ for (var i = start || 0, l0 = predicates.length; i < l0; i ++) {
+ predicate = predicates[i];
+
+ var deleteIndexes = [];
+ var nodes = nodeset.list();
+
+ for (var j = 0, l1 = nodes.length; j < l1; j ++) {
+
+ position = reverse ? (l1 - j) : (j + 1);
+ exrs = predicate.evaluate(new Ctx(nodes[j], position, l1));
+
+ switch (typeof exrs) {
+ case 'number':
+ exrs = (position == exrs);
+ break;
+ case 'string':
+ exrs = !!exrs;
+ break;
+ case 'object':
+ exrs = exrs.bool();
+ break;
+ }
+
+ if (!exrs) {
+ deleteIndexes.push(j);
+ }
+ }
+
+ for (var j = deleteIndexes.length - 1, l1 = 0; j >= l1; j --) {
+ nodeset.del(deleteIndexes[j]);
+ }
+
+ }
+
+ return nodeset;
+};
+
+
+/**
+ * class: BinaryExpr
+ */
+if (!window.BinaryExpr && window.defaultConfig)
+ window.BinaryExpr = null;
+
+BinaryExpr = function(op, left, right, datatype) {
+ this.op = op;
+ this.left = left;
+ this.right = right;
+
+ this.datatype = BinaryExpr.ops[op][2];
+
+ this.needContextPosition = left.needContextPosition || right.needContextPosition;
+ this.needContextNode = left.needContextNode || right.needContextNode;
+
+ // Optimize [@id="foo"] and [@name="bar"]
+ if (this.op == '=') {
+ if (!right.needContextNode && !right.needContextPosition &&
+ right.datatype != 'nodeset' && right.datatype != 'void' && left.quickAttr) {
+ this.quickAttr = true;
+ this.attrName = left.attrName;
+ this.attrValueExpr = right;
+ }
+ else if (!left.needContextNode && !left.needContextPosition &&
+ left.datatype != 'nodeset' && left.datatype != 'void' && right.quickAttr) {
+ this.quickAttr = true;
+ this.attrName = right.attrName;
+ this.attrValueExpr = left;
+ }
+ }
+};
+
+BinaryExpr.compare = function(op, comp, left, right, ctx) {
+ var type, lnodes, rnodes, nodes, nodeset, primitive;
+
+ left = left.evaluate(ctx);
+ right = right.evaluate(ctx);
+
+ if (left.isNodeSet && right.isNodeSet) {
+ lnodes = left.list();
+ rnodes = right.list();
+ for (var i = 0, l0 = lnodes.length; i < l0; i ++)
+ for (var j = 0, l1 = rnodes.length; j < l1; j ++)
+ if (comp(NodeUtil.to('string', lnodes[i]), NodeUtil.to('string', rnodes[j])))
+ return true;
+ return false;
+ }
+
+ if (left.isNodeSet || right.isNodeSet) {
+ if (left.isNodeSet)
+ nodeset = left, primitive = right;
+ else
+ nodeset = right, primitive = left;
+
+ nodes = nodeset.list();
+ type = typeof primitive;
+ for (var i = 0, l = nodes.length; i < l; i ++) {
+ if (comp(NodeUtil.to(type, nodes[i]), primitive))
+ return true;
+ }
+ return false;
+ }
+
+ if (op == '=' || op == '!=') {
+ if (typeof left == 'boolean' || typeof right == 'boolean') {
+ return comp(!!left, !!right);
+ }
+ if (typeof left == 'number' || typeof right == 'number') {
+ return comp(+left, +right);
+ }
+ return comp(left, right);
+ }
+
+ return comp(+left, +right);
+};
+
+
+BinaryExpr.ops = {
+ 'div': [6, function(left, right, ctx) {
+ return left.number(ctx) / right.number(ctx);
+ }, 'number'],
+ 'mod': [6, function(left, right, ctx) {
+ return left.number(ctx) % right.number(ctx);
+ }, 'number'],
+ '*': [6, function(left, right, ctx) {
+ return left.number(ctx) * right.number(ctx);
+ }, 'number'],
+ '+': [5, function(left, right, ctx) {
+ return left.number(ctx) + right.number(ctx);
+ }, 'number'],
+ '-': [5, function(left, right, ctx) {
+ return left.number(ctx) - right.number(ctx);
+ }, 'number'],
+ '<': [4, function(left, right, ctx) {
+ return BinaryExpr.compare('<',
+ function(a, b) { return a < b }, left, right, ctx);
+ }, 'boolean'],
+ '>': [4, function(left, right, ctx) {
+ return BinaryExpr.compare('>',
+ function(a, b) { return a > b }, left, right, ctx);
+ }, 'boolean'],
+ '<=': [4, function(left, right, ctx) {
+ return BinaryExpr.compare('<=',
+ function(a, b) { return a <= b }, left, right, ctx);
+ }, 'boolean'],
+ '>=': [4, function(left, right, ctx) {
+ return BinaryExpr.compare('>=',
+ function(a, b) { return a >= b }, left, right, ctx);
+ }, 'boolean'],
+ '=': [3, function(left, right, ctx) {
+ return BinaryExpr.compare('=',
+ function(a, b) { return a == b }, left, right, ctx);
+ }, 'boolean'],
+ '!=': [3, function(left, right, ctx) {
+ return BinaryExpr.compare('!=',
+ function(a, b) { return a != b }, left, right, ctx);
+ }, 'boolean'],
+ 'and': [2, function(left, right, ctx) {
+ return left.bool(ctx) && right.bool(ctx);
+ }, 'boolean'],
+ 'or': [1, function(left, right, ctx) {
+ return left.bool(ctx) || right.bool(ctx);
+ }, 'boolean']
+};
+
+
+BinaryExpr.parse = function(lexer) {
+ var op, precedence, info, expr, stack = [], index = lexer.index;
+
+ while (true) {
+
+ if (lexer.empty()) {
+ throw Error('missing right expression');
+ }
+ expr = UnaryExpr.parse(lexer);
+
+ op = lexer.next();
+ if (!op) {
+ break;
+ }
+
+ info = this.ops[op];
+ precedence = info && info[0];
+ if (!precedence) {
+ lexer.back();
+ break;
+ }
+
+ while (stack.length && precedence <= this.ops[stack[stack.length-1]][0]) {
+ expr = new BinaryExpr(stack.pop(), stack.pop(), expr);
+ }
+
+ stack.push(expr, op);
+ }
+
+ while (stack.length) {
+ expr = new BinaryExpr(stack.pop(), stack.pop(), expr);
+ }
+
+ return expr;
+};
+
+BinaryExpr.prototype = new BaseExpr();
+
+BinaryExpr.prototype.evaluate = function(ctx) {
+ return BinaryExpr.ops[this.op][1](this.left, this.right, ctx);
+};
+
+BinaryExpr.prototype.show = function(indent) {
+ indent = indent || '';
+ var t = '';
+ t += indent + 'binary: ' + this.op + '\n';
+ indent += ' ';
+ t += this.left.show(indent);
+ t += this.right.show(indent);
+ return t;
+};
+
+
+/**
+ * class: UnaryExpr
+ */
+if (!window.UnaryExpr && window.defaultConfig)
+ window.UnaryExpr = null;
+
+UnaryExpr = function(op, expr) {
+ this.op = op;
+ this.expr = expr;
+
+ this.needContextPosition = expr.needContextPosition;
+ this.needContextNode = expr.needContextNode;
+};
+
+UnaryExpr.ops = { '-': 1 };
+
+UnaryExpr.parse = function(lexer) {
+ var token;
+ if (this.ops[lexer.peek()])
+ return new UnaryExpr(lexer.next(), UnaryExpr.parse(lexer));
+ else
+ return UnionExpr.parse(lexer);
+};
+
+UnaryExpr.prototype = new BaseExpr();
+
+UnaryExpr.prototype.datatype = 'number';
+
+UnaryExpr.prototype.evaluate = function(ctx) {
+ return - this.expr.number(ctx);
+};
+
+UnaryExpr.prototype.show = function(indent) {
+ indent = indent || '';
+ var t = '';
+ t += indent + 'unary: ' + this.op + '\n';
+ indent += ' ';
+ t += this.expr.show(indent);
+ return t;
+};
+
+
+/**
+ * class: UnionExpr
+ */
+if (!window.UnionExpr && window.defaultConfig)
+ window.UnionExpr = null;
+
+UnionExpr = function() {
+ this.paths = [];
+};
+
+UnionExpr.ops = { '|': 1 };
+
+
+UnionExpr.parse = function(lexer) {
+ var union, expr;
+
+ expr = PathExpr.parse(lexer);
+ if (!this.ops[lexer.peek()])
+ return expr;
+
+ union = new UnionExpr();
+ union.path(expr);
+
+ while (true) {
+ if (!this.ops[lexer.next()]) break;
+ if (lexer.empty()) {
+ throw Error('missing next union location path');
+ }
+ union.path(PathExpr.parse(lexer));
+ }
+
+
+
+ lexer.back();
+ return union;
+};
+
+UnionExpr.prototype = new BaseExpr();
+
+UnionExpr.prototype.datatype = 'nodeset';
+
+UnionExpr.prototype.evaluate = function(ctx) {
+ var paths = this.paths;
+ var nodeset = new NodeSet();
+ for (var i = 0, l = paths.length; i < l; i ++) {
+ var exrs = paths[i].evaluate(ctx);
+ if (!exrs.isNodeSet) throw Error('PathExpr must be nodeset');
+ nodeset.merge(exrs);
+ }
+ return nodeset;
+};
+
+UnionExpr.prototype.path = function(path) {
+ this.paths.push(path);
+
+ if (path.needContextPosition) {
+ this.needContextPosition = true;
+ }
+ if (path.needContextNode) {
+ this.needContextNode = true;
+ }
+}
+UnionExpr.prototype.show = function(indent) {
+ indent = indent || '';
+ var t = '';
+ t += indent + 'union:' + '\n';
+ indent += ' ';
+ for (var i = 0; i < this.paths.length; i ++) {
+ t += this.paths[i].show(indent);
+ }
+ return t;
+};
+
+
+/**
+ * class: PathExpr
+ */
+if (!window.PathExpr && window.defaultConfig)
+ window.PathExpr = null;
+
+PathExpr = function(filter) {
+ this.filter = filter;
+ this.steps = [];
+
+ this.datatype = filter.datatype;
+
+ this.needContextPosition = filter.needContextPosition;
+ this.needContextNode = filter.needContextNode;
+};
+
+PathExpr.ops = { '//': 1, '/': 1 };
+
+PathExpr.parse = function(lexer) {
+ var op, expr, path, token;
+
+ if (this.ops[lexer.peek()]) {
+ op = lexer.next();
+ token = lexer.peek();
+
+ if (op == '/' && (lexer.empty() ||
+ (token != '.' && token != '..' && token != '@' && token != '*' &&
+ !/(?![0-9])[\w]/.test(token)))) {
+ return FilterExpr.root();
+ }
+
+ path = new PathExpr(FilterExpr.root()); // RootExpr
+
+ if (lexer.empty()) {
+ throw Error('missing next location step');
+ }
+ expr = Step.parse(lexer);
+ path.step(op, expr);
+ }
+ else {
+ expr = FilterExpr.parse(lexer);
+ if (!expr) {
+ expr = Step.parse(lexer);
+ path = new PathExpr(FilterExpr.context());
+ path.step('/', expr);
+ }
+ else if (!this.ops[lexer.peek()])
+ return expr;
+ else
+ path = new PathExpr(expr);
+ }
+
+ while (true) {
+ if (!this.ops[lexer.peek()]) break;
+ op = lexer.next();
+ if (lexer.empty()) {
+ throw Error('missing next location step');
+ }
+ path.step(op, Step.parse(lexer));
+ }
+
+ return path;
+};
+
+PathExpr.prototype = new BaseExpr();
+
+PathExpr.prototype.evaluate = function(ctx) {
+ var nodeset = this.filter.evaluate(ctx);
+ if (!nodeset.isNodeSet) throw Exception('Filter nodeset must be nodeset type');
+
+ var steps = this.steps;
+
+ for (var i = 0, l0 = steps.length; i < l0 && nodeset.length; i ++) {
+ var step = steps[i][1];
+ var reverse = step.reverse;
+ var iter = nodeset.iterator(reverse);
+ var prevNodeset = nodeset;
+ nodeset = null;
+ var node, next;
+ if (!step.needContextPosition && step.axis == 'following') {
+ for (node = iter(); next = iter(); node = next) {
+
+ // Safari 2 node.contains problem
+ if (uai.applewebkit2) {
+ var contains = false;
+ var ancestor = next;
+ do {
+ if (ancestor == node) {
+ contains = true;
+ break;
+ }
+ } while (ancestor = ancestor.parentNode);
+ if (!contains) break;
+ }
+ else {
+ try { if (!node.contains(next)) break }
+ catch(e) { if (!(next.compareDocumentPosition(node) & 8)) break }
+ }
+ }
+ nodeset = step.evaluate(new Ctx(node));
+ }
+ else if (!step.needContextPosition && step.axis == 'preceding') {
+ node = iter();
+ nodeset = step.evaluate(new Ctx(node));
+ }
+ else {
+ node = iter();
+ var j = 0;
+ nodeset = step.evaluate(new Ctx(node), false, prevNodeset, j);
+ while (node = iter()) {
+ j ++;
+ nodeset.merge(step.evaluate(new Ctx(node), false, prevNodeset, j));
+ }
+ }
+ }
+
+ return nodeset;
+};
+
+PathExpr.prototype.step = function(op, step) {
+ step.op = op;
+ this.steps.push([op, step]);
+
+ this.quickAttr = false;
+
+ if (this.steps.length == 1) {
+ if (op == '/' && step.axis == 'attribute') {
+ var test = step.test;
+ if (!test.notOnlyElement && test.name != '*') {
+ this.quickAttr = true;
+ this.attrName = test.name;
+ }
+ }
+ }
+};
+
+PathExpr.prototype.show = function(indent) {
+ indent = indent || '';
+ var t = '';
+ t += indent + 'path:' + '\n';
+ indent += ' ';
+ t += indent + 'filter:' + '\n';
+ t += this.filter.show(indent + ' ');
+ if (this.steps.length) {
+ t += indent + 'steps:' + '\n';
+ indent += ' ';
+ for (var i = 0; i < this.steps.length; i ++) {
+ var step = this.steps[i];
+ t += indent + 'operator: ' + step[0] + '\n';
+ t += step[1].show(indent);
+ }
+ }
+ return t;
+};
+
+
+/**
+ * class: FilterExpr
+ */
+if (!window.FilterExpr && window.defaultConfig)
+ window.FilterExpr = null;
+
+FilterExpr = function(primary) {
+ this.primary = primary;
+ this.predicates = [];
+
+ this.datatype = primary.datatype;
+
+ this.needContextPosition = primary.needContextPosition;
+
+ this.needContextNode = primary.needContextNode;
+};
+
+FilterExpr.parse = function(lexer) {
+ var expr, filter, token, ch;
+
+ token = lexer.peek();
+ ch = token.charAt(0);
+
+ switch (ch) {
+ case '$':
+ expr = VariableReference.parse(lexer);
+ break;
+
+ case '(':
+ lexer.next();
+ expr = BinaryExpr.parse(lexer);
+ if (lexer.empty()) {
+ throw Error('unclosed "("');
+ }
+ if (lexer.next() != ')') {
+ lexer.back();
+ throw Error('bad token: ' + lexer.next());
+ }
+ break;
+
+ case '"':
+ case "'":
+ expr = Literal.parse(lexer);
+ break;
+
+ default:
+ if (!isNaN(+token)) {
+ expr = Number.parse(lexer);
+ }
+
+ else if (NodeType.types[token]) {
+ return null;
+ }
+
+ else if (/(?![0-9])[\w]/.test(ch) && lexer.peek(1) == '(') {
+ expr = FunctionCall.parse(lexer);
+ }
+ else {
+ return null;
+ }
+ break;
+ }
+
+ if (lexer.peek() != '[') return expr;
+
+ filter = new FilterExpr(expr);
+
+ BaseExprHasPredicates.parsePredicates(lexer, filter);
+
+ return filter;
+};
+
+FilterExpr.root = function() {
+ return new FunctionCall('root-node');
+};
+FilterExpr.context = function() {
+ return new FunctionCall('context-node');
+};
+
+FilterExpr.prototype = new BaseExprHasPredicates();
+
+FilterExpr.prototype.evaluate = function(ctx) {
+ var nodeset = this.primary.evaluate(ctx);
+ if(!nodeset.isNodeSet) {
+ if (this.predicates.length)
+ throw Error(
+ 'Primary result must be nodeset type ' +
+ 'if filter have predicate expression');
+ return nodeset;
+ }
+
+ return this.evaluatePredicates(nodeset);
+};
+
+FilterExpr.prototype.predicate = function(predicate) {
+ this.predicates.push(predicate);
+};
+
+FilterExpr.prototype.show = function(indent) {
+ indent = indent || '';
+ var t = '';
+ t += indent + 'filter: ' + '\n';
+ indent += ' ';
+ t += this.primary.show(indent);
+ if (this.predicates.length) {
+ t += indent + 'predicates: ' + '\n';
+ indent += ' ';
+ for (var i = 0; i < this.predicates.length; i ++) {
+ t += this.predicates[i].show(indent);
+ }
+ }
+ return t;
+};
+
+
+if (!window.NodeUtil && window.defaultConfig)
+ window.NodeUtil = null;
+
+NodeUtil = {
+ to: function(valueType, node) {
+ var t, type = node.nodeType;
+ // Safari2: innerText contains some bugs
+ if (type == 1 && config.useInnerText && !uai.applewebkit2) {
+ t = node.textContent;
+ t = (t == undefined || t == null) ? node.innerText : t;
+ t = (t == undefined || t == null) ? '' : t;
+ }
+ if (typeof t != 'string') {
+/*@cc_on
+ if (type == 1 && node.nodeName.toLowerCase() == 'title') {
+ t = node.text;
+ }
+ else
+@*/
+ if (type == 9 || type == 1) {
+ if (type == 9) {
+ node = node.documentElement;
+ }
+ else {
+ node = node.firstChild;
+ }
+ for (t = '', stack = [], i = 0; node;) {
+ do {
+ if (node.nodeType != 1) {
+ t += node.nodeValue;
+ }
+/*@cc_on
+ else if (node.nodeName.toLowerCase() == 'title') {
+ t += node.text;
+ }
+@*/
+ stack[i++] = node; // push
+ } while (node = node.firstChild);
+ while (i && !(node = stack[--i].nextSibling)) {}
+ }
+ }
+ else {
+ t = node.nodeValue;
+ }
+ }
+ switch (valueType) {
+ case 'number':
+ return + t;
+ case 'boolean':
+ return !! t;
+ default:
+ return t;
+ }
+ },
+ attrPropMap: {
+ name: 'name',
+ 'class': 'className',
+ dir: 'dir',
+ id: 'id',
+ name: 'name',
+ title: 'title'
+ },
+ attrMatch: function(node, attrName, attrValue) {
+/*@cc_on @if (@_jscript)
+ var propName = NodeUtil.attrPropMap[attrName];
+ if (!attrName ||
+ attrValue == null && (
+ propName && node[propName] ||
+ !propName && node.getAttribute && node.getAttribute(attrName, 2)
+ ) ||
+ attrValue != null && (
+ propName && node[propName] == attrValue ||
+ !propName && node.getAttribute && node.getAttribute(attrName, 2) == attrValue
+ )) {
+@else @*/
+ if (!attrName ||
+ attrValue == null && node.hasAttribute && node.hasAttribute(attrName) ||
+ attrValue != null && node.getAttribute && node.getAttribute(attrName) == attrValue) {
+/*@end @*/
+ return true;
+ }
+ else {
+ return false;
+ }
+ },
+ getDescendantNodes: function(test, node, nodeset, attrName, attrValue, prevNodeset, prevIndex) {
+ if (prevNodeset) {
+ prevNodeset.delDescendant(node, prevIndex);
+ }
+/*@cc_on
+ try {
+ if (!test.notOnlyElement || test.type == 8 || (attrName && test.type == 0)) {
+
+ var all = node.all;
+ if (!all) {
+ return nodeset;
+ }
+
+ var name = test.name;
+ if (test.type == 8) name = '!';
+ else if (test.type == 0) name = '*';
+
+ if (name != '*') {
+ all = all.tags(name);
+ if (!all) {
+ return nodeset;
+ }
+ }
+
+ if (attrName) {
+ var result = []
+ var i = 0;
+ if (attrValue != null && (attrName == 'id' || attrName == 'name')) {
+ all = all[attrValue];
+ if (!all) {
+ return nodeset;
+ }
+ if (!all.length || all.nodeType) {
+ all = [all];
+ }
+ }
+
+ while (node = all[i++]) {
+ if (NodeUtil.attrMatch(node, attrName, attrValue)) result.push(node);
+ }
+
+ all = result;
+ }
+
+ var i = 0;
+ while (node = all[i++]) {
+ if (name != '*' || node.tagName != '!') {
+ nodeset.push(node);
+ }
+ }
+
+ return nodeset;
+ }
+
+ (function (parent) {
+ var g = arguments.callee;
+ var node = parent.firstChild;
+ if (node) {
+ for (; node; node = node.nextSibling) {
+ if (NodeUtil.attrMatch(node, attrName, attrValue)) {
+ if (test.match(node)) nodeset.push(node);
+ }
+ g(node);
+ }
+ }
+ })(node);
+
+ return nodeset;
+ }
+ catch(e) {
+@*/
+ if (attrValue && attrName == 'id' && node.getElementById) {
+ node = node.getElementById(attrValue);
+ if (node && test.match(node)) {
+ nodeset.push(node);
+ }
+ }
+ else if (attrValue && attrName == 'name' && node.getElementsByName) {
+ var nodes = node.getElementsByName(attrValue);
+ for (var i = 0, l = nodes.length; i < l; i ++) {
+ node = nodes[i];
+ if (uai.opera ? (node.name == attrValue && test.match(node)) : test.match(node)) {
+ nodeset.push(node);
+ }
+ }
+ }
+ else if (attrValue && attrName == 'class' && node.getElementsByClassName) {
+ var nodes = node.getElementsByClassName(attrValue);
+ for (var i = 0, l = nodes.length; i < l; i ++) {
+ node = nodes[i];
+ if (node.className == attrValue && test.match(node)) {
+ nodeset.push(node);
+ }
+ }
+ }
+ else if (test.notOnlyElement) {
+ (function (parent) {
+ var f = arguments.callee;
+ for (var node = parent.firstChild; node; node = node.nextSibling) {
+ if (NodeUtil.attrMatch(node, attrName, attrValue)) {
+ if (test.match(node.nodeType)) nodeset.push(node);
+ }
+ f(node);
+ }
+ })(node);
+ }
+ else {
+ var name = test.name;
+ if (node.getElementsByTagName) {
+ var nodes = node.getElementsByTagName(name);
+ if (nodes) {
+ var i = 0;
+ while (node = nodes[i++]) {
+ if (NodeUtil.attrMatch(node, attrName, attrValue)) nodeset.push(node);
+ }
+ }
+ }
+ }
+ return nodeset;
+/*@cc_on
+ }
+@*/
+ },
+
+ getChildNodes: function(test, node, nodeset, attrName, attrValue) {
+
+/*@cc_on
+ try {
+ var children;
+
+ if ((!test.notOnlyElement || test.type == 8 || (attrName && test.type == 0)) && (children = node.children)) {
+ var name, elm;
+
+ name = test.name;
+ if (test.type == 8) name = '!';
+ else if (test.type == 0) name = '*';
+
+ if (name != '*') {
+ children = children.tags(name);
+ if (!children) {
+ return nodeset;
+ }
+ }
+
+ if (attrName) {
+ var result = []
+ var i = 0;
+ if (attrName == 'id' || attrName == 'name') {
+ children = children[attrValue];
+
+ if (!children) {
+ return nodeset;
+ }
+
+ if (!children.length || children.nodeType) {
+ children = [children];
+ }
+ }
+
+ while (node = children[i++]) {
+ if (NodeUtil.attrMatch(node, attrName, attrValue)) result.push(node);
+ }
+ children = result;
+ }
+
+ var i = 0;
+ while (node = children[i++]) {
+ if (name != '*' || node.tagName != '!') {
+ nodeset.push(node);
+ }
+ }
+
+ return nodeset;
+ }
+
+ for (var i = 0, node = node.firstChild; node; i++, node = node.nextSibling) {
+ if (NodeUtil.attrMatch(node, attrName, attrValue)) {
+ if (test.match(node)) nodeset.push(node);
+ }
+ }
+
+ return nodeset;
+ } catch(e) {
+@*/
+ for (var node = node.firstChild; node; node = node.nextSibling) {
+ if (NodeUtil.attrMatch(node, attrName, attrValue)) {
+ if (test.match(node)) nodeset.push(node);
+ }
+ }
+ return nodeset;
+/*@cc_on
+ }
+@*/
+ }
+};
+
+/*@cc_on
+var AttributeWrapper = function(node, parent, sourceIndex) {
+ this.node = node;
+ this.nodeType = 2;
+ this.nodeValue = node.nodeValue;
+ this.nodeName = node.nodeName;
+ this.parentNode = parent;
+ this.ownerElement = parent;
+ this.parentSourceIndex = sourceIndex;
+};
+
+@*/
+
+
+/**
+ * class: Step
+ */
+if (!window.Step && window.defaultConfig)
+ window.Step = null;
+
+Step = function(axis, test) {
+ // TODO check arguments and throw axis error
+ this.axis = axis;
+ this.reverse = Step.axises[axis][0];
+ this.func = Step.axises[axis][1];
+ this.test = test;
+ this.predicates = [];
+ this._quickAttr = Step.axises[axis][2]
+};
+
+Step.axises = {
+
+ ancestor: [true, function(test, node, nodeset, _, __, prevNodeset, prevIndex) {
+ while (node = node.parentNode) {
+ if (prevNodeset && node.nodeType == 1) {
+ prevNodeset.reserveDelByNode(node, prevIndex, true);
+ }
+ if (test.match(node)) nodeset.unshift(node);
+ }
+ return nodeset;
+ }],
+
+ 'ancestor-or-self': [true, function(test, node, nodeset, _, __, prevNodeset, prevIndex) {
+ do {
+ if (prevNodeset && node.nodeType == 1) {
+ prevNodeset.reserveDelByNode(node, prevIndex, true);
+ }
+ if (test.match(node)) nodeset.unshift(node);
+ } while (node = node.parentNode)
+ return nodeset;
+ }],
+
+ attribute: [false, function(test, node, nodeset) {
+ var attrs = node.attributes;
+ if (attrs) {
+/*@cc_on
+ var sourceIndex = node.sourceIndex;
+@*/
+ if ((test.notOnlyElement && test.type == 0) || test.name == '*') {
+ for (var i = 0, attr; attr = attrs[i]; i ++) {
+/*@cc_on @if (@_jscript)
+ if (attr.nodeValue) {
+ nodeset.push(new AttributeWrapper(attr, node, sourceIndex));
+ }
+@else @*/
+ nodeset.push(attr);
+/*@end @*/
+ }
+ }
+ else {
+ var attr = attrs.getNamedItem(test.name);
+
+/*@cc_on @if (@_jscript)
+ if (attr && attr.nodeValue) {
+ attr = new AttributeWrapper(attr, node, sourceIndex);;
+@else @*/
+ if (attr) {
+/*@end @*/
+ nodeset.push(attr);
+ }
+ }
+ }
+ return nodeset;
+ }],
+
+ child: [false, NodeUtil.getChildNodes, true],
+
+ descendant: [false, NodeUtil.getDescendantNodes, true],
+
+ 'descendant-or-self': [false, function(test, node, nodeset, attrName, attrValue, prevNodeset, prevIndex) {
+ if (NodeUtil.attrMatch(node, attrName, attrValue)) {
+ if (test.match(node)) nodeset.push(node);
+ }
+ return NodeUtil.getDescendantNodes(test, node, nodeset, attrName, attrValue, prevNodeset, prevIndex);
+ }, true],
+
+ following: [false, function(test, node, nodeset, attrName, attrValue) {
+ do {
+ var child = node;
+ while (child = child.nextSibling) {
+ if (NodeUtil.attrMatch(child, attrName, attrValue)) {
+ if (test.match(child)) nodeset.push(child);
+ }
+ nodeset = NodeUtil.getDescendantNodes(test, child, nodeset, attrName, attrValue);
+ }
+ } while (node = node.parentNode);
+ return nodeset;
+ }, true],
+
+ 'following-sibling': [false, function(test, node, nodeset, _, __, prevNodeset, prevIndex) {
+ while (node = node.nextSibling) {
+
+ if (prevNodeset && node.nodeType == 1) {
+ prevNodeset.reserveDelByNode(node, prevIndex);
+ }
+
+ if (test.match(node)) {
+ nodeset.push(node);
+ }
+ }
+ return nodeset;
+ }],
+
+ namespace: [false, function(test, node, nodeset) {
+ // not implemented
+ return nodeset;
+ }],
+
+ parent: [false, function(test, node, nodeset) {
+ if (node.nodeType == 9) {
+ return nodeset;
+ }
+ if (node.nodeType == 2) {
+ nodeset.push(node.ownerElement);
+ return nodeset;
+ }
+ var node = node.parentNode;
+ if (test.match(node)) nodeset.push(node);
+ return nodeset;
+ }],
+
+ preceding: [true, function(test, node, nodeset, attrName, attrValue) {
+ var parents = [];
+ do {
+ parents.unshift(node);
+ } while (node = node.parentNode);
+
+ for (var i = 1, l0 = parents.length; i < l0; i ++) {
+ var siblings = [];
+ node = parents[i];
+ while (node = node.previousSibling) {
+ siblings.unshift(node);
+ }
+
+ for (var j = 0, l1 = siblings.length; j < l1; j ++) {
+ node = siblings[j];
+ if (NodeUtil.attrMatch(node, attrName, attrValue)) {
+ if (test.match(node)) nodeset.push(node);
+ }
+ nodeset = NodeUtil.getDescendantNodes(test, node, nodeset, attrName, attrValue);
+ }
+ }
+ return nodeset;
+ }, true],
+
+ 'preceding-sibling': [true, function(test, node, nodeset, _, __, prevNodeset, prevIndex) {
+ while (node = node.previousSibling) {
+
+ if (prevNodeset && node.nodeType == 1) {
+ prevNodeset.reserveDelByNode(node, prevIndex, true);
+ }
+
+ if (test.match(node)) {
+ nodeset.unshift(node)
+ }
+ }
+ return nodeset;
+ }],
+
+ self: [false, function(test, node, nodeset) {
+ if (test.match(node)) nodeset.push(node);
+ return nodeset;
+ }]
+};
+
+Step.parse = function(lexer) {
+ var axis, test, step, token;
+
+ if (lexer.peek() == '.') {
+ step = this.self();
+ lexer.next();
+ }
+ else if (lexer.peek() == '..') {
+ step = this.parent();
+ lexer.next();
+ }
+ else {
+ if (lexer.peek() == '@') {
+ axis = 'attribute';
+ lexer.next();
+ if (lexer.empty()) {
+ throw Error('missing attribute name');
+ }
+ }
+ else {
+ if (lexer.peek(1) == '::') {
+
+ if (!/(?![0-9])[\w]/.test(lexer.peek().charAt(0))) {
+ throw Error('bad token: ' + lexer.next());
+ }
+
+ axis = lexer.next();
+ lexer.next();
+
+ if (!this.axises[axis]) {
+ throw Error('invalid axis: ' + axis);
+ }
+ if (lexer.empty()) {
+ throw Error('missing node name');
+ }
+ }
+ else {
+ axis = 'child';
+ }
+ }
+
+ token = lexer.peek();
+ if (!/(?![0-9])[\w]/.test(token.charAt(0))) {
+ if (token == '*') {
+ test = NameTest.parse(lexer)
+ }
+ else {
+ throw Error('bad token: ' + lexer.next());
+ }
+ }
+ else {
+ if (lexer.peek(1) == '(') {
+ if (!NodeType.types[token]) {
+ throw Error('invalid node type: ' + token);
+ }
+ test = NodeType.parse(lexer)
+ }
+ else {
+ test = NameTest.parse(lexer);
+ }
+ }
+ step = new Step(axis, test);
+ }
+
+ BaseExprHasPredicates.parsePredicates(lexer, step);
+
+ return step;
+};
+
+Step.self = function() {
+ return new Step('self', new NodeType('node'));
+};
+
+Step.parent = function() {
+ return new Step('parent', new NodeType('node'));
+};
+
+Step.prototype = new BaseExprHasPredicates();
+
+Step.prototype.evaluate = function(ctx, special, prevNodeset, prevIndex) {
+ var node = ctx.node;
+ var reverse = false;
+
+ if (!special && this.op == '//') {
+
+ if (!this.needContextPosition && this.axis == 'child') {
+ if (this.quickAttr) {
+ var attrValue = this.attrValueExpr ? this.attrValueExpr.string(ctx) : null;
+ var nodeset = NodeUtil.getDescendantNodes(this.test, node, new NodeSet(), this.attrName, attrValue, prevNodeset, prevIndex);
+ nodeset = this.evaluatePredicates(nodeset, 1);
+ }
+ else {
+ var nodeset = NodeUtil.getDescendantNodes(this.test, node, new NodeSet(), null, null, prevNodeset, prevIndex);
+ nodeset = this.evaluatePredicates(nodeset);
+ }
+ }
+ else {
+ var step = new Step('descendant-or-self', new NodeType('node'));
+ var nodes = step.evaluate(ctx, false, prevNodeset, prevIndex).list();
+ var nodeset = null;
+ step.op = '/';
+ for (var i = 0, l = nodes.length; i < l; i ++) {
+ if (!nodeset) {
+ nodeset = this.evaluate(new Ctx(nodes[i]), true);
+ }
+ else {
+ nodeset.merge(this.evaluate(new Ctx(nodes[i]), true));
+ }
+ }
+ nodeset = nodeset || new NodeSet();
+ }
+ }
+ else {
+
+ if (this.needContextPosition) {
+ prevNodeset = null;
+ prevIndex = null;
+ }
+
+ if (this.quickAttr) {
+ var attrValue = this.attrValueExpr ? this.attrValueExpr.string(ctx) : null;
+ var nodeset = this.func(this.test, node, new NodeSet(), this.attrName, attrValue, prevNodeset, prevIndex);
+ nodeset = this.evaluatePredicates(nodeset, 1);
+ }
+ else {
+ var nodeset = this.func(this.test, node, new NodeSet(), null, null, prevNodeset, prevIndex);
+ nodeset = this.evaluatePredicates(nodeset);
+ }
+ if (prevNodeset) {
+ prevNodeset.doDel();
+ }
+ }
+ return nodeset;
+};
+
+Step.prototype.predicate = function(predicate) {
+ this.predicates.push(predicate);
+
+ if (predicate.needContextPosition ||
+ predicate.datatype == 'number'||
+ predicate.datatype == 'void') {
+ this.needContextPosition = true;
+ }
+
+ if (this._quickAttr && this.predicates.length == 1 && predicate.quickAttr) {
+ var attrName = predicate.attrName;
+/*@cc_on @if (@_jscript)
+ this.attrName = attrName.toLowerCase();
+@else @*/
+ this.attrName = attrName;
+/*@end @*/
+ this.attrValueExpr = predicate.attrValueExpr;
+ this.quickAttr = true;
+ }
+};
+
+Step.prototype.show = function(indent) {
+ indent = indent || '';
+ var t = '';
+ t += indent + 'step: ' + '\n';
+ indent += ' ';
+ if (this.axis) t += indent + 'axis: ' + this.axis + '\n';
+ t += this.test.show(indent);
+ if (this.predicates.length) {
+ t += indent + 'predicates: ' + '\n';
+ indent += ' ';
+ for (var i = 0; i < this.predicates.length; i ++) {
+ t += this.predicates[i].show(indent);
+ }
+ }
+ return t;
+};
+
+
+
+/**
+ * NodeType
+ */
+if (!window.NodeType && window.defaultConfig)
+ window.NodeType = null;
+
+NodeType = function(name, literal) {
+ this.name = name;
+ this.literal = literal;
+
+ switch (name) {
+ case 'comment':
+ this.type = 8;
+ break;
+ case 'text':
+ this.type = 3;
+ break;
+ case 'processing-instruction':
+ this.type = 7;
+ break;
+ case 'node':
+ this.type = 0;
+ break;
+ }
+};
+
+NodeType.types = {
+ 'comment':1, 'text':1, 'processing-instruction':1, 'node':1
+};
+
+NodeType.parse = function(lexer) {
+ var type, literal, ch;
+ type = lexer.next();
+ lexer.next();
+ if (lexer.empty()) {
+ throw Error('bad nodetype');
+ }
+ ch = lexer.peek().charAt(0);
+ if (ch == '"' || ch == "'") {
+ literal = Literal.parse(lexer);
+ }
+ if (lexer.empty()) {
+ throw Error('bad nodetype');
+ }
+ if (lexer.next() != ')') {
+ lexer.back();
+ throw Error('bad token ' + lexer.next());
+ }
+ return new NodeType(type, literal);
+};
+
+NodeType.prototype = new BaseExpr();
+
+NodeType.prototype.notOnlyElement = true;
+
+NodeType.prototype.match = function(node) {
+ return !this.type || this.type == node.nodeType;
+};
+
+NodeType.prototype.show = function(indent) {
+ indent = indent || '';
+ var t = '';
+ t += indent + 'nodetype: ' + this.type + '\n';
+ if (this.literal) {
+ indent += ' ';
+ t += this.literal.show(indent);
+ }
+ return t;
+};
+
+
+/**
+ * NodeType
+ */
+if (!window.NameTest && window.defaultConfig)
+ window.NameTest = null;
+
+NameTest = function(name) {
+ this.name = name.toLowerCase();
+};
+
+NameTest.parse = function(lexer) {
+ if (lexer.peek() != '*' && lexer.peek(1) == ':' && lexer.peek(2) == '*') {
+ return new NameTest(lexer.next() + lexer.next() + lexer.next());
+ }
+ return new NameTest(lexer.next());
+};
+
+NameTest.prototype = new BaseExpr();
+
+NameTest.prototype.match = function(node) {
+ var type = node.nodeType;
+
+ if (type == 1 || type == 2) {
+ if (this.name == '*' || this.name == node.nodeName.toLowerCase()) {
+ return true;
+ }
+ }
+ return false;
+};
+
+NameTest.prototype.show = function(indent) {
+ indent = indent || '';
+ var t = '';
+ t += indent + 'nametest: ' + this.name + '\n';
+ return t;
+};
+
+
+/**
+ * class: VariableRefernce
+ */
+if (!window.VariableReference && window.defaultConfig)
+ window.VariableReference = null;
+
+VariableReference = function(name) {
+ this.name = name.substring(1);
+};
+
+
+VariableReference.parse = function(lexer) {
+ var token = lexer.next();
+ if (token.length < 2) {
+ throw Error('unnamed variable reference');
+ }
+ return new VariableReference(token)
+};
+
+VariableReference.prototype = new BaseExpr();
+
+VariableReference.prototype.datatype = 'void';
+
+VariableReference.prototype.show = function(indent) {
+ indent = indent || '';
+ var t = '';
+ t += indent + 'variable: ' + this.name + '\n';
+ return t;
+};
+
+
+/**
+ * class: Literal
+ */
+if (!window.Literal && window.defaultConfig)
+ window.Literal = null;
+
+Literal = function(text) {
+ this.text = text.substring(1, text.length - 1);
+};
+
+Literal.parse = function(lexer) {
+ var token = lexer.next();
+ if (token.length < 2) {
+ throw Error('unclosed literal string');
+ }
+ return new Literal(token)
+};
+
+Literal.prototype = new BaseExpr();
+
+Literal.prototype.datatype = 'string';
+
+Literal.prototype.evaluate = function(ctx) {
+ return this.text;
+};
+
+Literal.prototype.show = function(indent) {
+ indent = indent || '';
+ var t = '';
+ t += indent + 'literal: ' + this.text + '\n';
+ return t;
+};
+
+
+/**
+ * class: Number
+ */
+if (!window.Number && window.defaultConfig)
+ window.Number = null;
+
+Number = function(digit) {
+ this.digit = +digit;
+};
+
+
+Number.parse = function(lexer) {
+ return new Number(lexer.next());
+};
+
+Number.prototype = new BaseExpr();
+
+Number.prototype.datatype = 'number';
+
+Number.prototype.evaluate = function(ctx) {
+ return this.digit;
+};
+
+Number.prototype.show = function(indent) {
+ indent = indent || '';
+ var t = '';
+ t += indent + 'number: ' + this.digit + '\n';
+ return t;
+};
+
+
+/**
+ * class: FunctionCall
+ */
+if (!window.FunctionCall && window.defaultConfig)
+ window.FunctionCall = null;
+
+FunctionCall = function(name) {
+ var info = FunctionCall.funcs[name];
+ if (!info)
+ throw Error(name +' is not a function');
+
+ this.name = name;
+ this.func = info[0];
+ this.args = [];
+
+ this.datatype = info[1];
+
+ if (info[2]) {
+ this.needContextPosition = true;
+ }
+
+ this.needContextNodeInfo = info[3];
+ this.needContextNode = this.needContextNodeInfo[0]
+};
+
+FunctionCall.funcs = {
+
+ // Original Function
+ 'context-node': [function() {
+ if (arguments.length != 0) {
+ throw Error('Function context-node expects ()');
+ }
+ var ns;
+ ns = new NodeSet();
+ ns.push(this.node);
+ return ns;
+ }, 'nodeset', false, [true]],
+
+ // Original Function
+ 'root-node': [function() {
+ if (arguments.length != 0) {
+ throw Error('Function root-node expects ()');
+ }
+ var ns, ctxn;
+ ns = new NodeSet();
+ ctxn = this.node;
+ if (ctxn.nodeType == 9)
+ ns.push(ctxn);
+ else
+ ns.push(ctxn.ownerDocument);
+ return ns;
+ }, 'nodeset', false, []],
+
+ last: [function() {
+ if (arguments.length != 0) {
+ throw Error('Function last expects ()');
+ }
+ return this.last;
+ }, 'number', true, []],
+
+ position: [function() {
+ if (arguments.length != 0) {
+ throw Error('Function position expects ()');
+ }
+ return this.position;
+ }, 'number', true, []],
+
+ count: [function(ns) {
+ if (arguments.length != 1 || !(ns = ns.evaluate(this)).isNodeSet) {
+ throw Error('Function count expects (nodeset)');
+ }
+ return ns.length;
+ }, 'number', false, []],
+
+ id: [function(s) {
+ var ids, ns, i, id, elm, ctxn, doc;
+ if (arguments.length != 1) {
+ throw Error('Function id expects (object)');
+ }
+ ctxn = this.node;
+ if (ctxn.nodeType == 9)
+ doc = ctxn;
+ else
+ doc = ctxn.ownerDocument;
+/*@cc_on
+ all = doc.all;
+@*/
+ s = s.string(this);
+ ids = s.split(/\s+/);
+ ns = new NodeSet();
+ for (i = 0, l = ids.length; i < l; i ++) {
+ id = ids[i];
+
+/*@cc_on @if (@_jscript)
+ elm = all[id];
+ if (elm) {
+ if ((!elm.length || elm.nodeType) && id == elm.id) {
+ ns.push(elm)
+ }
+ else if (elm.length) {
+ var elms = elm;
+ for (var j = 0, l0 = elms.length; j < l0; j ++) {
+ var elem = elms[j];
+ if (id == elem.id) {
+ ns.push(elem);
+ break;
+ }
+ }
+ }
+ }
+@else @*/
+ elm = doc.getElementById(id);
+ if (uai.opera && elm && elm.id != id) {
+ var elms = doc.getElementsByName(id);
+ for (var j = 0, l0 = elms.length; j < l0; j ++) {
+ elm = elms[j];
+ if (elm.id == id) {
+ ns.push(elm);
+ }
+ }
+ }
+ else {
+ if (elm) ns.push(elm)
+ }
+/*@end @*/
+
+ }
+ ns.isSorted = false;
+ return ns;
+ }, 'nodeset', false, []],
+
+ 'local-name': [function(ns) {
+ var nd;
+ switch (arguments.length) {
+ case 0:
+ nd = this.node;
+ break;
+ case 1:
+ if ((ns = ns.evaluate(this)).isNodeSet) {
+ nd = ns.first();
+ break;
+ }
+ default:
+ throw Error('Function local-name expects (nodeset?)');
+ break;
+ }
+ return '' + nd.nodeName.toLowerCase();
+ }, 'string', false, [true, false]],
+
+ name: [function(ns) {
+ // not implemented
+ return FunctionCall.funcs['local-name'][0].apply(this, arguments);
+ }, 'string', false, [true, false]],
+
+ 'namespace-uri': [function(ns) {
+ // not implemented
+ return '';
+ }, 'string', false, [true, false]],
+
+ string: [function(s) {
+ switch (arguments.length) {
+ case 0:
+ s = NodeUtil.to('string', this.node);
+ break;
+ case 1:
+ s = s.string(this);
+ break;
+ default:
+ throw Error('Function string expects (object?)');
+ break;
+ }
+ return s;
+ }, 'string', false, [true, false]],
+
+ concat: [function(s1, s2) {
+ if (arguments.length < 2) {
+ throw Error('Function concat expects (string, string[, ...])');
+ }
+ for (var t = '', i = 0, l = arguments.length; i < l; i ++) {
+ t += arguments[i].string(this);
+ }
+ return t;
+ }, 'string', false, []],
+
+ 'starts-with': [function(s1, s2) {
+ if (arguments.length != 2) {
+ throw Error('Function starts-with expects (string, string)');
+ }
+ s1 = s1.string(this);
+ s2 = s2.string(this);
+ return s1.indexOf(s2) == 0;
+ }, 'boolean', false, []],
+
+ contains: [function(s1, s2) {
+ if (arguments.length != 2) {
+ throw Error('Function contains expects (string, string)');
+ }
+ s1 = s1.string(this);
+ s2 = s2.string(this);
+ return s1.indexOf(s2) != -1;
+ }, 'boolean', false, []],
+
+ substring: [function(s, n1, n2) {
+ var a1, a2;
+ s = s.string(this);
+ n1 = n1.number(this);
+ switch (arguments.length) {
+ case 2:
+ n2 = s.length - n1 + 1;
+ break;
+ case 3:
+ n2 = n2.number(this);
+ break;
+ default:
+ throw Error('Function substring expects (string, string)');
+ break;
+ }
+ n1 = Math.round(n1);
+ n2 = Math.round(n2);
+ a1 = n1 - 1;
+ a2 = n1 + n2 - 1;
+ if (a2 == Infinity) {
+ return s.substring(a1 < 0 ? 0 : a1);
+ }
+ else {
+ return s.substring(a1 < 0 ? 0 : a1, a2)
+ }
+ }, 'string', false, []],
+
+ 'substring-before': [function(s1, s2) {
+ var n;
+ if (arguments.length != 2) {
+ throw Error('Function substring-before expects (string, string)');
+ }
+ s1 = s1.string(this);
+ s2 = s2.string(this);
+ n = s1.indexOf(s2);
+ if (n == -1) return '';
+ return s1.substring(0, n);
+ }, 'string', false, []],
+
+ 'substring-after': [function(s1, s2) {
+ if (arguments.length != 2) {
+ throw Error('Function substring-after expects (string, string)');
+ }
+ s1 = s1.string(this);
+ s2 = s2.string(this);
+ var n = s1.indexOf(s2);
+ if (n == -1) return '';
+ return s1.substring(n + s2.length);
+ }, 'string', false, []],
+
+ 'string-length': [function(s) {
+ switch (arguments.length) {
+ case 0:
+ s = NodeUtil.to('string', this.node);
+ break;
+ case 1:
+ s = s.string(this);
+ break;
+ default:
+ throw Error('Function string-length expects (string?)');
+ break;
+ }
+ return s.length;
+ }, 'number', false, [true, false]],
+
+ 'normalize-space': [function(s) {
+ switch (arguments.length) {
+ case 0:
+ s = NodeUtil.to('string', this.node);
+ break;
+ case 1:
+ s = s.string(this);
+ break;
+ default:
+ throw Error('Function normalize-space expects (string?)');
+ break;
+ }
+ return s.replace(/\s+/g, ' ').replace(/^ /, '').replace(/ $/, '');
+ }, 'string', false, [true, false]],
+
+ translate: [function(s1, s2, s3) {
+ if (arguments.length != 3) {
+ throw Error('Function translate expects (string, string, string)');
+ }
+ s1 = s1.string(this);
+ s2 = s2.string(this);
+ s3 = s3.string(this);
+
+ var map = [];
+ for (var i = 0, l = s2.length; i < l; i ++) {
+ var ch = s2.charAt(i);
+ if (!map[ch]) map[ch] = s3.charAt(i) || '';
+ }
+ for (var t = '', i = 0, l = s1.length; i < l; i ++) {
+ var ch = s1.charAt(i);
+ var replace = map[ch]
+ t += (replace != undefined) ? replace : ch;
+ }
+ return t;
+ }, 'string', false, []],
+
+ 'boolean': [function(b) {
+ if (arguments.length != 1) {
+ throw Error('Function boolean expects (object)');
+ }
+ return b.bool(this)
+ }, 'boolean', false, []],
+
+ not: [function(b) {
+ if (arguments.length != 1) {
+ throw Error('Function not expects (object)');
+ }
+ return !b.bool(this)
+ }, 'boolean', false, []],
+
+ 'true': [function() {
+ if (arguments.length != 0) {
+ throw Error('Function true expects ()');
+ }
+ return true;
+ }, 'boolean', false, []],
+
+ 'false': [function() {
+ if (arguments.length != 0) {
+ throw Error('Function false expects ()');
+ }
+ return false;
+ }, 'boolean', false, []],
+
+ lang: [function(s) {
+ // not implemented
+ return false;
+ }, 'boolean', false, []],
+
+ number: [function(n) {
+ switch (arguments.length) {
+ case 0:
+ n = NodeUtil.to('number', this.node);
+ break;
+ case 1:
+ n = n.number(this);
+ break;
+ default:
+ throw Error('Function number expects (object?)');
+ break;
+ }
+ return n;
+ }, 'number', false, [true, false]],
+
+ sum: [function(ns) {
+ var nodes, n, i, l;
+ if (arguments.length != 1 || !(ns = ns.evaluate(this)).isNodeSet) {
+ throw Error('Function sum expects (nodeset)');
+ }
+ nodes = ns.list();
+ n = 0;
+ for (i = 0, l = nodes.length; i < l; i ++) {
+ n += NodeUtil.to('number', nodes[i]);
+ }
+ return n;
+ }, 'number', false, []],
+
+ floor: [function(n) {
+ if (arguments.length != 1) {
+ throw Error('Function floor expects (number)');
+ }
+ n = n.number(this);
+ return Math.floor(n);
+ }, 'number', false, []],
+
+ ceiling: [function(n) {
+ if (arguments.length != 1) {
+ throw Error('Function ceiling expects (number)');
+ }
+ n = n.number(this);
+ return Math.ceil(n);
+ }, 'number', false, []],
+
+ round: [function(n) {
+ if (arguments.length != 1) {
+ throw Error('Function round expects (number)');
+ }
+ n = n.number(this);
+ return Math.round(n);
+ }, 'number', false, []]
+};
+
+FunctionCall.parse = function(lexer) {
+ var expr, func = new FunctionCall(lexer.next());
+ lexer.next();
+ while (lexer.peek() != ')') {
+ if (lexer.empty()) {
+ throw Error('missing function argument list');
+ }
+ expr = BinaryExpr.parse(lexer);
+ func.arg(expr);
+ if (lexer.peek() != ',') break;
+ lexer.next();
+ }
+ if (lexer.empty()) {
+ throw Error('unclosed function argument list');
+ }
+ if (lexer.next() != ')') {
+ lexer.back();
+ throw Error('bad token: ' + lexer.next());
+ }
+ return func
+};
+
+FunctionCall.prototype = new BaseExpr();
+
+FunctionCall.prototype.evaluate = function (ctx) {
+ return this.func.apply(ctx, this.args);
+};
+
+FunctionCall.prototype.arg = function(arg) {
+ this.args.push(arg);
+
+ if (arg.needContextPosition) {
+ this.needContextPosition = true;
+ }
+
+ var args = this.args;
+ if (arg.needContextNode) {
+ args.needContexNode = true;
+ }
+ this.needContextNode = args.needContextNode ||
+ this.needContextNodeInfo[args.length];
+};
+
+FunctionCall.prototype.show = function(indent) {
+ indent = indent || '';
+ var t = '';
+ t += indent + 'function: ' + this.name + '\n';
+ indent += ' ';
+
+ if (this.args.length) {
+ t += indent + 'arguments: ' + '\n';
+ indent += ' ';
+ for (var i = 0; i < this.args.length; i ++) {
+ t += this.args[i].show(indent);
+ }
+ }
+
+ return t;
+};
+
+
+/*@cc_on @if (@_jscript)
+var NodeWrapper = function(node, sourceIndex, subIndex, attributeName) {
+ this.node = node;
+ this.nodeType = node.nodeType;
+ this.sourceIndex = sourceIndex;
+ this.subIndex = subIndex;
+ this.attributeName = attributeName || '';
+ this.order = String.fromCharCode(sourceIndex) + String.fromCharCode(subIndex) + attributeName;
+};
+
+NodeWrapper.prototype.toString = function() {
+ return this.order;
+};
+@else @*/
+var NodeID = {
+ uuid: 1,
+ get: function(node) {
+ return node.__jsxpath_id__ || (node.__jsxpath_id__ = this.uuid++);
+ }
+};
+/*@end @*/
+
+if (!window.NodeSet && window.defaultConfig)
+ window.NodeSet = null;
+
+NodeSet = function() {
+ this.length = 0;
+ this.nodes = [];
+ this.seen = {};
+ this.idIndexMap = null;
+ this.reserveDels = [];
+};
+
+NodeSet.prototype.isNodeSet = true;
+NodeSet.prototype.isSorted = true;
+
+/*@_cc_on
+NodeSet.prototype.shortcut = true;
+@*/
+
+NodeSet.prototype.merge = function(nodeset) {
+ this.isSorted = false;
+ if (nodeset.only) {
+ return this.push(nodeset.only);
+ }
+
+ if (this.only){
+ var only = this.only;
+ delete this.only;
+ this.push(only);
+ this.length --;
+ }
+
+ var nodes = nodeset.nodes;
+ for (var i = 0, l = nodes.length; i < l; i ++) {
+ this._add(nodes[i]);
+ }
+};
+
+NodeSet.prototype.sort = function() {
+ if (this.only) return;
+ if (this.sortOff) return;
+
+ if (!this.isSorted) {
+ this.isSorted = true;
+ this.idIndexMap = null;
+
+/*@cc_on
+ if (this.shortcut) {
+ this.nodes.sort();
+ }
+ else {
+ this.nodes.sort(function(a, b) {
+ var result;
+ result = a.sourceIndex - b.sourceIndex;
+ if (result == 0)
+ return a.subIndex - a.subIndex;
+ else
+ return result;
+ });
+ }
+ return;
+@*/
+ var nodes = this.nodes;
+ nodes.sort(function(a, b) {
+ if (a == b) return 0;
+
+ if (a.compareDocumentPosition) {
+ var result = a.compareDocumentPosition(b);
+ if (result & 2) return 1;
+ if (result & 4) return -1;
+ return 0;
+ }
+ else {
+ var node1 = a, node2 = b, ancestor1 = a, ancestor2 = b, deep1 = 0, deep2 = 0;
+
+ while(ancestor1 = ancestor1.parentNode) deep1 ++;
+ while(ancestor2 = ancestor2.parentNode) deep2 ++;
+
+ // same deep
+ if (deep1 > deep2) {
+ while (deep1-- != deep2) node1 = node1.parentNode;
+ if (node1 == node2) return 1;
+ }
+ else if (deep2 > deep1) {
+ while (deep2-- != deep1) node2 = node2.parentNode;
+ if (node1 == node2) return -1;
+ }
+
+ while ((ancestor1 = node1.parentNode) != (ancestor2 = node2.parentNode)) {
+ node1 = ancestor1;
+ node2 = ancestor2;
+ }
+
+ // node1 is node2's sibling
+ while (node1 = node1.nextSibling) if (node1 == node2) return -1;
+
+ return 1;
+ }
+ });
+ }
+};
+
+
+/*@cc_on @if (@_jscript)
+NodeSet.prototype.sourceOffset = 1;
+NodeSet.prototype.subOffset = 2;
+NodeSet.prototype.createWrapper = function(node) {
+ var parent, child, attributes, attributesLength, sourceIndex, subIndex, attributeName;
+
+ sourceIndex = node.sourceIndex;
+
+ if (typeof sourceIndex != 'number') {
+ type = node.nodeType;
+ switch (type) {
+ case 2:
+ parent = node.parentNode;
+ sourceIndex = node.parentSourceIndex;
+ subIndex = -1;
+ attributeName = node.nodeName;
+ break;
+ case 9:
+ subIndex = -2;
+ sourceIndex = -1;
+ break;
+ default:
+ child = node;
+ subIndex = 0;
+ do {
+ subIndex ++;
+ sourceIndex = child.sourceIndex;
+ if (sourceIndex) {
+ parent = child;
+ child = child.lastChild;
+ if (!child) {
+ child = parent;
+ break;
+ }
+ subIndex ++;
+ }
+ } while (child = child.previousSibling);
+ if (!sourceIndex) {
+ sourceIndex = node.parentNode.sourceIndex;
+ }
+ break;
+ }
+ }
+ else {
+ subIndex = -2;
+ }
+
+ sourceIndex += this.sourceOffset;
+ subIndex += this.subOffset;
+
+ return new NodeWrapper(node, sourceIndex, subIndex, attributeName);
+};
+
+NodeSet.prototype.reserveDelBySourceIndexAndSubIndex = function(sourceIndex, subIndex, offset, reverse) {
+ var map = this.createIdIndexMap();
+ var index;
+ if ((map = map[sourceIndex]) && (index = map[subIndex])) {
+ if (reverse && (this.length - offset - 1) > index || !reverse && offset < index) {
+ var obj = {
+ value: index,
+ order: String.fromCharCode(index),
+ toString: function() { return this.order },
+ valueOf: function() { return this.value }
+ };
+ this.reserveDels.push(obj);
+ }
+ }
+};
+@else @*/
+NodeSet.prototype.reserveDelByNodeID = function(id, offset, reverse) {
+ var map = this.createIdIndexMap();
+ var index;
+ if (index = map[id]) {
+ if (reverse && (this.length - offset - 1) > index || !reverse && offset < index) {
+ var obj = {
+ value: index,
+ order: String.fromCharCode(index),
+ toString: function() { return this.order },
+ valueOf: function() { return this.value }
+ };
+ this.reserveDels.push(obj);
+ }
+ }
+};
+/*@end @*/
+
+NodeSet.prototype.reserveDelByNode = function(node, offset, reverse) {
+/*@cc_on @if (@_jscript)
+ node = this.createWrapper(node);
+ this.reserveDelBySourceIndexAndSubIndex(node.sourceIndex, node.subIndex, offset, reverse);
+@else @*/
+ this.reserveDelByNodeID(NodeID.get(node), offset, reverse);
+/*@end @*/
+};
+
+NodeSet.prototype.doDel = function() {
+ if (!this.reserveDels.length) return;
+
+ if (this.length < 0x10000) {
+ var dels = this.reserveDels.sort(function(a, b) { return b - a });
+ }
+ else {
+ var dels = this.reserveDels.sort(function(a, b) { return b - a });
+ }
+ for (var i = 0, l = dels.length; i < l; i ++) {
+ this.del(dels[i]);
+ }
+ this.reserveDels = [];
+ this.idIndexMap = null;
+};
+
+NodeSet.prototype.createIdIndexMap = function() {
+ if (this.idIndexMap) {
+ return this.idIndexMap;
+ }
+ else {
+ var map = this.idIndexMap = {};
+ var nodes = this.nodes;
+ for (var i = 0, l = nodes.length; i < l; i ++) {
+ var node = nodes[i];
+/*@cc_on @if (@_jscript)
+ var sourceIndex = node.sourceIndex;
+ var subIndex = node.subIndex;
+ if (!map[sourceIndex]) map[sourceIndex] = {};
+ map[sourceIndex][subIndex] = i;
+@else @*/
+ var id = NodeID.get(node);
+ map[id] = i;
+/*@end @*/
+ }
+ return map;
+ }
+};
+
+NodeSet.prototype.del = function(index) {
+ this.length --;
+ if (this.only) {
+ delete this.only;
+ }
+ else {
+ var node = this.nodes.splice(index, 1)[0];
+
+ if (this._first == node) {
+ delete this._first;
+ delete this._firstSourceIndex;
+ delete this._firstSubIndex;
+ }
+
+/*@cc_on @if (@_jscript)
+ delete this.seen[node.sourceIndex][node.subIndex];
+@else @*/
+ delete this.seen[NodeID.get(node)];
+/*@end @*/
+ }
+};
+
+
+NodeSet.prototype.delDescendant = function(elm, offset) {
+ if (this.only) return;
+ var nodeType = elm.nodeType;
+ if (nodeType != 1 && nodeType != 9) return;
+ if (uai.applewebkit2) return;
+
+ // element || document
+ if (!elm.contains) {
+ if (nodeType == 1) {
+ var _elm = elm;
+ elm = {
+ contains: function(node) {
+ return node.compareDocumentPosition(_elm) & 8;
+ }
+ };
+ }
+ else {
+ // document
+ elm = {
+ contains: function() {
+ return true;
+ }
+ };
+ }
+ }
+
+ var nodes = this.nodes;
+ for (var i = offset + 1; i < nodes.length; i ++) {
+
+/*@cc_on @if (@_jscript)
+ if (nodes[i].node.nodeType == 1 && elm.contains(nodes[i].node)) {
+@else @*/
+ if (elm.contains(nodes[i])) {
+/*@end @*/
+ this.del(i);
+ i --;
+ }
+ }
+};
+
+NodeSet.prototype._add = function(node, reverse) {
+
+/*@cc_on @if (@_jscript)
+
+ var first, firstSourceIndex, firstSubIndex, sourceIndex, subIndex, attributeName;
+
+ sourceIndex = node.sourceIndex;
+ subIndex = node.subIndex;
+ attributeName = node.attributeName;
+ seen = this.seen;
+
+ seen = seen[sourceIndex] || (seen[sourceIndex] = {});
+
+ if (node.nodeType == 2) {
+ seen = seen[subIndex] || (seen[subIndex] = {});
+ if (seen[attributeName]) {
+ return true;
+ }
+ seen[attributeName] = true;
+ }
+ else {
+ if (seen[subIndex]) {
+ return true;
+ }
+ seen[subIndex] = true;
+ }
+
+ if (sourceIndex >= 0x10000 || subIndex >= 0x10000) {
+ this.shortcut = false;
+ }
+
+ // if this._first is undefined and this.nodes is not empty
+ // then first node shortcut is disabled.
+ if (this._first || this.nodes.length == 0) {
+ first = this._first;
+ firstSourceIndex = this._firstSourceIndex;
+ firstSubIndex = this._firstSubIndex;
+ if (!first || firstSourceIndex > sourceIndex || (firstSourceIndex == sourceIndex && firstSubIndex > subIndex)) {
+ this._first = node;
+ this._firstSourceIndex = sourceIndex;
+ this._firstSubIndex = subIndex
+ }
+ }
+
+@else @*/
+
+ var seen = this.seen;
+ var id = NodeID.get(node);
+ if (seen[id]) return true;
+ seen[id] = true;
+
+/*@end @*/
+
+ this.length++;
+ if (reverse)
+ this.nodes.unshift(node);
+ else
+ this.nodes.push(node);
+};
+
+
+NodeSet.prototype.unshift = function(node) {
+ if (!this.length) {
+ this.length ++;
+ this.only = node;
+ return
+ }
+ if (this.only){
+ var only = this.only;
+ delete this.only;
+ this.unshift(only);
+ this.length --;
+ }
+/*@cc_on
+ node = this.createWrapper(node);
+@*/
+ return this._add(node, true);
+};
+
+
+NodeSet.prototype.push = function(node) {
+ if (!this.length) {
+ this.length ++;
+ this.only = node;
+ return;
+ }
+ if (this.only) {
+ var only = this.only;
+ delete this.only;
+ this.push(only);
+ this.length --;
+ }
+/*@cc_on
+ node = this.createWrapper(node);
+@*/
+ return this._add(node);
+};
+
+NodeSet.prototype.first = function() {
+ if (this.only) return this.only;
+/*@cc_on
+ if (this._first) return this._first.node;
+ if (this.nodes.length > 1) this.sort();
+ var node = this.nodes[0];
+ return node ? node.node : undefined;
+@*/
+ if (this.nodes.length > 1) this.sort();
+ return this.nodes[0];
+};
+
+NodeSet.prototype.list = function() {
+ if (this.only) return [this.only];
+ this.sort();
+/*@cc_on
+ var i, l, nodes, results;
+ nodes = this.nodes;
+ results = [];
+ for (i = 0, l = nodes.length; i < l; i ++) {
+ results.push(nodes[i].node);
+ }
+ return results;
+@*/
+ return this.nodes;
+};
+
+NodeSet.prototype.string = function() {
+ var node = this.only || this.first();
+ return node ? NodeUtil.to('string', node) : '';
+};
+
+NodeSet.prototype.bool = function() {
+ return !! (this.length || this.only);
+};
+
+NodeSet.prototype.number = function() {
+ return + this.string();
+};
+
+NodeSet.prototype.iterator = function(reverse) {
+ this.sort();
+ var nodeset = this;
+
+ if (!reverse) {
+ var count = 0;
+ return function() {
+ if (nodeset.only && count++ == 0) return nodeset.only;
+/*@cc_on @if(@_jscript)
+ var wrapper = nodeset.nodes[count++];
+ if (wrapper) return wrapper.node;
+ return undefined;
+@else @*/
+ return nodeset.nodes[count++];
+/*@end @*/
+ };
+ }
+ else {
+ var count = 0;
+ return function() {
+ var index = nodeset.length - (count++) - 1;
+ if (nodeset.only && index == 0) return nodeset.only;
+/*@cc_on @if(@_jscript)
+ var wrapper = nodeset.nodes[index];
+ if (wrapper) return wrapper.node;
+ return undefined;
+@else @*/
+ return nodeset.nodes[index];
+/*@end @*/
+ };
+ }
+};
+
+
+var install = function(win) {
+
+ win = win || this;
+ var doc = win.document;
+ var undefined = win.undefined;
+
+ win.XPathExpression = function(expr) {
+ if (!expr.length) {
+ throw win.Error('no expression');
+ }
+ var lexer = this.lexer = Lexer(expr);
+ if (lexer.empty()) {
+ throw win.Error('no expression');
+ }
+ this.expr = BinaryExpr.parse(lexer);
+ if (!lexer.empty()) {
+ throw win.Error('bad token: ' + lexer.next());
+ }
+ };
+
+ win.XPathExpression.prototype.evaluate = function(node, type) {
+ return new win.XPathResult(this.expr.evaluate(new Ctx(node)), type);
+ };
+
+ win.XPathResult = function (value, type) {
+ if (type == 0) {
+ switch (typeof value) {
+ case 'object': type ++; // 4
+ case 'boolean': type ++; // 3
+ case 'string': type ++; // 2
+ case 'number': type ++; // 1
+ }
+ }
+
+ this.resultType = type;
+
+ switch (type) {
+ case 1:
+ this.numberValue = value.isNodeSet ? value.number() : +value;
+ return;
+ case 2:
+ this.stringValue = value.isNodeSet ? value.string() : '' + value;
+ return;
+ case 3:
+ this.booleanValue = value.isNodeSet ? value.bool() : !! value;
+ return;
+ case 4: case 5: case 6: case 7:
+ this.nodes = value.list();
+ this.snapshotLength = value.length;
+ this.index = 0;
+ this.invalidIteratorState = false;
+ break;
+ case 8: case 9:
+ this.singleNodeValue = value.first();
+ return;
+ }
+ };
+
+ win.XPathResult.prototype.iterateNext = function() { return this.nodes[this.index++] };
+ win.XPathResult.prototype.snapshotItem = function(i) { return this.nodes[i] };
+
+ win.XPathResult.ANY_TYPE = 0;
+ win.XPathResult.NUMBER_TYPE = 1;
+ win.XPathResult.STRING_TYPE = 2;
+ win.XPathResult.BOOLEAN_TYPE = 3;
+ win.XPathResult.UNORDERED_NODE_ITERATOR_TYPE = 4;
+ win.XPathResult.ORDERED_NODE_ITERATOR_TYPE = 5;
+ win.XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE = 6;
+ win.XPathResult.ORDERED_NODE_SNAPSHOT_TYPE = 7;
+ win.XPathResult.ANY_UNORDERED_NODE_TYPE = 8;
+ win.XPathResult.FIRST_ORDERED_NODE_TYPE = 9;
+
+
+ doc.createExpression = function(expr) {
+ return new win.XPathExpression(expr, null);
+ };
+
+ doc.evaluate = function(expr, context, _, type) {
+ return doc.createExpression(expr, null).evaluate(context, type);
+ };
+};
+
+var win;
+
+if (config.targetFrame) {
+ var frame = document.getElementById(config.targetFrame);
+ if (frame) win = frame.contentWindow;
+}
+
+if (config.exportInstaller) {
+ window.install = install;
+}
+
+if (!config.hasNative || !config.useNative) {
+ install(win || window);
+}
+
+
+})();
+
+// Thanks for reading this source code. We love JavaScript.
+
diff --git a/tests/selenium/selenium-lib/core/xpath/misc.js b/tests/selenium/selenium-lib/core/xpath/misc.js deleted file mode 100644 index accac3d2..00000000 --- a/tests/selenium/selenium-lib/core/xpath/misc.js +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright 2005 Google Inc.
-// All Rights Reserved
-//
-// Miscellania that support the ajaxslt implementation.
-//
-// Author: Steffen Meschkat <[email protected]>
-//
-
-function el(i) {
- return document.getElementById(i);
-}
-
-function px(x) {
- return x + 'px';
-}
-
-// Split a string s at all occurrences of character c. This is like
-// the split() method of the string object, but IE omits empty
-// strings, which violates the invariant (s.split(x).join(x) == s).
-function stringSplit(s, c) {
- var a = s.indexOf(c);
- if (a == -1) {
- return [ s ];
- }
-
- var parts = [];
- parts.push(s.substr(0,a));
- while (a != -1) {
- var a1 = s.indexOf(c, a + 1);
- if (a1 != -1) {
- parts.push(s.substr(a + 1, a1 - a - 1));
- } else {
- parts.push(s.substr(a + 1));
- }
- a = a1;
- }
-
- return parts;
-}
-
-// Returns the text value if a node; for nodes without children this
-// is the nodeValue, for nodes with children this is the concatenation
-// of the value of all children.
-function xmlValue(node) {
- if (!node) {
- return '';
- }
-
- var ret = '';
- if (node.nodeType == DOM_TEXT_NODE ||
- node.nodeType == DOM_CDATA_SECTION_NODE ||
- node.nodeType == DOM_ATTRIBUTE_NODE) {
- ret += node.nodeValue;
-
- } else if (node.nodeType == DOM_ELEMENT_NODE ||
- node.nodeType == DOM_DOCUMENT_NODE ||
- node.nodeType == DOM_DOCUMENT_FRAGMENT_NODE) {
- for (var i = 0; i < node.childNodes.length; ++i) {
- ret += arguments.callee(node.childNodes[i]);
- }
- }
- return ret;
-}
-
-// Returns the representation of a node as XML text.
-function xmlText(node) {
- var ret = '';
- if (node.nodeType == DOM_TEXT_NODE) {
- ret += xmlEscapeText(node.nodeValue);
-
- } else if (node.nodeType == DOM_ELEMENT_NODE) {
- ret += '<' + node.nodeName;
- for (var i = 0; i < node.attributes.length; ++i) {
- var a = node.attributes[i];
- if (a && a.nodeName && a.nodeValue) {
- ret += ' ' + a.nodeName;
- ret += '="' + xmlEscapeAttr(a.nodeValue) + '"';
- }
- }
-
- if (node.childNodes.length == 0) {
- ret += '/>';
-
- } else {
- ret += '>';
- for (var i = 0; i < node.childNodes.length; ++i) {
- ret += arguments.callee(node.childNodes[i]);
- }
- ret += '</' + node.nodeName + '>';
- }
-
- } else if (node.nodeType == DOM_DOCUMENT_NODE ||
- node.nodeType == DOM_DOCUMENT_FRAGMENT_NODE) {
- for (var i = 0; i < node.childNodes.length; ++i) {
- ret += arguments.callee(node.childNodes[i]);
- }
- }
-
- return ret;
-}
-
-// Applies the given function to each element of the array.
-function mapExec(array, func) {
- for (var i = 0; i < array.length; ++i) {
- func(array[i]);
- }
-}
-
-// Returns an array that contains the return value of the given
-// function applied to every element of the input array.
-function mapExpr(array, func) {
- var ret = [];
- for (var i = 0; i < array.length; ++i) {
- ret.push(func(array[i]));
- }
- return ret;
-};
-
-// Reverses the given array in place.
-function reverseInplace(array) {
- for (var i = 0; i < array.length / 2; ++i) {
- var h = array[i];
- var ii = array.length - i - 1;
- array[i] = array[ii];
- array[ii] = h;
- }
-}
-
-// Shallow-copies an array.
-function copyArray(dst, src) {
- for (var i = 0; i < src.length; ++i) {
- dst.push(src[i]);
- }
-}
-
-function assert(b) {
- if (!b) {
- throw 'assertion failed';
- }
-}
-
-// Based on
-// <http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247>
-var DOM_ELEMENT_NODE = 1;
-var DOM_ATTRIBUTE_NODE = 2;
-var DOM_TEXT_NODE = 3;
-var DOM_CDATA_SECTION_NODE = 4;
-var DOM_ENTITY_REFERENCE_NODE = 5;
-var DOM_ENTITY_NODE = 6;
-var DOM_PROCESSING_INSTRUCTION_NODE = 7;
-var DOM_COMMENT_NODE = 8;
-var DOM_DOCUMENT_NODE = 9;
-var DOM_DOCUMENT_TYPE_NODE = 10;
-var DOM_DOCUMENT_FRAGMENT_NODE = 11;
-var DOM_NOTATION_NODE = 12;
-
-
-var xpathdebug = false; // trace xpath parsing
-var xsltdebug = false; // trace xslt processing
-
-
-// Escape XML special markup chracters: tag delimiter < > and entity
-// reference start delimiter &. The escaped string can be used in XML
-// text portions (i.e. between tags).
-function xmlEscapeText(s) {
- return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
-}
-
-// Escape XML special markup characters: tag delimiter < > entity
-// reference start delimiter & and quotes ". The escaped string can be
-// used in double quoted XML attribute value portions (i.e. in
-// attributes within start tags).
-function xmlEscapeAttr(s) {
- return xmlEscapeText(s).replace(/\"/g, '"');
-}
-
-// Escape markup in XML text, but don't touch entity references. The
-// escaped string can be used as XML text (i.e. between tags).
-function xmlEscapeTags(s) {
- return s.replace(/</g, '<').replace(/>/g, '>');
-}
-
-// An implementation of the debug log.
-
-var logging__ = false;
-
-function Log() {};
-
-Log.lines = [];
-
-Log.write = function(s) {
- LOG.debug("xpath logging: " + s);
-};
-
-// Writes the given XML with every tag on a new line.
-Log.writeXML = function(xml) {
- if (logging__) {
- var s0 = xml.replace(/</g, '\n<');
- var s1 = xmlEscapeText(s0);
- var s2 = s1.replace(/\s*\n(\s|\n)*/g, '<br/>');
- this.lines.push(s2);
- this.show();
- }
-}
-
-// Writes without any escaping
-Log.writeRaw = function(s) {
- if (logging__) {
- this.lines.push(s);
- this.show();
- }
-}
-
-Log.clear = function() {
- if (logging__) {
- var l = this.div();
- l.innerHTML = '';
- this.lines = [];
- }
-}
-
-Log.show = function() {
- var l = this.div();
- l.innerHTML += this.lines.join('<br/>') + '<br/>';
- this.lines = [];
- l.scrollTop = l.scrollHeight;
-}
-
-Log.div = function() {
- var l = document.getElementById('log');
- if (!l) {
- l = document.createElement('div');
- l.id = 'log';
- l.style.position = 'absolute';
- l.style.right = '5px';
- l.style.top = '5px';
- l.style.width = '250px';
- l.style.height = '150px';
- l.style.overflow = 'auto';
- l.style.backgroundColor = '#f0f0f0';
- l.style.border = '1px solid gray';
- l.style.fontSize = '10px';
- l.style.padding = '5px';
- document.body.appendChild(l);
- }
- return l;
-}
-
-
-function Timer() {}
-Timer.start = function() {}
-Timer.end = function() {}
diff --git a/tests/selenium/selenium-lib/core/xpath/util.js b/tests/selenium/selenium-lib/core/xpath/util.js new file mode 100644 index 00000000..cf1b6960 --- /dev/null +++ b/tests/selenium/selenium-lib/core/xpath/util.js @@ -0,0 +1,549 @@ +// Copyright 2005 Google +// +// Author: Steffen Meschkat <[email protected]> +// +// Miscellaneous utility and placeholder functions. + +// Dummy implmentation for the logging functions. Replace by something +// useful when you want to debug. +function xpathLog(msg) {}; +function xsltLog(msg) {}; +function xsltLogXml(msg) {}; + +var ajaxsltIsIE6 = navigator.appVersion.match(/MSIE 6.0/); + +// Throws an exception if false. +function assert(b) { + if (!b) { + throw "Assertion failed"; + } +} + +// Splits a string s at all occurrences of character c. This is like +// the split() method of the string object, but IE omits empty +// strings, which violates the invariant (s.split(x).join(x) == s). +function stringSplit(s, c) { + var a = s.indexOf(c); + if (a == -1) { + return [ s ]; + } + var parts = []; + parts.push(s.substr(0,a)); + while (a != -1) { + var a1 = s.indexOf(c, a + 1); + if (a1 != -1) { + parts.push(s.substr(a + 1, a1 - a - 1)); + } else { + parts.push(s.substr(a + 1)); + } + a = a1; + } + return parts; +} + +// The following function does what document.importNode(node, true) +// would do for us here; however that method is broken in Safari/1.3, +// so we have to emulate it. +function xmlImportNode(doc, node) { + if (node.nodeType == DOM_TEXT_NODE) { + return domCreateTextNode(doc, node.nodeValue); + + } else if (node.nodeType == DOM_CDATA_SECTION_NODE) { + return domCreateCDATASection(doc, node.nodeValue); + + } else if (node.nodeType == DOM_ELEMENT_NODE) { + var newNode = domCreateElement(doc, node.nodeName); + for (var i = 0; i < node.attributes.length; ++i) { + var an = node.attributes[i]; + var name = an.nodeName; + var value = an.nodeValue; + domSetAttribute(newNode, name, value); + } + + for (var c = node.firstChild; c; c = c.nextSibling) { + var cn = arguments.callee(doc, c); + domAppendChild(newNode, cn); + } + + return newNode; + + } else { + return domCreateComment(doc, node.nodeName); + } +} + +// A set data structure. It can also be used as a map (i.e. the keys +// can have values other than 1), but we don't call it map because it +// would be ambiguous in this context. Also, the map is iterable, so +// we can use it to replace for-in loops over core javascript Objects. +// For-in iteration breaks when Object.prototype is modified, which +// some clients of the maps API do. +// +// NOTE(mesch): The set keys by the string value of its element, NOT +// by the typed value. In particular, objects can't be used as keys. +// +// @constructor +function Set() { + this.keys = []; +} + +Set.prototype.size = function() { + return this.keys.length; +} + +// Adds the entry to the set, ignoring if it is present. +Set.prototype.add = function(key, opt_value) { + var value = opt_value || 1; + if (!this.contains(key)) { + this[':' + key] = value; + this.keys.push(key); + } +} + +// Sets the entry in the set, adding if it is not yet present. +Set.prototype.set = function(key, opt_value) { + var value = opt_value || 1; + if (!this.contains(key)) { + this[':' + key] = value; + this.keys.push(key); + } else { + this[':' + key] = value; + } +} + +// Increments the key's value by 1. This works around the fact that +// numbers are always passed by value, never by reference, so that we +// can't increment the value returned by get(), or the iterator +// argument. Sets the key's value to 1 if it doesn't exist yet. +Set.prototype.inc = function(key) { + if (!this.contains(key)) { + this[':' + key] = 1; + this.keys.push(key); + } else { + this[':' + key]++; + } +} + +Set.prototype.get = function(key) { + if (this.contains(key)) { + return this[':' + key]; + } else { + var undefined; + return undefined; + } +} + +// Removes the entry from the set. +Set.prototype.remove = function(key) { + if (this.contains(key)) { + delete this[':' + key]; + removeFromArray(this.keys, key, true); + } +} + +// Tests if an entry is in the set. +Set.prototype.contains = function(entry) { + return typeof this[':' + entry] != 'undefined'; +} + +// Gets a list of values in the set. +Set.prototype.items = function() { + var list = []; + for (var i = 0; i < this.keys.length; ++i) { + var k = this.keys[i]; + var v = this[':' + k]; + list.push(v); + } + return list; +} + + +// Invokes function f for every key value pair in the set as a method +// of the set. +Set.prototype.map = function(f) { + for (var i = 0; i < this.keys.length; ++i) { + var k = this.keys[i]; + f.call(this, k, this[':' + k]); + } +} + +Set.prototype.clear = function() { + for (var i = 0; i < this.keys.length; ++i) { + delete this[':' + this.keys[i]]; + } + this.keys.length = 0; +} + + +// Applies the given function to each element of the array, preserving +// this, and passing the index. +function mapExec(array, func) { + for (var i = 0; i < array.length; ++i) { + func.call(this, array[i], i); + } +} + +// Returns an array that contains the return value of the given +// function applied to every element of the input array. +function mapExpr(array, func) { + var ret = []; + for (var i = 0; i < array.length; ++i) { + ret.push(func(array[i])); + } + return ret; +}; + +// Reverses the given array in place. +function reverseInplace(array) { + for (var i = 0; i < array.length / 2; ++i) { + var h = array[i]; + var ii = array.length - i - 1; + array[i] = array[ii]; + array[ii] = h; + } +} + +// Removes value from array. Returns the number of instances of value +// that were removed from array. +function removeFromArray(array, value, opt_notype) { + var shift = 0; + for (var i = 0; i < array.length; ++i) { + if (array[i] === value || (opt_notype && array[i] == value)) { + array.splice(i--, 1); + shift++; + } + } + return shift; +} + +// Shallow-copies an array to the end of another array +// Basically Array.concat, but works with other non-array collections +function copyArray(dst, src) { + if (!src) return; + var dstLength = dst.length; + for (var i = src.length - 1; i >= 0; --i) { + dst[i+dstLength] = src[i]; + } +} + +/** + * This is an optimization for copying attribute lists in IE. IE includes many + * extraneous properties in its DOM attribute lists, which take require + * significant extra processing when evaluating attribute steps. With this + * function, we ignore any such attributes that has an empty string value. + */ +function copyArrayIgnoringAttributesWithoutValue(dst, src) +{ + if (!src) return; + for (var i = src.length - 1; i >= 0; --i) { + // this test will pass so long as the attribute has a non-empty string + // value, even if that value is "false", "0", "undefined", etc. + if (src[i].nodeValue) { + dst.push(src[i]); + } + } +} + +// Returns the text value of a node; for nodes without children this +// is the nodeValue, for nodes with children this is the concatenation +// of the value of all children. Browser-specific optimizations are used by +// default; they can be disabled by passing "true" in as the second parameter. +function xmlValue(node, disallowBrowserSpecificOptimization) { + if (!node) { + return ''; + } + + var ret = ''; + if (node.nodeType == DOM_TEXT_NODE || + node.nodeType == DOM_CDATA_SECTION_NODE) { + ret += node.nodeValue; + + } else if (node.nodeType == DOM_ATTRIBUTE_NODE) { + if (ajaxsltIsIE6) { + ret += xmlValueIE6Hack(node); + } else { + ret += node.nodeValue; + } + } else if (node.nodeType == DOM_ELEMENT_NODE || + node.nodeType == DOM_DOCUMENT_NODE || + node.nodeType == DOM_DOCUMENT_FRAGMENT_NODE) { + if (!disallowBrowserSpecificOptimization) { + // IE, Safari, Opera, and friends + var innerText = node.innerText; + if (innerText != undefined) { + return innerText; + } + // Firefox + var textContent = node.textContent; + if (textContent != undefined) { + return textContent; + } + } + // pobrecito! + var len = node.childNodes.length; + for (var i = 0; i < len; ++i) { + ret += arguments.callee(node.childNodes[i]); + } + } + return ret; +} + +function xmlValueIE6Hack(node) { + // Issue 19, IE6 mangles href attribute when it's a javascript: url + var nodeName = node.nodeName; + var nodeValue = node.nodeValue; + if (nodeName.length != 4) return nodeValue; + if (!/^href$/i.test(nodeName)) return nodeValue; + if (!/^javascript:/.test(nodeValue)) return nodeValue; + return unescape(nodeValue); +} + +// Returns the representation of a node as XML text. +function xmlText(node, opt_cdata) { + var buf = []; + xmlTextR(node, buf, opt_cdata); + return buf.join(''); +} + +function xmlTextR(node, buf, cdata) { + if (node.nodeType == DOM_TEXT_NODE) { + buf.push(xmlEscapeText(node.nodeValue)); + + } else if (node.nodeType == DOM_CDATA_SECTION_NODE) { + if (cdata) { + buf.push(node.nodeValue); + } else { + buf.push('<![CDATA[' + node.nodeValue + ']]>'); + } + + } else if (node.nodeType == DOM_COMMENT_NODE) { + buf.push('<!--' + node.nodeValue + '-->'); + + } else if (node.nodeType == DOM_ELEMENT_NODE) { + buf.push('<' + xmlFullNodeName(node)); + for (var i = 0; i < node.attributes.length; ++i) { + var a = node.attributes[i]; + if (a && a.nodeName && a.nodeValue) { + buf.push(' ' + xmlFullNodeName(a) + '="' + + xmlEscapeAttr(a.nodeValue) + '"'); + } + } + + if (node.childNodes.length == 0) { + buf.push('/>'); + } else { + buf.push('>'); + for (var i = 0; i < node.childNodes.length; ++i) { + arguments.callee(node.childNodes[i], buf, cdata); + } + buf.push('</' + xmlFullNodeName(node) + '>'); + } + + } else if (node.nodeType == DOM_DOCUMENT_NODE || + node.nodeType == DOM_DOCUMENT_FRAGMENT_NODE) { + for (var i = 0; i < node.childNodes.length; ++i) { + arguments.callee(node.childNodes[i], buf, cdata); + } + } +} + +function xmlFullNodeName(n) { + if (n.prefix && n.nodeName.indexOf(n.prefix + ':') != 0) { + return n.prefix + ':' + n.nodeName; + } else { + return n.nodeName; + } +} + +// Escape XML special markup chracters: tag delimiter < > and entity +// reference start delimiter &. The escaped string can be used in XML +// text portions (i.e. between tags). +function xmlEscapeText(s) { + return ('' + s).replace(/&/g, '&').replace(/</g, '<'). + replace(/>/g, '>'); +} + +// Escape XML special markup characters: tag delimiter < > entity +// reference start delimiter & and quotes ". The escaped string can be +// used in double quoted XML attribute value portions (i.e. in +// attributes within start tags). +function xmlEscapeAttr(s) { + return xmlEscapeText(s).replace(/\"/g, '"'); +} + +// Escape markup in XML text, but don't touch entity references. The +// escaped string can be used as XML text (i.e. between tags). +function xmlEscapeTags(s) { + return s.replace(/</g, '<').replace(/>/g, '>'); +} + +/** + * Wrapper function to access the owner document uniformly for document + * and other nodes: for the document node, the owner document is the + * node itself, for all others it's the ownerDocument property. + * + * @param {Node} node + * @return {Document} + */ +function xmlOwnerDocument(node) { + if (node.nodeType == DOM_DOCUMENT_NODE) { + return node; + } else { + return node.ownerDocument; + } +} + +// Wrapper around DOM methods so we can condense their invocations. +function domGetAttribute(node, name) { + return node.getAttribute(name); +} + +function domSetAttribute(node, name, value) { + return node.setAttribute(name, value); +} + +function domRemoveAttribute(node, name) { + return node.removeAttribute(name); +} + +function domAppendChild(node, child) { + return node.appendChild(child); +} + +function domRemoveChild(node, child) { + return node.removeChild(child); +} + +function domReplaceChild(node, newChild, oldChild) { + return node.replaceChild(newChild, oldChild); +} + +function domInsertBefore(node, newChild, oldChild) { + return node.insertBefore(newChild, oldChild); +} + +function domRemoveNode(node) { + return domRemoveChild(node.parentNode, node); +} + +function domCreateTextNode(doc, text) { + return doc.createTextNode(text); +} + +function domCreateElement(doc, name) { + return doc.createElement(name); +} + +function domCreateAttribute(doc, name) { + return doc.createAttribute(name); +} + +function domCreateCDATASection(doc, data) { + return doc.createCDATASection(data); +} + +function domCreateComment(doc, text) { + return doc.createComment(text); +} + +function domCreateDocumentFragment(doc) { + return doc.createDocumentFragment(); +} + +function domGetElementById(doc, id) { + return doc.getElementById(id); +} + +// Same for window methods. +function windowSetInterval(win, fun, time) { + return win.setInterval(fun, time); +} + +function windowClearInterval(win, id) { + return win.clearInterval(id); +} + +/** + * Escape the special regular expression characters when the regular expression + * is specified as a string. + * + * Based on: http://simonwillison.net/2006/Jan/20/escape/ + */ +RegExp.escape = (function() { + var specials = [ + '/', '.', '*', '+', '?', '|', '^', '$', + '(', ')', '[', ']', '{', '}', '\\' + ]; + + var sRE = new RegExp( + '(\\' + specials.join('|\\') + ')', 'g' + ); + + return function(text) { + return text.replace(sRE, '\\$1'); + } +})(); + +/** + * Determines whether a predicate expression contains a "positional selector". + * A positional selector filters nodes from the nodelist input based on their + * position within that list. When such selectors are encountered, the + * evaluation of the predicate cannot be depth-first, because the positional + * selector may be based on the result of evaluating predicates that precede + * it. + */ +function predicateExprHasPositionalSelector(expr, isRecursiveCall) { + if (!expr) { + return false; + } + if (!isRecursiveCall && exprReturnsNumberValue(expr)) { + // this is a "proximity position"-based predicate + return true; + } + if (expr instanceof FunctionCallExpr) { + var value = expr.name.value; + return (value == 'last' || value == 'position'); + } + if (expr instanceof BinaryExpr) { + return ( + predicateExprHasPositionalSelector(expr.expr1, true) || + predicateExprHasPositionalSelector(expr.expr2, true)); + } + return false; +} + +function exprReturnsNumberValue(expr) { + if (expr instanceof FunctionCallExpr) { + var isMember = { + last: true + , position: true + , count: true + , 'string-length': true + , number: true + , sum: true + , floor: true + , ceiling: true + , round: true + }; + return isMember[expr.name.value]; + } + else if (expr instanceof UnaryMinusExpr) { + return true; + } + else if (expr instanceof BinaryExpr) { + var isMember = { + '+': true + , '-': true + , '*': true + , mod: true + , div: true + }; + return isMember[expr.op.value]; + } + else if (expr instanceof NumberExpr) { + return true; + } + return false; +} + + diff --git a/tests/selenium/selenium-lib/core/xpath/xmltoken.js b/tests/selenium/selenium-lib/core/xpath/xmltoken.js new file mode 100644 index 00000000..30c58a39 --- /dev/null +++ b/tests/selenium/selenium-lib/core/xpath/xmltoken.js @@ -0,0 +1,149 @@ +// Copyright 2006 Google Inc. +// All Rights Reserved +// +// Defines regular expression patterns to extract XML tokens from string. +// See <http://www.w3.org/TR/REC-xml/#sec-common-syn>, +// <http://www.w3.org/TR/xml11/#sec-common-syn> and +// <http://www.w3.org/TR/REC-xml-names/#NT-NCName> for the specifications. +// +// Author: Junji Takagi <[email protected]> + +// Detect whether RegExp supports Unicode characters or not. + +var REGEXP_UNICODE = function() { + var tests = [' ', '\u0120', -1, // Konquerer 3.4.0 fails here. + '!', '\u0120', -1, + '\u0120', '\u0120', 0, + '\u0121', '\u0120', -1, + '\u0121', '\u0120|\u0121', 0, + '\u0122', '\u0120|\u0121', -1, + '\u0120', '[\u0120]', 0, // Safari 2.0.3 fails here. + '\u0121', '[\u0120]', -1, + '\u0121', '[\u0120\u0121]', 0, // Safari 2.0.3 fails here. + '\u0122', '[\u0120\u0121]', -1, + '\u0121', '[\u0120-\u0121]', 0, // Safari 2.0.3 fails here. + '\u0122', '[\u0120-\u0121]', -1]; + for (var i = 0; i < tests.length; i += 3) { + if (tests[i].search(new RegExp(tests[i + 1])) != tests[i + 2]) { + return false; + } + } + return true; +}(); + +// Common tokens in XML 1.0 and XML 1.1. + +var XML_S = '[ \t\r\n]+'; +var XML_EQ = '(' + XML_S + ')?=(' + XML_S + ')?'; +var XML_CHAR_REF = '&#[0-9]+;|&#x[0-9a-fA-F]+;'; + +// XML 1.0 tokens. + +var XML10_VERSION_INFO = XML_S + 'version' + XML_EQ + '("1\\.0"|' + "'1\\.0')"; +var XML10_BASE_CHAR = (REGEXP_UNICODE) ? + '\u0041-\u005a\u0061-\u007a\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff' + + '\u0100-\u0131\u0134-\u013e\u0141-\u0148\u014a-\u017e\u0180-\u01c3' + + '\u01cd-\u01f0\u01f4-\u01f5\u01fa-\u0217\u0250-\u02a8\u02bb-\u02c1\u0386' + + '\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03ce\u03d0-\u03d6\u03da\u03dc' + + '\u03de\u03e0\u03e2-\u03f3\u0401-\u040c\u040e-\u044f\u0451-\u045c' + + '\u045e-\u0481\u0490-\u04c4\u04c7-\u04c8\u04cb-\u04cc\u04d0-\u04eb' + + '\u04ee-\u04f5\u04f8-\u04f9\u0531-\u0556\u0559\u0561-\u0586\u05d0-\u05ea' + + '\u05f0-\u05f2\u0621-\u063a\u0641-\u064a\u0671-\u06b7\u06ba-\u06be' + + '\u06c0-\u06ce\u06d0-\u06d3\u06d5\u06e5-\u06e6\u0905-\u0939\u093d' + + '\u0958-\u0961\u0985-\u098c\u098f-\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2' + + '\u09b6-\u09b9\u09dc-\u09dd\u09df-\u09e1\u09f0-\u09f1\u0a05-\u0a0a' + + '\u0a0f-\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32-\u0a33\u0a35-\u0a36' + + '\u0a38-\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8b\u0a8d' + + '\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2-\u0ab3\u0ab5-\u0ab9' + + '\u0abd\u0ae0\u0b05-\u0b0c\u0b0f-\u0b10\u0b13-\u0b28\u0b2a-\u0b30' + + '\u0b32-\u0b33\u0b36-\u0b39\u0b3d\u0b5c-\u0b5d\u0b5f-\u0b61\u0b85-\u0b8a' + + '\u0b8e-\u0b90\u0b92-\u0b95\u0b99-\u0b9a\u0b9c\u0b9e-\u0b9f\u0ba3-\u0ba4' + + '\u0ba8-\u0baa\u0bae-\u0bb5\u0bb7-\u0bb9\u0c05-\u0c0c\u0c0e-\u0c10' + + '\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c60-\u0c61\u0c85-\u0c8c' + + '\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cde\u0ce0-\u0ce1' + + '\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d28\u0d2a-\u0d39\u0d60-\u0d61' + + '\u0e01-\u0e2e\u0e30\u0e32-\u0e33\u0e40-\u0e45\u0e81-\u0e82\u0e84' + + '\u0e87-\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5' + + '\u0ea7\u0eaa-\u0eab\u0ead-\u0eae\u0eb0\u0eb2-\u0eb3\u0ebd\u0ec0-\u0ec4' + + '\u0f40-\u0f47\u0f49-\u0f69\u10a0-\u10c5\u10d0-\u10f6\u1100\u1102-\u1103' + + '\u1105-\u1107\u1109\u110b-\u110c\u110e-\u1112\u113c\u113e\u1140\u114c' + + '\u114e\u1150\u1154-\u1155\u1159\u115f-\u1161\u1163\u1165\u1167\u1169' + + '\u116d-\u116e\u1172-\u1173\u1175\u119e\u11a8\u11ab\u11ae-\u11af' + + '\u11b7-\u11b8\u11ba\u11bc-\u11c2\u11eb\u11f0\u11f9\u1e00-\u1e9b' + + '\u1ea0-\u1ef9\u1f00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d' + + '\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc' + + '\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec' + + '\u1ff2-\u1ff4\u1ff6-\u1ffc\u2126\u212a-\u212b\u212e\u2180-\u2182' + + '\u3041-\u3094\u30a1-\u30fa\u3105-\u312c\uac00-\ud7a3' : + 'A-Za-z'; +var XML10_IDEOGRAPHIC = (REGEXP_UNICODE) ? + '\u4e00-\u9fa5\u3007\u3021-\u3029' : + ''; +var XML10_COMBINING_CHAR = (REGEXP_UNICODE) ? + '\u0300-\u0345\u0360-\u0361\u0483-\u0486\u0591-\u05a1\u05a3-\u05b9' + + '\u05bb-\u05bd\u05bf\u05c1-\u05c2\u05c4\u064b-\u0652\u0670\u06d6-\u06dc' + + '\u06dd-\u06df\u06e0-\u06e4\u06e7-\u06e8\u06ea-\u06ed\u0901-\u0903\u093c' + + '\u093e-\u094c\u094d\u0951-\u0954\u0962-\u0963\u0981-\u0983\u09bc\u09be' + + '\u09bf\u09c0-\u09c4\u09c7-\u09c8\u09cb-\u09cd\u09d7\u09e2-\u09e3\u0a02' + + '\u0a3c\u0a3e\u0a3f\u0a40-\u0a42\u0a47-\u0a48\u0a4b-\u0a4d\u0a70-\u0a71' + + '\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0b01-\u0b03' + + '\u0b3c\u0b3e-\u0b43\u0b47-\u0b48\u0b4b-\u0b4d\u0b56-\u0b57\u0b82-\u0b83' + + '\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0c01-\u0c03\u0c3e-\u0c44' + + '\u0c46-\u0c48\u0c4a-\u0c4d\u0c55-\u0c56\u0c82-\u0c83\u0cbe-\u0cc4' + + '\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5-\u0cd6\u0d02-\u0d03\u0d3e-\u0d43' + + '\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1' + + '\u0eb4-\u0eb9\u0ebb-\u0ebc\u0ec8-\u0ecd\u0f18-\u0f19\u0f35\u0f37\u0f39' + + '\u0f3e\u0f3f\u0f71-\u0f84\u0f86-\u0f8b\u0f90-\u0f95\u0f97\u0f99-\u0fad' + + '\u0fb1-\u0fb7\u0fb9\u20d0-\u20dc\u20e1\u302a-\u302f\u3099\u309a' : + ''; +var XML10_DIGIT = (REGEXP_UNICODE) ? + '\u0030-\u0039\u0660-\u0669\u06f0-\u06f9\u0966-\u096f\u09e6-\u09ef' + + '\u0a66-\u0a6f\u0ae6-\u0aef\u0b66-\u0b6f\u0be7-\u0bef\u0c66-\u0c6f' + + '\u0ce6-\u0cef\u0d66-\u0d6f\u0e50-\u0e59\u0ed0-\u0ed9\u0f20-\u0f29' : + '0-9'; +var XML10_EXTENDER = (REGEXP_UNICODE) ? + '\u00b7\u02d0\u02d1\u0387\u0640\u0e46\u0ec6\u3005\u3031-\u3035' + + '\u309d-\u309e\u30fc-\u30fe' : + ''; +var XML10_LETTER = XML10_BASE_CHAR + XML10_IDEOGRAPHIC; +var XML10_NAME_CHAR = XML10_LETTER + XML10_DIGIT + '\\._:' + + XML10_COMBINING_CHAR + XML10_EXTENDER + '-'; +var XML10_NAME = '[' + XML10_LETTER + '_:][' + XML10_NAME_CHAR + ']*'; + +var XML10_ENTITY_REF = '&' + XML10_NAME + ';'; +var XML10_REFERENCE = XML10_ENTITY_REF + '|' + XML_CHAR_REF; +var XML10_ATT_VALUE = '"(([^<&"]|' + XML10_REFERENCE + ')*)"|' + + "'(([^<&']|" + XML10_REFERENCE + ")*)'"; +var XML10_ATTRIBUTE = + '(' + XML10_NAME + ')' + XML_EQ + '(' + XML10_ATT_VALUE + ')'; + +// XML 1.1 tokens. +// TODO(jtakagi): NameStartChar also includes \u10000-\ueffff. +// ECMAScript Language Specifiction defines UnicodeEscapeSequence as +// "\u HexDigit HexDigit HexDigit HexDigit" and we may need to use +// surrogate pairs, but any browser doesn't support surrogate paris in +// character classes of regular expression, so avoid including them for now. + +var XML11_VERSION_INFO = XML_S + 'version' + XML_EQ + '("1\\.1"|' + "'1\\.1')"; +var XML11_NAME_START_CHAR = (REGEXP_UNICODE) ? + ':A-Z_a-z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02ff\u0370-\u037d' + + '\u037f-\u1fff\u200c-\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff' + + '\uf900-\ufdcf\ufdf0-\ufffd' : + ':A-Z_a-z'; +var XML11_NAME_CHAR = XML11_NAME_START_CHAR + + ((REGEXP_UNICODE) ? '\\.0-9\u00b7\u0300-\u036f\u203f-\u2040-' : '\\.0-9-'); +var XML11_NAME = '[' + XML11_NAME_START_CHAR + '][' + XML11_NAME_CHAR + ']*'; + +var XML11_ENTITY_REF = '&' + XML11_NAME + ';'; +var XML11_REFERENCE = XML11_ENTITY_REF + '|' + XML_CHAR_REF; +var XML11_ATT_VALUE = '"(([^<&"]|' + XML11_REFERENCE + ')*)"|' + + "'(([^<&']|" + XML11_REFERENCE + ")*)'"; +var XML11_ATTRIBUTE = + '(' + XML11_NAME + ')' + XML_EQ + '(' + XML11_ATT_VALUE + ')'; + +// XML Namespace tokens. +// Used in XML parser and XPath parser. + +var XML_NC_NAME_CHAR = XML10_LETTER + XML10_DIGIT + '\\._' + + XML10_COMBINING_CHAR + XML10_EXTENDER + '-'; +var XML_NC_NAME = '[' + XML10_LETTER + '_][' + XML_NC_NAME_CHAR + ']*'; diff --git a/tests/selenium/selenium-lib/core/xpath/xpath.js b/tests/selenium/selenium-lib/core/xpath/xpath.js index d05356ba..c920d1cc 100644 --- a/tests/selenium/selenium-lib/core/xpath/xpath.js +++ b/tests/selenium/selenium-lib/core/xpath/xpath.js @@ -1,2223 +1,2481 @@ -// Copyright 2005 Google Inc.
-// All Rights Reserved
-//
-// An XPath parser and evaluator written in JavaScript. The
-// implementation is complete except for functions handling
-// namespaces.
-//
-// Reference: [XPATH] XPath Specification
-// <http://www.w3.org/TR/1999/REC-xpath-19991116>.
-//
-//
-// The API of the parser has several parts:
-//
-// 1. The parser function xpathParse() that takes a string and returns
-// an expession object.
-//
-// 2. The expression object that has an evaluate() method to evaluate the
-// XPath expression it represents. (It is actually a hierarchy of
-// objects that resembles the parse tree, but an application will call
-// evaluate() only on the top node of this hierarchy.)
-//
-// 3. The context object that is passed as an argument to the evaluate()
-// method, which represents the DOM context in which the expression is
-// evaluated.
-//
-// 4. The value object that is returned from evaluate() and represents
-// values of the different types that are defined by XPath (number,
-// string, boolean, and node-set), and allows to convert between them.
-//
-// These parts are near the top of the file, the functions and data
-// that are used internally follow after them.
-//
-//
-// TODO(mesch): add jsdoc comments. Use more coherent naming.
-//
-//
-// Author: Steffen Meschkat <[email protected]>
-
-
-// The entry point for the parser.
-//
-// @param expr a string that contains an XPath expression.
-// @return an expression object that can be evaluated with an
-// expression context.
-
-function xpathParse(expr) {
- //xpathdebug = true;
- if (xpathdebug) {
- Log.write('XPath parse ' + expr);
- }
- xpathParseInit();
-
- var cached = xpathCacheLookup(expr);
- if (cached) {
- if (xpathdebug) {
- Log.write(' ... cached');
- }
- return cached;
- }
-
- // Optimize for a few common cases: simple attribute node tests
- // (@id), simple element node tests (page), variable references
- // ($address), numbers (4), multi-step path expressions where each
- // step is a plain element node test
- // (page/overlay/locations/location).
-
- if (expr.match(/^(\$|@)?\w+$/i)) {
- var ret = makeSimpleExpr(expr);
- xpathParseCache[expr] = ret;
- if (xpathdebug) {
- Log.write(' ... simple');
- }
- return ret;
- }
-
- if (expr.match(/^\w+(\/\w+)*$/i)) {
- var ret = makeSimpleExpr2(expr);
- xpathParseCache[expr] = ret;
- if (xpathdebug) {
- Log.write(' ... simple 2');
- }
- return ret;
- }
-
- var cachekey = expr; // expr is modified during parse
- if (xpathdebug) {
- Timer.start('XPath parse', cachekey);
- }
-
- var stack = [];
- var ahead = null;
- var previous = null;
- var done = false;
-
- var parse_count = 0;
- var lexer_count = 0;
- var reduce_count = 0;
-
- while (!done) {
- parse_count++;
- expr = expr.replace(/^\s*/, '');
- previous = ahead;
- ahead = null;
-
- var rule = null;
- var match = '';
- for (var i = 0; i < xpathTokenRules.length; ++i) {
- var result = xpathTokenRules[i].re.exec(expr);
- lexer_count++;
- if (result && result.length > 0 && result[0].length > match.length) {
- rule = xpathTokenRules[i];
- match = result[0];
- break;
- }
- }
-
- // Special case: allow operator keywords to be element and
- // variable names.
-
- // NOTE(mesch): The parser resolves conflicts by looking ahead,
- // and this is the only case where we look back to
- // disambiguate. So this is indeed something different, and
- // looking back is usually done in the lexer (via states in the
- // general case, called "start conditions" in flex(1)). Also,the
- // conflict resolution in the parser is not as robust as it could
- // be, so I'd like to keep as much off the parser as possible (all
- // these precedence values should be computed from the grammar
- // rules and possibly associativity declarations, as in bison(1),
- // and not explicitly set.
-
- if (rule &&
- (rule == TOK_DIV ||
- rule == TOK_MOD ||
- rule == TOK_AND ||
- rule == TOK_OR) &&
- (!previous ||
- previous.tag == TOK_AT ||
- previous.tag == TOK_DSLASH ||
- previous.tag == TOK_SLASH ||
- previous.tag == TOK_AXIS ||
- previous.tag == TOK_DOLLAR)) {
- rule = TOK_QNAME;
- }
-
- if (rule) {
- expr = expr.substr(match.length);
- if (xpathdebug) {
- Log.write('token: ' + match + ' -- ' + rule.label);
- }
- ahead = {
- tag: rule,
- match: match,
- prec: rule.prec ? rule.prec : 0, // || 0 is removed by the compiler
- expr: makeTokenExpr(match)
- };
-
- } else {
- if (xpathdebug) {
- Log.write('DONE');
- }
- done = true;
- }
-
- while (xpathReduce(stack, ahead)) {
- reduce_count++;
- if (xpathdebug) {
- Log.write('stack: ' + stackToString(stack));
- }
- }
- }
-
- if (xpathdebug) {
- Log.write(stackToString(stack));
- }
-
- // DGF any valid XPath should "reduce" to a single Expr token
- if (stack.length != 1) {
- throw 'XPath parse error ' + cachekey + ':\n' + stackToString(stack);
- }
-
- var result = stack[0].expr;
- xpathParseCache[cachekey] = result;
-
- if (xpathdebug) {
- Timer.end('XPath parse', cachekey);
- }
-
- if (xpathdebug) {
- Log.write('XPath parse: ' + parse_count + ' / ' +
- lexer_count + ' / ' + reduce_count);
- }
-
- return result;
-}
-
-var xpathParseCache = {};
-
-function xpathCacheLookup(expr) {
- return xpathParseCache[expr];
-}
-
-/*DGF xpathReduce is where the magic happens in this parser.
-Skim down to the bottom of this file to find the table of
-grammatical rules and precedence numbers, "The productions of the grammar".
-
-The idea here
-is that we want to take a stack of tokens and apply
-grammatical rules to them, "reducing" them to higher-level
-tokens. Ultimately, any valid XPath should reduce to exactly one
-"Expr" token.
-
-Reduce too early or too late and you'll have two tokens that can't reduce
-to single Expr. For example, you may hastily reduce a qname that
-should name a function, incorrectly treating it as a tag name.
-Or you may reduce too late, accidentally reducing the last part of the
-XPath into a top-level "Expr" that won't reduce with earlier parts of
-the XPath.
-
-A "cand" is a grammatical rule candidate, with a given precedence
-number. "ahead" is the upcoming token, which also has a precedence
-number. If the token has a higher precedence number than
-the rule candidate, we'll "shift" the token onto the token stack,
-instead of immediately applying the rule candidate.
-
-Some tokens have left associativity, in which case we shift when they
-have LOWER precedence than the candidate.
-*/
-function xpathReduce(stack, ahead) {
- var cand = null;
-
- if (stack.length > 0) {
- var top = stack[stack.length-1];
- var ruleset = xpathRules[top.tag.key];
-
- if (ruleset) {
- for (var i = 0; i < ruleset.length; ++i) {
- var rule = ruleset[i];
- var match = xpathMatchStack(stack, rule[1]);
- if (match.length) {
- cand = {
- tag: rule[0],
- rule: rule,
- match: match
- };
- cand.prec = xpathGrammarPrecedence(cand);
- break;
- }
- }
- }
- }
-
- var ret;
- if (cand && (!ahead || cand.prec > ahead.prec ||
- (ahead.tag.left && cand.prec >= ahead.prec))) {
- for (var i = 0; i < cand.match.matchlength; ++i) {
- stack.pop();
- }
-
- if (xpathdebug) {
- Log.write('reduce ' + cand.tag.label + ' ' + cand.prec +
- ' ahead ' + (ahead ? ahead.tag.label + ' ' + ahead.prec +
- (ahead.tag.left ? ' left' : '')
- : ' none '));
- }
-
- var matchexpr = mapExpr(cand.match, function(m) { return m.expr; });
- if (xpathdebug) {
- Log.write('about to run ' + cand.rule[3].toString());
- }
- cand.expr = cand.rule[3].apply(null, matchexpr);
-
- stack.push(cand);
- ret = true;
-
- } else {
- if (ahead) {
- if (xpathdebug) {
- Log.write('shift ' + ahead.tag.label + ' ' + ahead.prec +
- (ahead.tag.left ? ' left' : '') +
- ' over ' + (cand ? cand.tag.label + ' ' +
- cand.prec : ' none'));
- }
- stack.push(ahead);
- }
- ret = false;
- }
- return ret;
-}
-
-function xpathMatchStack(stack, pattern) {
-
- // NOTE(mesch): The stack matches for variable cardinality are
- // greedy but don't do backtracking. This would be an issue only
- // with rules of the form A* A, i.e. with an element with variable
- // cardinality followed by the same element. Since that doesn't
- // occur in the grammar at hand, all matches on the stack are
- // unambiguous.
-
- var S = stack.length;
- var P = pattern.length;
- var p, s;
- var match = [];
- match.matchlength = 0;
- var ds = 0;
- for (p = P - 1, s = S - 1; p >= 0 && s >= 0; --p, s -= ds) {
- ds = 0;
- var qmatch = [];
- if (pattern[p] == Q_MM) {
- p -= 1;
- match.push(qmatch);
- while (s - ds >= 0 && stack[s - ds].tag == pattern[p]) {
- qmatch.push(stack[s - ds]);
- ds += 1;
- match.matchlength += 1;
- }
-
- } else if (pattern[p] == Q_01) {
- p -= 1;
- match.push(qmatch);
- while (s - ds >= 0 && ds < 2 && stack[s - ds].tag == pattern[p]) {
- qmatch.push(stack[s - ds]);
- ds += 1;
- match.matchlength += 1;
- }
-
- } else if (pattern[p] == Q_1M) {
- p -= 1;
- match.push(qmatch);
- if (stack[s].tag == pattern[p]) {
- while (s - ds >= 0 && stack[s - ds].tag == pattern[p]) {
- qmatch.push(stack[s - ds]);
- ds += 1;
- match.matchlength += 1;
- }
- } else {
- return [];
- }
-
- } else if (stack[s].tag == pattern[p]) {
- match.push(stack[s]);
- ds += 1;
- match.matchlength += 1;
-
- } else {
- return [];
- }
-
- reverseInplace(qmatch);
- qmatch.expr = mapExpr(qmatch, function(m) { return m.expr; });
- }
-
- reverseInplace(match);
-
- if (p == -1) {
- return match;
-
- } else {
- return [];
- }
-}
-
-function xpathTokenPrecedence(tag) {
- return tag.prec || 2;
-}
-
-function xpathGrammarPrecedence(frame) {
- var ret = 0;
-
- if (frame.rule) { /* normal reduce */
- if (frame.rule.length >= 3 && frame.rule[2] >= 0) {
- ret = frame.rule[2];
-
- } else {
- for (var i = 0; i < frame.rule[1].length; ++i) {
- var p = xpathTokenPrecedence(frame.rule[1][i]);
- ret = Math.max(ret, p);
- }
- }
- } else if (frame.tag) { /* TOKEN match */
- ret = xpathTokenPrecedence(frame.tag);
-
- } else if (frame.length) { /* Q_ match */
- for (var j = 0; j < frame.length; ++j) {
- var p = xpathGrammarPrecedence(frame[j]);
- ret = Math.max(ret, p);
- }
- }
-
- return ret;
-}
-
-function stackToString(stack) {
- var ret = '';
- for (var i = 0; i < stack.length; ++i) {
- if (ret) {
- ret += '\n';
- }
- ret += stack[i].tag.label;
- }
- return ret;
-}
-
-
-// XPath expression evaluation context. An XPath context consists of a
-// DOM node, a list of DOM nodes that contains this node, a number
-// that represents the position of the single node in the list, and a
-// current set of variable bindings. (See XPath spec.)
-//
-// The interface of the expression context:
-//
-// Constructor -- gets the node, its position, the node set it
-// belongs to, and a parent context as arguments. The parent context
-// is used to implement scoping rules for variables: if a variable
-// is not found in the current context, it is looked for in the
-// parent context, recursively. Except for node, all arguments have
-// default values: default position is 0, default node set is the
-// set that contains only the node, and the default parent is null.
-//
-// Notice that position starts at 0 at the outside interface;
-// inside XPath expressions this shows up as position()=1.
-//
-// clone() -- creates a new context with the current context as
-// parent. If passed as argument to clone(), the new context has a
-// different node, position, or node set. What is not passed is
-// inherited from the cloned context.
-//
-// setVariable(name, expr) -- binds given XPath expression to the
-// name.
-//
-// getVariable(name) -- what the name says.
-//
-// setNode(node, position) -- sets the context to the new node and
-// its corresponding position. Needed to implement scoping rules for
-// variables in XPath. (A variable is visible to all subsequent
-// siblings, not only to its children.)
-
-function ExprContext(node, position, nodelist, parent) {
- this.node = node;
- this.position = position || 0;
- this.nodelist = nodelist || [ node ];
- this.variables = {};
- this.parent = parent || null;
- this.root = parent ? parent.root : node.ownerDocument;
-}
-
-ExprContext.prototype.clone = function(node, position, nodelist) {
- return new
- ExprContext(node || this.node,
- typeof position != 'undefined' ? position : this.position,
- nodelist || this.nodelist, this);
-};
-
-ExprContext.prototype.setVariable = function(name, value) {
- this.variables[name] = value;
-};
-
-ExprContext.prototype.getVariable = function(name) {
- if (typeof this.variables[name] != 'undefined') {
- return this.variables[name];
-
- } else if (this.parent) {
- return this.parent.getVariable(name);
-
- } else {
- return null;
- }
-}
-
-ExprContext.prototype.setNode = function(node, position) {
- this.node = node;
- this.position = position;
-}
-
-
-// XPath expression values. They are what XPath expressions evaluate
-// to. Strangely, the different value types are not specified in the
-// XPath syntax, but only in the semantics, so they don't show up as
-// nonterminals in the grammar. Yet, some expressions are required to
-// evaluate to particular types, and not every type can be coerced
-// into every other type. Although the types of XPath values are
-// similar to the types present in JavaScript, the type coercion rules
-// are a bit peculiar, so we explicitly model XPath types instead of
-// mapping them onto JavaScript types. (See XPath spec.)
-//
-// The four types are:
-//
-// StringValue
-//
-// NumberValue
-//
-// BooleanValue
-//
-// NodeSetValue
-//
-// The common interface of the value classes consists of methods that
-// implement the XPath type coercion rules:
-//
-// stringValue() -- returns the value as a JavaScript String,
-//
-// numberValue() -- returns the value as a JavaScript Number,
-//
-// booleanValue() -- returns the value as a JavaScript Boolean,
-//
-// nodeSetValue() -- returns the value as a JavaScript Array of DOM
-// Node objects.
-//
-
-function StringValue(value) {
- this.value = value;
- this.type = 'string';
-}
-
-StringValue.prototype.stringValue = function() {
- return this.value;
-}
-
-StringValue.prototype.booleanValue = function() {
- return this.value.length > 0;
-}
-
-StringValue.prototype.numberValue = function() {
- return this.value - 0;
-}
-
-StringValue.prototype.nodeSetValue = function() {
- throw this + ' ' + Error().stack;
-}
-
-function BooleanValue(value) {
- this.value = value;
- this.type = 'boolean';
-}
-
-BooleanValue.prototype.stringValue = function() {
- return '' + this.value;
-}
-
-BooleanValue.prototype.booleanValue = function() {
- return this.value;
-}
-
-BooleanValue.prototype.numberValue = function() {
- return this.value ? 1 : 0;
-}
-
-BooleanValue.prototype.nodeSetValue = function() {
- throw this + ' ' + Error().stack;
-}
-
-function NumberValue(value) {
- this.value = value;
- this.type = 'number';
-}
-
-NumberValue.prototype.stringValue = function() {
- return '' + this.value;
-}
-
-NumberValue.prototype.booleanValue = function() {
- return !!this.value;
-}
-
-NumberValue.prototype.numberValue = function() {
- return this.value - 0;
-}
-
-NumberValue.prototype.nodeSetValue = function() {
- throw this + ' ' + Error().stack;
-}
-
-function NodeSetValue(value) {
- this.value = value;
- this.type = 'node-set';
-}
-
-NodeSetValue.prototype.stringValue = function() {
- if (this.value.length == 0) {
- return '';
- } else {
- return xmlValue(this.value[0]);
- }
-}
-
-NodeSetValue.prototype.booleanValue = function() {
- return this.value.length > 0;
-}
-
-NodeSetValue.prototype.numberValue = function() {
- return this.stringValue() - 0;
-}
-
-NodeSetValue.prototype.nodeSetValue = function() {
- return this.value;
-};
-
-// XPath expressions. They are used as nodes in the parse tree and
-// possess an evaluate() method to compute an XPath value given an XPath
-// context. Expressions are returned from the parser. Teh set of
-// expression classes closely mirrors the set of non terminal symbols
-// in the grammar. Every non trivial nonterminal symbol has a
-// corresponding expression class.
-//
-// The common expression interface consists of the following methods:
-//
-// evaluate(context) -- evaluates the expression, returns a value.
-//
-// toString() -- returns the XPath text representation of the
-// expression (defined in xsltdebug.js).
-//
-// parseTree(indent) -- returns a parse tree representation of the
-// expression (defined in xsltdebug.js).
-
-function TokenExpr(m) {
- this.value = m;
-}
-
-TokenExpr.prototype.evaluate = function() {
- return new StringValue(this.value);
-};
-
-function LocationExpr() {
- this.absolute = false;
- this.steps = [];
-}
-
-LocationExpr.prototype.appendStep = function(s) {
- this.steps.push(s);
-}
-
-LocationExpr.prototype.prependStep = function(s) {
- var steps0 = this.steps;
- this.steps = [ s ];
- for (var i = 0; i < steps0.length; ++i) {
- this.steps.push(steps0[i]);
- }
-};
-
-LocationExpr.prototype.evaluate = function(ctx) {
- var start;
- if (this.absolute) {
- start = ctx.root;
-
- } else {
- start = ctx.node;
- }
-
- var nodes = [];
- xPathStep(nodes, this.steps, 0, start, ctx);
- return new NodeSetValue(nodes);
-};
-
-function xPathStep(nodes, steps, step, input, ctx) {
- var s = steps[step];
- var ctx2 = ctx.clone(input);
- var nodelist = s.evaluate(ctx2).nodeSetValue();
-
- for (var i = 0; i < nodelist.length; ++i) {
- if (step == steps.length - 1) {
- nodes.push(nodelist[i]);
- } else {
- xPathStep(nodes, steps, step + 1, nodelist[i], ctx);
- }
- }
-}
-
-function StepExpr(axis, nodetest, predicate) {
- this.axis = axis;
- this.nodetest = nodetest;
- this.predicate = predicate || [];
-}
-
-StepExpr.prototype.appendPredicate = function(p) {
- this.predicate.push(p);
-}
-
-StepExpr.prototype.evaluate = function(ctx) {
- var input = ctx.node;
- var nodelist = [];
-
- // NOTE(mesch): When this was a switch() statement, it didn't work
- // in Safari/2.0. Not sure why though; it resulted in the JavaScript
- // console output "undefined" (without any line number or so).
-
- if (this.axis == xpathAxis.ANCESTOR_OR_SELF) {
- nodelist.push(input);
- for (var n = input.parentNode; n; n = input.parentNode) {
- nodelist.push(n);
- }
-
- } else if (this.axis == xpathAxis.ANCESTOR) {
- for (var n = input.parentNode; n; n = input.parentNode) {
- nodelist.push(n);
- }
-
- } else if (this.axis == xpathAxis.ATTRIBUTE) {
- copyArray(nodelist, input.attributes);
-
- } else if (this.axis == xpathAxis.CHILD) {
- copyArray(nodelist, input.childNodes);
-
- } else if (this.axis == xpathAxis.DESCENDANT_OR_SELF) {
- nodelist.push(input);
- xpathCollectDescendants(nodelist, input);
-
- } else if (this.axis == xpathAxis.DESCENDANT) {
- xpathCollectDescendants(nodelist, input);
-
- } else if (this.axis == xpathAxis.FOLLOWING) {
- for (var n = input.parentNode; n; n = n.parentNode) {
- for (var nn = n.nextSibling; nn; nn = nn.nextSibling) {
- nodelist.push(nn);
- xpathCollectDescendants(nodelist, nn);
- }
- }
-
- } else if (this.axis == xpathAxis.FOLLOWING_SIBLING) {
- for (var n = input.nextSibling; n; n = input.nextSibling) {
- nodelist.push(n);
- }
-
- } else if (this.axis == xpathAxis.NAMESPACE) {
- alert('not implemented: axis namespace');
-
- } else if (this.axis == xpathAxis.PARENT) {
- if (input.parentNode) {
- nodelist.push(input.parentNode);
- }
-
- } else if (this.axis == xpathAxis.PRECEDING) {
- for (var n = input.parentNode; n; n = n.parentNode) {
- for (var nn = n.previousSibling; nn; nn = nn.previousSibling) {
- nodelist.push(nn);
- xpathCollectDescendantsReverse(nodelist, nn);
- }
- }
-
- } else if (this.axis == xpathAxis.PRECEDING_SIBLING) {
- for (var n = input.previousSibling; n; n = input.previousSibling) {
- nodelist.push(n);
- }
-
- } else if (this.axis == xpathAxis.SELF) {
- nodelist.push(input);
-
- } else {
- throw 'ERROR -- NO SUCH AXIS: ' + this.axis;
- }
-
- // process node test
- var nodelist0 = nodelist;
- nodelist = [];
- for (var i = 0; i < nodelist0.length; ++i) {
- var n = nodelist0[i];
- if (this.nodetest.evaluate(ctx.clone(n, i, nodelist0)).booleanValue()) {
- nodelist.push(n);
- }
- }
-
- // process predicates
- for (var i = 0; i < this.predicate.length; ++i) {
- var nodelist0 = nodelist;
- nodelist = [];
- for (var ii = 0; ii < nodelist0.length; ++ii) {
- var n = nodelist0[ii];
- if (this.predicate[i].evaluate(ctx.clone(n, ii, nodelist0)).booleanValue()) {
- nodelist.push(n);
- }
- }
- }
-
- return new NodeSetValue(nodelist);
-};
-
-function NodeTestAny() {
- this.value = new BooleanValue(true);
-}
-
-NodeTestAny.prototype.evaluate = function(ctx) {
- return this.value;
-};
-
-function NodeTestElement() {}
-
-NodeTestElement.prototype.evaluate = function(ctx) {
- return new BooleanValue(ctx.node.nodeType == DOM_ELEMENT_NODE);
-}
-
-function NodeTestText() {}
-
-NodeTestText.prototype.evaluate = function(ctx) {
- return new BooleanValue(ctx.node.nodeType == DOM_TEXT_NODE);
-}
-
-function NodeTestComment() {}
-
-NodeTestComment.prototype.evaluate = function(ctx) {
- return new BooleanValue(ctx.node.nodeType == DOM_COMMENT_NODE);
-}
-
-function NodeTestPI(target) {
- this.target = target;
-}
-
-NodeTestPI.prototype.evaluate = function(ctx) {
- return new
- BooleanValue(ctx.node.nodeType == DOM_PROCESSING_INSTRUCTION_NODE &&
- (!this.target || ctx.node.nodeName == this.target));
-}
-
-function NodeTestNC(nsprefix) {
- this.regex = new RegExp("^" + nsprefix + ":");
- this.nsprefix = nsprefix;
-}
-
-NodeTestNC.prototype.evaluate = function(ctx) {
- var n = ctx.node;
- return new BooleanValue(this.regex.match(n.nodeName));
-}
-
-function NodeTestName(name) {
- this.name = name;
-}
-
-NodeTestName.prototype.evaluate = function(ctx) {
- var n = ctx.node;
- // NOTE (Patrick Lightbody): this change allows node selection to be case-insensitive
- return new BooleanValue(n.nodeName.toUpperCase() == this.name.toUpperCase());
-}
-
-function PredicateExpr(expr) {
- this.expr = expr;
-}
-
-PredicateExpr.prototype.evaluate = function(ctx) {
- var v = this.expr.evaluate(ctx);
- if (v.type == 'number') {
- // NOTE(mesch): Internally, position is represented starting with
- // 0, however in XPath position starts with 1. See functions
- // position() and last().
- return new BooleanValue(ctx.position == v.numberValue() - 1);
- } else {
- return new BooleanValue(v.booleanValue());
- }
-};
-
-function FunctionCallExpr(name) {
- this.name = name;
- this.args = [];
-}
-
-FunctionCallExpr.prototype.appendArg = function(arg) {
- this.args.push(arg);
-};
-
-FunctionCallExpr.prototype.evaluate = function(ctx) {
- var fn = '' + this.name.value;
- var f = this.xpathfunctions[fn];
- if (f) {
- return f.call(this, ctx);
- } else {
- Log.write('XPath NO SUCH FUNCTION ' + fn);
- return new BooleanValue(false);
- }
-};
-
-FunctionCallExpr.prototype.xpathfunctions = {
- 'last': function(ctx) {
- assert(this.args.length == 0);
- // NOTE(mesch): XPath position starts at 1.
- return new NumberValue(ctx.nodelist.length);
- },
-
- 'position': function(ctx) {
- assert(this.args.length == 0);
- // NOTE(mesch): XPath position starts at 1.
- return new NumberValue(ctx.position + 1);
- },
-
- 'count': function(ctx) {
- assert(this.args.length == 1);
- var v = this.args[0].evaluate(ctx);
- return new NumberValue(v.nodeSetValue().length);
- },
-
- 'id': function(ctx) {
- assert(this.args.length == 1);
- var e = this.args[0].evaluate(ctx);
- var ret = [];
- var ids;
- if (e.type == 'node-set') {
- ids = [];
- for (var i = 0; i < e.length; ++i) {
- var v = xmlValue(e[i]).split(/\s+/);
- for (var ii = 0; ii < v.length; ++ii) {
- ids.push(v[ii]);
- }
- }
- } else {
- ids = e.stringValue().split(/\s+/);
- }
- var contextNode = ctx.node;
- var d;
- if (contextNode.nodeName == "#document") {
- d = contextNode;
- } else {
- d = contextNode.ownerDocument;
- }
- for (var i = 0; i < ids.length; ++i) {
- var n = d.getElementById(ids[i]);
- if (n) {
- ret.push(n);
- }
- }
- return new NodeSetValue(ret);
- },
-
- 'local-name': function(ctx) {
- alert('not implmented yet: XPath function local-name()');
- },
-
- 'namespace-uri': function(ctx) {
- alert('not implmented yet: XPath function namespace-uri()');
- },
-
- 'name': function(ctx) {
- assert(this.args.length == 1 || this.args.length == 0);
- var n;
- if (this.args.length == 0) {
- n = [ ctx.node ];
- } else {
- n = this.args[0].evaluate(ctx).nodeSetValue();
- }
-
- if (n.length == 0) {
- return new StringValue('');
- } else {
- return new StringValue(n[0].nodeName);
- }
- },
-
- 'string': function(ctx) {
- assert(this.args.length == 1 || this.args.length == 0);
- if (this.args.length == 0) {
- return new StringValue(new NodeSetValue([ ctx.node ]).stringValue());
- } else {
- return new StringValue(this.args[0].evaluate(ctx).stringValue());
- }
- },
-
- 'concat': function(ctx) {
- var ret = '';
- for (var i = 0; i < this.args.length; ++i) {
- ret += this.args[i].evaluate(ctx).stringValue();
- }
- return new StringValue(ret);
- },
-
- 'starts-with': function(ctx) {
- assert(this.args.length == 2);
- var s0 = this.args[0].evaluate(ctx).stringValue();
- var s1 = this.args[1].evaluate(ctx).stringValue();
- return new BooleanValue(s0.indexOf(s1) == 0);
- },
-
- 'contains': function(ctx) {
- assert(this.args.length == 2);
- var s0 = this.args[0].evaluate(ctx).stringValue();
- var s1 = this.args[1].evaluate(ctx).stringValue();
- return new BooleanValue(s0.indexOf(s1) != -1);
- },
-
- 'substring-before': function(ctx) {
- assert(this.args.length == 2);
- var s0 = this.args[0].evaluate(ctx).stringValue();
- var s1 = this.args[1].evaluate(ctx).stringValue();
- var i = s0.indexOf(s1);
- var ret;
- if (i == -1) {
- ret = '';
- } else {
- ret = s0.substr(0,i);
- }
- return new StringValue(ret);
- },
-
- 'substring-after': function(ctx) {
- assert(this.args.length == 2);
- var s0 = this.args[0].evaluate(ctx).stringValue();
- var s1 = this.args[1].evaluate(ctx).stringValue();
- var i = s0.indexOf(s1);
- var ret;
- if (i == -1) {
- ret = '';
- } else {
- ret = s0.substr(i + s1.length);
- }
- return new StringValue(ret);
- },
-
- 'substring': function(ctx) {
- // NOTE: XPath defines the position of the first character in a
- // string to be 1, in JavaScript this is 0 ([XPATH] Section 4.2).
- assert(this.args.length == 2 || this.args.length == 3);
- var s0 = this.args[0].evaluate(ctx).stringValue();
- var s1 = this.args[1].evaluate(ctx).numberValue();
- var ret;
- if (this.args.length == 2) {
- var i1 = Math.max(0, Math.round(s1) - 1);
- ret = s0.substr(i1);
-
- } else {
- var s2 = this.args[2].evaluate(ctx).numberValue();
- var i0 = Math.round(s1) - 1;
- var i1 = Math.max(0, i0);
- var i2 = Math.round(s2) - Math.max(0, -i0);
- ret = s0.substr(i1, i2);
- }
- return new StringValue(ret);
- },
-
- 'string-length': function(ctx) {
- var s;
- if (this.args.length > 0) {
- s = this.args[0].evaluate(ctx).stringValue();
- } else {
- s = new NodeSetValue([ ctx.node ]).stringValue();
- }
- return new NumberValue(s.length);
- },
-
- 'normalize-space': function(ctx) {
- var s;
- if (this.args.length > 0) {
- s = this.args[0].evaluate(ctx).stringValue();
- } else {
- s = new NodeSetValue([ ctx.node ]).stringValue();
- }
- s = s.replace(/^\s*/,'').replace(/\s*$/,'').replace(/\s+/g, ' ');
- return new StringValue(s);
- },
-
- 'translate': function(ctx) {
- assert(this.args.length == 3);
- var s0 = this.args[0].evaluate(ctx).stringValue();
- var s1 = this.args[1].evaluate(ctx).stringValue();
- var s2 = this.args[2].evaluate(ctx).stringValue();
-
- for (var i = 0; i < s1.length; ++i) {
- s0 = s0.replace(new RegExp(s1.charAt(i), 'g'), s2.charAt(i));
- }
- return new StringValue(s0);
- },
-
- 'boolean': function(ctx) {
- assert(this.args.length == 1);
- return new BooleanValue(this.args[0].evaluate(ctx).booleanValue());
- },
-
- 'not': function(ctx) {
- assert(this.args.length == 1);
- var ret = !this.args[0].evaluate(ctx).booleanValue();
- return new BooleanValue(ret);
- },
-
- 'true': function(ctx) {
- assert(this.args.length == 0);
- return new BooleanValue(true);
- },
-
- 'false': function(ctx) {
- assert(this.args.length == 0);
- return new BooleanValue(false);
- },
-
- 'lang': function(ctx) {
- assert(this.args.length == 1);
- var lang = this.args[0].evaluate(ctx).stringValue();
- var xmllang;
- var n = ctx.node;
- while (n && n != n.parentNode /* just in case ... */) {
- xmllang = n.getAttribute('xml:lang');
- if (xmllang) {
- break;
- }
- n = n.parentNode;
- }
- if (!xmllang) {
- return new BooleanValue(false);
- } else {
- var re = new RegExp('^' + lang + '$', 'i');
- return new BooleanValue(xmllang.match(re) ||
- xmllang.replace(/_.*$/,'').match(re));
- }
- },
-
- 'number': function(ctx) {
- assert(this.args.length == 1 || this.args.length == 0);
-
- if (this.args.length == 1) {
- return new NumberValue(this.args[0].evaluate(ctx).numberValue());
- } else {
- return new NumberValue(new NodeSetValue([ ctx.node ]).numberValue());
- }
- },
-
- 'sum': function(ctx) {
- assert(this.args.length == 1);
- var n = this.args[0].evaluate(ctx).nodeSetValue();
- var sum = 0;
- for (var i = 0; i < n.length; ++i) {
- sum += xmlValue(n[i]) - 0;
- }
- return new NumberValue(sum);
- },
-
- 'floor': function(ctx) {
- assert(this.args.length == 1);
- var num = this.args[0].evaluate(ctx).numberValue();
- return new NumberValue(Math.floor(num));
- },
-
- 'ceiling': function(ctx) {
- assert(this.args.length == 1);
- var num = this.args[0].evaluate(ctx).numberValue();
- return new NumberValue(Math.ceil(num));
- },
-
- 'round': function(ctx) {
- assert(this.args.length == 1);
- var num = this.args[0].evaluate(ctx).numberValue();
- return new NumberValue(Math.round(num));
- },
-
- // TODO(mesch): The following functions are custom. There is a
- // standard that defines how to add functions, which should be
- // applied here.
-
- 'ext-join': function(ctx) {
- assert(this.args.length == 2);
- var nodes = this.args[0].evaluate(ctx).nodeSetValue();
- var delim = this.args[1].evaluate(ctx).stringValue();
- var ret = '';
- for (var i = 0; i < nodes.length; ++i) {
- if (ret) {
- ret += delim;
- }
- ret += xmlValue(nodes[i]);
- }
- return new StringValue(ret);
- },
-
- // ext-if() evaluates and returns its second argument, if the
- // boolean value of its first argument is true, otherwise it
- // evaluates and returns its third argument.
-
- 'ext-if': function(ctx) {
- assert(this.args.length == 3);
- if (this.args[0].evaluate(ctx).booleanValue()) {
- return this.args[1].evaluate(ctx);
- } else {
- return this.args[2].evaluate(ctx);
- }
- },
-
- 'ext-sprintf': function(ctx) {
- assert(this.args.length >= 1);
- var args = [];
- for (var i = 0; i < this.args.length; ++i) {
- args.push(this.args[i].evaluate(ctx).stringValue());
- }
- return new StringValue(sprintf.apply(null, args));
- },
-
- // ext-cardinal() evaluates its single argument as a number, and
- // returns the current node that many times. It can be used in the
- // select attribute to iterate over an integer range.
-
- 'ext-cardinal': function(ctx) {
- assert(this.args.length >= 1);
- var c = this.args[0].evaluate(ctx).numberValue();
- var ret = [];
- for (var i = 0; i < c; ++i) {
- ret.push(ctx.node);
- }
- return new NodeSetValue(ret);
- }
-};
-
-function UnionExpr(expr1, expr2) {
- this.expr1 = expr1;
- this.expr2 = expr2;
-}
-
-UnionExpr.prototype.evaluate = function(ctx) {
- var nodes1 = this.expr1.evaluate(ctx).nodeSetValue();
- var nodes2 = this.expr2.evaluate(ctx).nodeSetValue();
- var I1 = nodes1.length;
- for (var i2 = 0; i2 < nodes2.length; ++i2) {
- for (var i1 = 0; i1 < I1; ++i1) {
- if (nodes1[i1] == nodes2[i2]) {
- // break inner loop and continue outer loop, labels confuse
- // the js compiler, so we don't use them here.
- i1 = I1;
- }
- }
- nodes1.push(nodes2[i2]);
- }
- return new NodeSetValue(nodes2);
-};
-
-function PathExpr(filter, rel) {
- this.filter = filter;
- this.rel = rel;
-}
-
-PathExpr.prototype.evaluate = function(ctx) {
- var nodes = this.filter.evaluate(ctx).nodeSetValue();
- var nodes1 = [];
- for (var i = 0; i < nodes.length; ++i) {
- var nodes0 = this.rel.evaluate(ctx.clone(nodes[i], i, nodes)).nodeSetValue();
- for (var ii = 0; ii < nodes0.length; ++ii) {
- nodes1.push(nodes0[ii]);
- }
- }
- return new NodeSetValue(nodes1);
-};
-
-function FilterExpr(expr, predicate) {
- this.expr = expr;
- this.predicate = predicate;
-}
-
-FilterExpr.prototype.evaluate = function(ctx) {
- var nodes = this.expr.evaluate(ctx).nodeSetValue();
- for (var i = 0; i < this.predicate.length; ++i) {
- var nodes0 = nodes;
- nodes = [];
- for (var j = 0; j < nodes0.length; ++j) {
- var n = nodes0[j];
- if (this.predicate[i].evaluate(ctx.clone(n, j, nodes0)).booleanValue()) {
- nodes.push(n);
- }
- }
- }
-
- return new NodeSetValue(nodes);
-}
-
-function UnaryMinusExpr(expr) {
- this.expr = expr;
-}
-
-UnaryMinusExpr.prototype.evaluate = function(ctx) {
- return new NumberValue(-this.expr.evaluate(ctx).numberValue());
-};
-
-function BinaryExpr(expr1, op, expr2) {
- this.expr1 = expr1;
- this.expr2 = expr2;
- this.op = op;
-}
-
-BinaryExpr.prototype.evaluate = function(ctx) {
- var ret;
- switch (this.op.value) {
- case 'or':
- ret = new BooleanValue(this.expr1.evaluate(ctx).booleanValue() ||
- this.expr2.evaluate(ctx).booleanValue());
- break;
-
- case 'and':
- ret = new BooleanValue(this.expr1.evaluate(ctx).booleanValue() &&
- this.expr2.evaluate(ctx).booleanValue());
- break;
-
- case '+':
- ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() +
- this.expr2.evaluate(ctx).numberValue());
- break;
-
- case '-':
- ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() -
- this.expr2.evaluate(ctx).numberValue());
- break;
-
- case '*':
- ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() *
- this.expr2.evaluate(ctx).numberValue());
- break;
-
- case 'mod':
- ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() %
- this.expr2.evaluate(ctx).numberValue());
- break;
-
- case 'div':
- ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() /
- this.expr2.evaluate(ctx).numberValue());
- break;
-
- case '=':
- ret = this.compare(ctx, function(x1, x2) { return x1 == x2; });
- break;
-
- case '!=':
- ret = this.compare(ctx, function(x1, x2) { return x1 != x2; });
- break;
-
- case '<':
- ret = this.compare(ctx, function(x1, x2) { return x1 < x2; });
- break;
-
- case '<=':
- ret = this.compare(ctx, function(x1, x2) { return x1 <= x2; });
- break;
-
- case '>':
- ret = this.compare(ctx, function(x1, x2) { return x1 > x2; });
- break;
-
- case '>=':
- ret = this.compare(ctx, function(x1, x2) { return x1 >= x2; });
- break;
-
- default:
- alert('BinaryExpr.evaluate: ' + this.op.value);
- }
- return ret;
-};
-
-BinaryExpr.prototype.compare = function(ctx, cmp) {
- var v1 = this.expr1.evaluate(ctx);
- var v2 = this.expr2.evaluate(ctx);
-
- var ret;
- if (v1.type == 'node-set' && v2.type == 'node-set') {
- var n1 = v1.nodeSetValue();
- var n2 = v2.nodeSetValue();
- ret = false;
- for (var i1 = 0; i1 < n1.length; ++i1) {
- for (var i2 = 0; i2 < n2.length; ++i2) {
- if (cmp(xmlValue(n1[i1]), xmlValue(n2[i2]))) {
- ret = true;
- // Break outer loop. Labels confuse the jscompiler and we
- // don't use them.
- i2 = n2.length;
- i1 = n1.length;
- }
- }
- }
-
- } else if (v1.type == 'node-set' || v2.type == 'node-set') {
-
- if (v1.type == 'number') {
- var s = v1.numberValue();
- var n = v2.nodeSetValue();
-
- ret = false;
- for (var i = 0; i < n.length; ++i) {
- var nn = xmlValue(n[i]) - 0;
- if (cmp(s, nn)) {
- ret = true;
- break;
- }
- }
-
- } else if (v2.type == 'number') {
- var n = v1.nodeSetValue();
- var s = v2.numberValue();
-
- ret = false;
- for (var i = 0; i < n.length; ++i) {
- var nn = xmlValue(n[i]) - 0;
- if (cmp(nn, s)) {
- ret = true;
- break;
- }
- }
-
- } else if (v1.type == 'string') {
- var s = v1.stringValue();
- var n = v2.nodeSetValue();
-
- ret = false;
- for (var i = 0; i < n.length; ++i) {
- var nn = xmlValue(n[i]);
- if (cmp(s, nn)) {
- ret = true;
- break;
- }
- }
-
- } else if (v2.type == 'string') {
- var n = v1.nodeSetValue();
- var s = v2.stringValue();
-
- ret = false;
- for (var i = 0; i < n.length; ++i) {
- var nn = xmlValue(n[i]);
- if (cmp(nn, s)) {
- ret = true;
- break;
- }
- }
-
- } else {
- ret = cmp(v1.booleanValue(), v2.booleanValue());
- }
-
- } else if (v1.type == 'boolean' || v2.type == 'boolean') {
- ret = cmp(v1.booleanValue(), v2.booleanValue());
-
- } else if (v1.type == 'number' || v2.type == 'number') {
- ret = cmp(v1.numberValue(), v2.numberValue());
-
- } else {
- ret = cmp(v1.stringValue(), v2.stringValue());
- }
-
- return new BooleanValue(ret);
-}
-
-function LiteralExpr(value) {
- this.value = value;
-}
-
-LiteralExpr.prototype.evaluate = function(ctx) {
- return new StringValue(this.value);
-};
-
-function NumberExpr(value) {
- this.value = value;
-}
-
-NumberExpr.prototype.evaluate = function(ctx) {
- return new NumberValue(this.value);
-};
-
-function VariableExpr(name) {
- this.name = name;
-}
-
-VariableExpr.prototype.evaluate = function(ctx) {
- return ctx.getVariable(this.name);
-}
-
-// Factory functions for semantic values (i.e. Expressions) of the
-// productions in the grammar. When a production is matched to reduce
-// the current parse state stack, the function is called with the
-// semantic values of the matched elements as arguments, and returns
-// another semantic value. The semantic value is a node of the parse
-// tree, an expression object with an evaluate() method that evaluates the
-// expression in an actual context. These factory functions are used
-// in the specification of the grammar rules, below.
-
-function makeTokenExpr(m) {
- return new TokenExpr(m);
-}
-
-function passExpr(e) {
- return e;
-}
-
-function makeLocationExpr1(slash, rel) {
- rel.absolute = true;
- return rel;
-}
-
-function makeLocationExpr2(dslash, rel) {
- rel.absolute = true;
- rel.prependStep(makeAbbrevStep(dslash.value));
- return rel;
-}
-
-function makeLocationExpr3(slash) {
- var ret = new LocationExpr();
- ret.appendStep(makeAbbrevStep('.'));
- ret.absolute = true;
- return ret;
-}
-
-function makeLocationExpr4(dslash) {
- var ret = new LocationExpr();
- ret.absolute = true;
- ret.appendStep(makeAbbrevStep(dslash.value));
- return ret;
-}
-
-function makeLocationExpr5(step) {
- var ret = new LocationExpr();
- ret.appendStep(step);
- return ret;
-}
-
-function makeLocationExpr6(rel, slash, step) {
- rel.appendStep(step);
- return rel;
-}
-
-function makeLocationExpr7(rel, dslash, step) {
- rel.appendStep(makeAbbrevStep(dslash.value));
- rel.appendStep(step);
- return rel;
-}
-
-function makeStepExpr1(dot) {
- return makeAbbrevStep(dot.value);
-}
-
-function makeStepExpr2(ddot) {
- return makeAbbrevStep(ddot.value);
-}
-
-function makeStepExpr3(axisname, axis, nodetest) {
- return new StepExpr(axisname.value, nodetest);
-}
-
-function makeStepExpr4(at, nodetest) {
- return new StepExpr('attribute', nodetest);
-}
-
-function makeStepExpr5(nodetest) {
- return new StepExpr('child', nodetest);
-}
-
-function makeStepExpr6(step, predicate) {
- step.appendPredicate(predicate);
- return step;
-}
-
-function makeAbbrevStep(abbrev) {
- switch (abbrev) {
- case '//':
- return new StepExpr('descendant-or-self', new NodeTestAny);
-
- case '.':
- return new StepExpr('self', new NodeTestAny);
-
- case '..':
- return new StepExpr('parent', new NodeTestAny);
- }
-}
-
-function makeNodeTestExpr1(asterisk) {
- return new NodeTestElement;
-}
-
-function makeNodeTestExpr2(ncname, colon, asterisk) {
- return new NodeTestNC(ncname.value);
-}
-
-function makeNodeTestExpr3(qname) {
- return new NodeTestName(qname.value);
-}
-
-function makeNodeTestExpr4(typeo, parenc) {
- var type = typeo.value.replace(/\s*\($/, '');
- switch(type) {
- case 'node':
- return new NodeTestAny;
-
- case 'text':
- return new NodeTestText;
-
- case 'comment':
- return new NodeTestComment;
-
- case 'processing-instruction':
- return new NodeTestPI;
- }
-}
-
-function makeNodeTestExpr5(typeo, target, parenc) {
- var type = typeo.replace(/\s*\($/, '');
- if (type != 'processing-instruction') {
- throw type + ' ' + Error().stack;
- }
- return new NodeTestPI(target.value);
-}
-
-function makePredicateExpr(pareno, expr, parenc) {
- return new PredicateExpr(expr);
-}
-
-function makePrimaryExpr(pareno, expr, parenc) {
- return expr;
-}
-
-function makeFunctionCallExpr1(name, pareno, parenc) {
- return new FunctionCallExpr(name);
-}
-
-function makeFunctionCallExpr2(name, pareno, arg1, args, parenc) {
- var ret = new FunctionCallExpr(name);
- ret.appendArg(arg1);
- for (var i = 0; i < args.length; ++i) {
- ret.appendArg(args[i]);
- }
- return ret;
-}
-
-function makeArgumentExpr(comma, expr) {
- return expr;
-}
-
-function makeUnionExpr(expr1, pipe, expr2) {
- return new UnionExpr(expr1, expr2);
-}
-
-function makePathExpr1(filter, slash, rel) {
- return new PathExpr(filter, rel);
-}
-
-function makePathExpr2(filter, dslash, rel) {
- rel.prependStep(makeAbbrevStep(dslash.value));
- return new PathExpr(filter, rel);
-}
-
-function makeFilterExpr(expr, predicates) {
- if (predicates.length > 0) {
- return new FilterExpr(expr, predicates);
- } else {
- return expr;
- }
-}
-
-function makeUnaryMinusExpr(minus, expr) {
- return new UnaryMinusExpr(expr);
-}
-
-function makeBinaryExpr(expr1, op, expr2) {
- return new BinaryExpr(expr1, op, expr2);
-}
-
-function makeLiteralExpr(token) {
- // remove quotes from the parsed value:
- var value = token.value.substring(1, token.value.length - 1);
- return new LiteralExpr(value);
-}
-
-function makeNumberExpr(token) {
- return new NumberExpr(token.value);
-}
-
-function makeVariableReference(dollar, name) {
- return new VariableExpr(name.value);
-}
-
-// Used before parsing for optimization of common simple cases. See
-// the begin of xpathParse() for which they are.
-function makeSimpleExpr(expr) {
- if (expr.charAt(0) == '$') {
- return new VariableExpr(expr.substr(1));
- } else if (expr.charAt(0) == '@') {
- var a = new NodeTestName(expr.substr(1));
- var b = new StepExpr('attribute', a);
- var c = new LocationExpr();
- c.appendStep(b);
- return c;
- } else if (expr.match(/^[0-9]+$/)) {
- return new NumberExpr(expr);
- } else {
- var a = new NodeTestName(expr);
- var b = new StepExpr('child', a);
- var c = new LocationExpr();
- c.appendStep(b);
- return c;
- }
-}
-
-function makeSimpleExpr2(expr) {
- var steps = expr.split('/');
- var c = new LocationExpr();
- for (var i in steps) {
- var a = new NodeTestName(steps[i]);
- var b = new StepExpr('child', a);
- c.appendStep(b);
- }
- return c;
-}
-
-// The axes of XPath expressions.
-
-var xpathAxis = {
- ANCESTOR_OR_SELF: 'ancestor-or-self',
- ANCESTOR: 'ancestor',
- ATTRIBUTE: 'attribute',
- CHILD: 'child',
- DESCENDANT_OR_SELF: 'descendant-or-self',
- DESCENDANT: 'descendant',
- FOLLOWING_SIBLING: 'following-sibling',
- FOLLOWING: 'following',
- NAMESPACE: 'namespace',
- PARENT: 'parent',
- PRECEDING_SIBLING: 'preceding-sibling',
- PRECEDING: 'preceding',
- SELF: 'self'
-};
-
-var xpathAxesRe = [
- xpathAxis.ANCESTOR_OR_SELF,
- xpathAxis.ANCESTOR,
- xpathAxis.ATTRIBUTE,
- xpathAxis.CHILD,
- xpathAxis.DESCENDANT_OR_SELF,
- xpathAxis.DESCENDANT,
- xpathAxis.FOLLOWING_SIBLING,
- xpathAxis.FOLLOWING,
- xpathAxis.NAMESPACE,
- xpathAxis.PARENT,
- xpathAxis.PRECEDING_SIBLING,
- xpathAxis.PRECEDING,
- xpathAxis.SELF
-].join('|');
-
-
-// The tokens of the language. The label property is just used for
-// generating debug output. The prec property is the precedence used
-// for shift/reduce resolution. Default precedence is 0 as a lookahead
-// token and 2 on the stack. TODO(mesch): this is certainly not
-// necessary and too complicated. Simplify this!
-
-// NOTE: tabular formatting is the big exception, but here it should
-// be OK.
-
-var TOK_PIPE = { label: "|", prec: 17, re: new RegExp("^\\|") };
-var TOK_DSLASH = { label: "//", prec: 19, re: new RegExp("^//") };
-var TOK_SLASH = { label: "/", prec: 30, re: new RegExp("^/") };
-var TOK_AXIS = { label: "::", prec: 20, re: new RegExp("^::") };
-var TOK_COLON = { label: ":", prec: 1000, re: new RegExp("^:") };
-var TOK_AXISNAME = { label: "[axis]", re: new RegExp('^(' + xpathAxesRe + ')') };
-var TOK_PARENO = { label: "(", prec: 34, re: new RegExp("^\\(") };
-var TOK_PARENC = { label: ")", re: new RegExp("^\\)") };
-var TOK_DDOT = { label: "..", prec: 34, re: new RegExp("^\\.\\.") };
-var TOK_DOT = { label: ".", prec: 34, re: new RegExp("^\\.") };
-var TOK_AT = { label: "@", prec: 34, re: new RegExp("^@") };
-
-var TOK_COMMA = { label: ",", re: new RegExp("^,") };
-
-var TOK_OR = { label: "or", prec: 10, re: new RegExp("^or\\b") };
-var TOK_AND = { label: "and", prec: 11, re: new RegExp("^and\\b") };
-var TOK_EQ = { label: "=", prec: 12, re: new RegExp("^=") };
-var TOK_NEQ = { label: "!=", prec: 12, re: new RegExp("^!=") };
-var TOK_GE = { label: ">=", prec: 13, re: new RegExp("^>=") };
-var TOK_GT = { label: ">", prec: 13, re: new RegExp("^>") };
-var TOK_LE = { label: "<=", prec: 13, re: new RegExp("^<=") };
-var TOK_LT = { label: "<", prec: 13, re: new RegExp("^<") };
-var TOK_PLUS = { label: "+", prec: 14, re: new RegExp("^\\+"), left: true };
-var TOK_MINUS = { label: "-", prec: 14, re: new RegExp("^\\-"), left: true };
-var TOK_DIV = { label: "div", prec: 15, re: new RegExp("^div\\b"), left: true };
-var TOK_MOD = { label: "mod", prec: 15, re: new RegExp("^mod\\b"), left: true };
-
-var TOK_BRACKO = { label: "[", prec: 32, re: new RegExp("^\\[") };
-var TOK_BRACKC = { label: "]", re: new RegExp("^\\]") };
-var TOK_DOLLAR = { label: "$", re: new RegExp("^\\$") };
-
-var TOK_NCNAME = { label: "[ncname]", re: new RegExp('^[a-z][-\\w]*','i') };
-
-var TOK_ASTERISK = { label: "*", prec: 15, re: new RegExp("^\\*"), left: true };
-var TOK_LITERALQ = { label: "[litq]", prec: 20, re: new RegExp("^'[^\\']*'") };
-var TOK_LITERALQQ = {
- label: "[litqq]",
- prec: 20,
- re: new RegExp('^"[^\\"]*"')
-};
-
-var TOK_NUMBER = {
- label: "[number]",
- prec: 35,
- re: new RegExp('^\\d+(\\.\\d*)?') };
-
-var TOK_QNAME = {
- label: "[qname]",
- re: new RegExp('^([a-z][-\\w]*:)?[a-z][-\\w]*','i')
-};
-
-var TOK_NODEO = {
- label: "[nodetest-start]",
- re: new RegExp('^(processing-instruction|comment|text|node)\\(')
-};
-
-// The table of the tokens of our grammar, used by the lexer: first
-// column the tag, second column a regexp to recognize it in the
-// input, third column the precedence of the token, fourth column a
-// factory function for the semantic value of the token.
-//
-// NOTE: order of this list is important, because the first match
-// counts. Cf. DDOT and DOT, and AXIS and COLON.
-
-var xpathTokenRules = [
- TOK_DSLASH,
- TOK_SLASH,
- TOK_DDOT,
- TOK_DOT,
- TOK_AXIS,
- TOK_COLON,
- TOK_AXISNAME,
- TOK_NODEO,
- TOK_PARENO,
- TOK_PARENC,
- TOK_BRACKO,
- TOK_BRACKC,
- TOK_AT,
- TOK_COMMA,
- TOK_OR,
- TOK_AND,
- TOK_NEQ,
- TOK_EQ,
- TOK_GE,
- TOK_GT,
- TOK_LE,
- TOK_LT,
- TOK_PLUS,
- TOK_MINUS,
- TOK_ASTERISK,
- TOK_PIPE,
- TOK_MOD,
- TOK_DIV,
- TOK_LITERALQ,
- TOK_LITERALQQ,
- TOK_NUMBER,
- TOK_QNAME,
- TOK_NCNAME,
- TOK_DOLLAR
-];
-
-// All the nonterminals of the grammar. The nonterminal objects are
-// identified by object identity; the labels are used in the debug
-// output only.
-var XPathLocationPath = { label: "LocationPath" };
-var XPathRelativeLocationPath = { label: "RelativeLocationPath" };
-var XPathAbsoluteLocationPath = { label: "AbsoluteLocationPath" };
-var XPathStep = { label: "Step" };
-var XPathNodeTest = { label: "NodeTest" };
-var XPathPredicate = { label: "Predicate" };
-var XPathLiteral = { label: "Literal" };
-var XPathExpr = { label: "Expr" };
-var XPathPrimaryExpr = { label: "PrimaryExpr" };
-var XPathVariableReference = { label: "Variablereference" };
-var XPathNumber = { label: "Number" };
-var XPathFunctionCall = { label: "FunctionCall" };
-var XPathArgumentRemainder = { label: "ArgumentRemainder" };
-var XPathPathExpr = { label: "PathExpr" };
-var XPathUnionExpr = { label: "UnionExpr" };
-var XPathFilterExpr = { label: "FilterExpr" };
-var XPathDigits = { label: "Digits" };
-
-var xpathNonTerminals = [
- XPathLocationPath,
- XPathRelativeLocationPath,
- XPathAbsoluteLocationPath,
- XPathStep,
- XPathNodeTest,
- XPathPredicate,
- XPathLiteral,
- XPathExpr,
- XPathPrimaryExpr,
- XPathVariableReference,
- XPathNumber,
- XPathFunctionCall,
- XPathArgumentRemainder,
- XPathPathExpr,
- XPathUnionExpr,
- XPathFilterExpr,
- XPathDigits
-];
-
-// Quantifiers that are used in the productions of the grammar.
-var Q_01 = { label: "?" };
-var Q_MM = { label: "*" };
-var Q_1M = { label: "+" };
-
-// Tag for left associativity (right assoc is implied by undefined).
-var ASSOC_LEFT = true;
-
-// The productions of the grammar. Columns of the table:
-//
-// - target nonterminal,
-// - pattern,
-// - precedence,
-// - semantic value factory
-//
-// The semantic value factory is a function that receives parse tree
-// nodes from the stack frames of the matched symbols as arguments and
-// returns an a node of the parse tree. The node is stored in the top
-// stack frame along with the target object of the rule. The node in
-// the parse tree is an expression object that has an evaluate() method
-// and thus evaluates XPath expressions.
-//
-// The precedence is used to decide between reducing and shifting by
-// comparing the precendence of the rule that is candidate for
-// reducing with the precedence of the look ahead token. Precedence of
-// -1 means that the precedence of the tokens in the pattern is used
-// instead. TODO: It shouldn't be necessary to explicitly assign
-// precedences to rules.
-
-// DGF Where do these precedence rules come from? I just tweaked some
-// of these numbers to fix bug SEL-486.
-
-var xpathGrammarRules =
- [
- [ XPathLocationPath, [ XPathRelativeLocationPath ], 18,
- passExpr ],
- [ XPathLocationPath, [ XPathAbsoluteLocationPath ], 18,
- passExpr ],
-
- [ XPathAbsoluteLocationPath, [ TOK_SLASH, XPathRelativeLocationPath ], 18,
- makeLocationExpr1 ],
- [ XPathAbsoluteLocationPath, [ TOK_DSLASH, XPathRelativeLocationPath ], 18,
- makeLocationExpr2 ],
-
- [ XPathAbsoluteLocationPath, [ TOK_SLASH ], 0,
- makeLocationExpr3 ],
- [ XPathAbsoluteLocationPath, [ TOK_DSLASH ], 0,
- makeLocationExpr4 ],
-
- [ XPathRelativeLocationPath, [ XPathStep ], 31,
- makeLocationExpr5 ],
- [ XPathRelativeLocationPath,
- [ XPathRelativeLocationPath, TOK_SLASH, XPathStep ], 31,
- makeLocationExpr6 ],
- [ XPathRelativeLocationPath,
- [ XPathRelativeLocationPath, TOK_DSLASH, XPathStep ], 31,
- makeLocationExpr7 ],
-
- [ XPathStep, [ TOK_DOT ], 33,
- makeStepExpr1 ],
- [ XPathStep, [ TOK_DDOT ], 33,
- makeStepExpr2 ],
- [ XPathStep,
- [ TOK_AXISNAME, TOK_AXIS, XPathNodeTest ], 33,
- makeStepExpr3 ],
- [ XPathStep, [ TOK_AT, XPathNodeTest ], 33,
- makeStepExpr4 ],
- [ XPathStep, [ XPathNodeTest ], 33,
- makeStepExpr5 ],
- [ XPathStep, [ XPathStep, XPathPredicate ], 33,
- makeStepExpr6 ],
-
- [ XPathNodeTest, [ TOK_ASTERISK ], 33,
- makeNodeTestExpr1 ],
- [ XPathNodeTest, [ TOK_NCNAME, TOK_COLON, TOK_ASTERISK ], 33,
- makeNodeTestExpr2 ],
- [ XPathNodeTest, [ TOK_QNAME ], 33,
- makeNodeTestExpr3 ],
- [ XPathNodeTest, [ TOK_NODEO, TOK_PARENC ], 33,
- makeNodeTestExpr4 ],
- [ XPathNodeTest, [ TOK_NODEO, XPathLiteral, TOK_PARENC ], 33,
- makeNodeTestExpr5 ],
-
- [ XPathPredicate, [ TOK_BRACKO, XPathExpr, TOK_BRACKC ], 33,
- makePredicateExpr ],
-
- [ XPathPrimaryExpr, [ XPathVariableReference ], 33,
- passExpr ],
- [ XPathPrimaryExpr, [ TOK_PARENO, XPathExpr, TOK_PARENC ], 33,
- makePrimaryExpr ],
- [ XPathPrimaryExpr, [ XPathLiteral ], 30,
- passExpr ],
- [ XPathPrimaryExpr, [ XPathNumber ], 30,
- passExpr ],
- [ XPathPrimaryExpr, [ XPathFunctionCall ], 31,
- passExpr ],
-
- [ XPathFunctionCall, [ TOK_QNAME, TOK_PARENO, TOK_PARENC ], -1,
- makeFunctionCallExpr1 ],
- [ XPathFunctionCall,
- [ TOK_QNAME, TOK_PARENO, XPathExpr, XPathArgumentRemainder, Q_MM,
- TOK_PARENC ], -1,
- makeFunctionCallExpr2 ],
- [ XPathArgumentRemainder, [ TOK_COMMA, XPathExpr ], -1,
- makeArgumentExpr ],
-
- [ XPathUnionExpr, [ XPathPathExpr ], 20,
- passExpr ],
- [ XPathUnionExpr, [ XPathUnionExpr, TOK_PIPE, XPathPathExpr ], 20,
- makeUnionExpr ],
-
- [ XPathPathExpr, [ XPathLocationPath ], 20,
- passExpr ],
- [ XPathPathExpr, [ XPathFilterExpr ], 19,
- passExpr ],
- [ XPathPathExpr,
- [ XPathFilterExpr, TOK_SLASH, XPathRelativeLocationPath ], 19,
- makePathExpr1 ],
- [ XPathPathExpr,
- [ XPathFilterExpr, TOK_DSLASH, XPathRelativeLocationPath ], 19,
- makePathExpr2 ],
-
- [ XPathFilterExpr, [ XPathPrimaryExpr, XPathPredicate, Q_MM ], 31,
- makeFilterExpr ],
-
- [ XPathExpr, [ XPathPrimaryExpr ], 16,
- passExpr ],
- [ XPathExpr, [ XPathUnionExpr ], 16,
- passExpr ],
-
- [ XPathExpr, [ TOK_MINUS, XPathExpr ], -1,
- makeUnaryMinusExpr ],
-
- [ XPathExpr, [ XPathExpr, TOK_OR, XPathExpr ], -1,
- makeBinaryExpr ],
- [ XPathExpr, [ XPathExpr, TOK_AND, XPathExpr ], -1,
- makeBinaryExpr ],
-
- [ XPathExpr, [ XPathExpr, TOK_EQ, XPathExpr ], -1,
- makeBinaryExpr ],
- [ XPathExpr, [ XPathExpr, TOK_NEQ, XPathExpr ], -1,
- makeBinaryExpr ],
-
- [ XPathExpr, [ XPathExpr, TOK_LT, XPathExpr ], -1,
- makeBinaryExpr ],
- [ XPathExpr, [ XPathExpr, TOK_LE, XPathExpr ], -1,
- makeBinaryExpr ],
- [ XPathExpr, [ XPathExpr, TOK_GT, XPathExpr ], -1,
- makeBinaryExpr ],
- [ XPathExpr, [ XPathExpr, TOK_GE, XPathExpr ], -1,
- makeBinaryExpr ],
-
- [ XPathExpr, [ XPathExpr, TOK_PLUS, XPathExpr ], -1,
- makeBinaryExpr, ASSOC_LEFT ],
- [ XPathExpr, [ XPathExpr, TOK_MINUS, XPathExpr ], -1,
- makeBinaryExpr, ASSOC_LEFT ],
-
- [ XPathExpr, [ XPathExpr, TOK_ASTERISK, XPathExpr ], -1,
- makeBinaryExpr, ASSOC_LEFT ],
- [ XPathExpr, [ XPathExpr, TOK_DIV, XPathExpr ], -1,
- makeBinaryExpr, ASSOC_LEFT ],
- [ XPathExpr, [ XPathExpr, TOK_MOD, XPathExpr ], -1,
- makeBinaryExpr, ASSOC_LEFT ],
-
- [ XPathLiteral, [ TOK_LITERALQ ], -1,
- makeLiteralExpr ],
- [ XPathLiteral, [ TOK_LITERALQQ ], -1,
- makeLiteralExpr ],
-
- [ XPathNumber, [ TOK_NUMBER ], -1,
- makeNumberExpr ],
-
- [ XPathVariableReference, [ TOK_DOLLAR, TOK_QNAME ], 200,
- makeVariableReference ]
- ];
-
-// That function computes some optimizations of the above data
-// structures and will be called right here. It merely takes the
-// counter variables out of the global scope.
-
-var xpathRules = [];
-
-function xpathParseInit() {
- if (xpathRules.length) {
- return;
- }
-
- // Some simple optimizations for the xpath expression parser: sort
- // grammar rules descending by length, so that the longest match is
- // first found.
-
- xpathGrammarRules.sort(function(a,b) {
- var la = a[1].length;
- var lb = b[1].length;
- if (la < lb) {
- return 1;
- } else if (la > lb) {
- return -1;
- } else {
- return 0;
- }
- });
-
- var k = 1;
- for (var i = 0; i < xpathNonTerminals.length; ++i) {
- xpathNonTerminals[i].key = k++;
- }
-
- for (i = 0; i < xpathTokenRules.length; ++i) {
- xpathTokenRules[i].key = k++;
- }
-
- Log.write('XPath parse INIT: ' + k + ' rules');
-
- // Another slight optimization: sort the rules into bins according
- // to the last element (observing quantifiers), so we can restrict
- // the match against the stack to the subest of rules that match the
- // top of the stack.
- //
- // TODO(mesch): What we actually want is to compute states as in
- // bison, so that we don't have to do any explicit and iterated
- // match against the stack.
-
- function push_(array, position, element) {
- if (!array[position]) {
- array[position] = [];
- }
- array[position].push(element);
- }
-
- for (i = 0; i < xpathGrammarRules.length; ++i) {
- var rule = xpathGrammarRules[i];
- var pattern = rule[1];
-
- for (var j = pattern.length - 1; j >= 0; --j) {
- if (pattern[j] == Q_1M) {
- push_(xpathRules, pattern[j-1].key, rule);
- break;
-
- } else if (pattern[j] == Q_MM || pattern[j] == Q_01) {
- push_(xpathRules, pattern[j-1].key, rule);
- --j;
-
- } else {
- push_(xpathRules, pattern[j].key, rule);
- break;
- }
- }
- }
-
- Log.write('XPath parse INIT: ' + xpathRules.length + ' rule bins');
-
- var sum = 0;
- mapExec(xpathRules, function(i) {
- if (i) {
- sum += i.length;
- }
- });
-
- Log.write('XPath parse INIT: ' + (sum / xpathRules.length) + ' average bin size');
-}
-
-// Local utility functions that are used by the lexer or parser.
-
-function xpathCollectDescendants(nodelist, node) {
- for (var n = node.firstChild; n; n = n.nextSibling) {
- nodelist.push(n);
- arguments.callee(nodelist, n);
- }
-}
-
-function xpathCollectDescendantsReverse(nodelist, node) {
- for (var n = node.lastChild; n; n = n.previousSibling) {
- nodelist.push(n);
- arguments.callee(nodelist, n);
- }
-}
-
-
-// The entry point for the library: match an expression against a DOM
-// node. Returns an XPath value.
-function xpathDomEval(expr, node) {
- var expr1 = xpathParse(expr);
- var ret = expr1.evaluate(new ExprContext(node));
- return ret;
-}
-
-// Utility function to sort a list of nodes. Used by xsltSort() and
-// nxslSelect().
-function xpathSort(input, sort) {
- if (sort.length == 0) {
- return;
- }
-
- var sortlist = [];
-
- for (var i = 0; i < input.nodelist.length; ++i) {
- var node = input.nodelist[i];
- var sortitem = { node: node, key: [] };
- var context = input.clone(node, 0, [ node ]);
-
- for (var j = 0; j < sort.length; ++j) {
- var s = sort[j];
- var value = s.expr.evaluate(context);
-
- var evalue;
- if (s.type == 'text') {
- evalue = value.stringValue();
- } else if (s.type == 'number') {
- evalue = value.numberValue();
- }
- sortitem.key.push({ value: evalue, order: s.order });
- }
-
- // Make the sort stable by adding a lowest priority sort by
- // id. This is very convenient and furthermore required by the
- // spec ([XSLT] - Section 10 Sorting).
- sortitem.key.push({ value: i, order: 'ascending' });
-
- sortlist.push(sortitem);
- }
-
- sortlist.sort(xpathSortByKey);
-
- var nodes = [];
- for (var i = 0; i < sortlist.length; ++i) {
- nodes.push(sortlist[i].node);
- }
- input.nodelist = nodes;
- input.setNode(nodes[0], 0);
-}
-
-
-// Sorts by all order criteria defined. According to the JavaScript
-// spec ([ECMA] Section 11.8.5), the compare operators compare strings
-// as strings and numbers as numbers.
-//
-// NOTE: In browsers which do not follow the spec, this breaks only in
-// the case that numbers should be sorted as strings, which is very
-// uncommon.
-
-function xpathSortByKey(v1, v2) {
- // NOTE: Sort key vectors of different length never occur in
- // xsltSort.
-
- for (var i = 0; i < v1.key.length; ++i) {
- var o = v1.key[i].order == 'descending' ? -1 : 1;
- if (v1.key[i].value > v2.key[i].value) {
- return +1 * o;
- } else if (v1.key[i].value < v2.key[i].value) {
- return -1 * o;
- }
- }
-
- return 0;
-}
+// Copyright 2005 Google Inc. +// All Rights Reserved +// +// An XPath parser and evaluator written in JavaScript. The +// implementation is complete except for functions handling +// namespaces. +// +// Reference: [XPATH] XPath Specification +// <http://www.w3.org/TR/1999/REC-xpath-19991116>. +// +// +// The API of the parser has several parts: +// +// 1. The parser function xpathParse() that takes a string and returns +// an expession object. +// +// 2. The expression object that has an evaluate() method to evaluate the +// XPath expression it represents. (It is actually a hierarchy of +// objects that resembles the parse tree, but an application will call +// evaluate() only on the top node of this hierarchy.) +// +// 3. The context object that is passed as an argument to the evaluate() +// method, which represents the DOM context in which the expression is +// evaluated. +// +// 4. The value object that is returned from evaluate() and represents +// values of the different types that are defined by XPath (number, +// string, boolean, and node-set), and allows to convert between them. +// +// These parts are near the top of the file, the functions and data +// that are used internally follow after them. +// +// +// Author: Steffen Meschkat <[email protected]> + + +// The entry point for the parser. +// +// @param expr a string that contains an XPath expression. +// @return an expression object that can be evaluated with an +// expression context. + +function xpathParse(expr) { + xpathLog('parse ' + expr); + xpathParseInit(); + + var cached = xpathCacheLookup(expr); + if (cached) { + xpathLog(' ... cached'); + return cached; + } + + // Optimize for a few common cases: simple attribute node tests + // (@id), simple element node tests (page), variable references + // ($address), numbers (4), multi-step path expressions where each + // step is a plain element node test + // (page/overlay/locations/location). + + if (expr.match(/^(\$|@)?\w+$/i)) { + var ret = makeSimpleExpr(expr); + xpathParseCache[expr] = ret; + xpathLog(' ... simple'); + return ret; + } + + if (expr.match(/^\w+(\/\w+)*$/i)) { + var ret = makeSimpleExpr2(expr); + xpathParseCache[expr] = ret; + xpathLog(' ... simple 2'); + return ret; + } + + var cachekey = expr; // expr is modified during parse + + var stack = []; + var ahead = null; + var previous = null; + var done = false; + + var parse_count = 0; + var lexer_count = 0; + var reduce_count = 0; + + while (!done) { + parse_count++; + expr = expr.replace(/^\s*/, ''); + previous = ahead; + ahead = null; + + var rule = null; + var match = ''; + for (var i = 0; i < xpathTokenRules.length; ++i) { + var result = xpathTokenRules[i].re.exec(expr); + lexer_count++; + if (result && result.length > 0 && result[0].length > match.length) { + rule = xpathTokenRules[i]; + match = result[0]; + break; + } + } + + // Special case: allow operator keywords to be element and + // variable names. + + // NOTE(mesch): The parser resolves conflicts by looking ahead, + // and this is the only case where we look back to + // disambiguate. So this is indeed something different, and + // looking back is usually done in the lexer (via states in the + // general case, called "start conditions" in flex(1)). Also,the + // conflict resolution in the parser is not as robust as it could + // be, so I'd like to keep as much off the parser as possible (all + // these precedence values should be computed from the grammar + // rules and possibly associativity declarations, as in bison(1), + // and not explicitly set. + + if (rule && + (rule == TOK_DIV || + rule == TOK_MOD || + rule == TOK_AND || + rule == TOK_OR) && + (!previous || + previous.tag == TOK_AT || + previous.tag == TOK_DSLASH || + previous.tag == TOK_SLASH || + previous.tag == TOK_AXIS || + previous.tag == TOK_DOLLAR)) { + rule = TOK_QNAME; + } + + if (rule) { + expr = expr.substr(match.length); + xpathLog('token: ' + match + ' -- ' + rule.label); + ahead = { + tag: rule, + match: match, + prec: rule.prec ? rule.prec : 0, // || 0 is removed by the compiler + expr: makeTokenExpr(match) + }; + + } else { + xpathLog('DONE'); + done = true; + } + + while (xpathReduce(stack, ahead)) { + reduce_count++; + xpathLog('stack: ' + stackToString(stack)); + } + } + + xpathLog('stack: ' + stackToString(stack)); + + // DGF any valid XPath should "reduce" to a single Expr token + if (stack.length != 1) { + throw 'XPath parse error ' + cachekey + ':\n' + stackToString(stack); + } + + var result = stack[0].expr; + xpathParseCache[cachekey] = result; + + xpathLog('XPath parse: ' + parse_count + ' / ' + + lexer_count + ' / ' + reduce_count); + + return result; +} + +var xpathParseCache = {}; + +function xpathCacheLookup(expr) { + return xpathParseCache[expr]; +} + +/*DGF xpathReduce is where the magic happens in this parser. +Skim down to the bottom of this file to find the table of +grammatical rules and precedence numbers, "The productions of the grammar". + +The idea here +is that we want to take a stack of tokens and apply +grammatical rules to them, "reducing" them to higher-level +tokens. Ultimately, any valid XPath should reduce to exactly one +"Expr" token. + +Reduce too early or too late and you'll have two tokens that can't reduce +to single Expr. For example, you may hastily reduce a qname that +should name a function, incorrectly treating it as a tag name. +Or you may reduce too late, accidentally reducing the last part of the +XPath into a top-level "Expr" that won't reduce with earlier parts of +the XPath. + +A "cand" is a grammatical rule candidate, with a given precedence +number. "ahead" is the upcoming token, which also has a precedence +number. If the token has a higher precedence number than +the rule candidate, we'll "shift" the token onto the token stack, +instead of immediately applying the rule candidate. + +Some tokens have left associativity, in which case we shift when they +have LOWER precedence than the candidate. +*/ +function xpathReduce(stack, ahead) { + var cand = null; + + if (stack.length > 0) { + var top = stack[stack.length-1]; + var ruleset = xpathRules[top.tag.key]; + + if (ruleset) { + for (var i = 0; i < ruleset.length; ++i) { + var rule = ruleset[i]; + var match = xpathMatchStack(stack, rule[1]); + if (match.length) { + cand = { + tag: rule[0], + rule: rule, + match: match + }; + cand.prec = xpathGrammarPrecedence(cand); + break; + } + } + } + } + + var ret; + if (cand && (!ahead || cand.prec > ahead.prec || + (ahead.tag.left && cand.prec >= ahead.prec))) { + for (var i = 0; i < cand.match.matchlength; ++i) { + stack.pop(); + } + + xpathLog('reduce ' + cand.tag.label + ' ' + cand.prec + + ' ahead ' + (ahead ? ahead.tag.label + ' ' + ahead.prec + + (ahead.tag.left ? ' left' : '') + : ' none ')); + + var matchexpr = mapExpr(cand.match, function(m) { return m.expr; }); + xpathLog('going to apply ' + cand.rule[3].toString()); + cand.expr = cand.rule[3].apply(null, matchexpr); + + stack.push(cand); + ret = true; + + } else { + if (ahead) { + xpathLog('shift ' + ahead.tag.label + ' ' + ahead.prec + + (ahead.tag.left ? ' left' : '') + + ' over ' + (cand ? cand.tag.label + ' ' + + cand.prec : ' none')); + stack.push(ahead); + } + ret = false; + } + return ret; +} + +function xpathMatchStack(stack, pattern) { + + // NOTE(mesch): The stack matches for variable cardinality are + // greedy but don't do backtracking. This would be an issue only + // with rules of the form A* A, i.e. with an element with variable + // cardinality followed by the same element. Since that doesn't + // occur in the grammar at hand, all matches on the stack are + // unambiguous. + + var S = stack.length; + var P = pattern.length; + var p, s; + var match = []; + match.matchlength = 0; + var ds = 0; + for (p = P - 1, s = S - 1; p >= 0 && s >= 0; --p, s -= ds) { + ds = 0; + var qmatch = []; + if (pattern[p] == Q_MM) { + p -= 1; + match.push(qmatch); + while (s - ds >= 0 && stack[s - ds].tag == pattern[p]) { + qmatch.push(stack[s - ds]); + ds += 1; + match.matchlength += 1; + } + + } else if (pattern[p] == Q_01) { + p -= 1; + match.push(qmatch); + while (s - ds >= 0 && ds < 2 && stack[s - ds].tag == pattern[p]) { + qmatch.push(stack[s - ds]); + ds += 1; + match.matchlength += 1; + } + + } else if (pattern[p] == Q_1M) { + p -= 1; + match.push(qmatch); + if (stack[s].tag == pattern[p]) { + while (s - ds >= 0 && stack[s - ds].tag == pattern[p]) { + qmatch.push(stack[s - ds]); + ds += 1; + match.matchlength += 1; + } + } else { + return []; + } + + } else if (stack[s].tag == pattern[p]) { + match.push(stack[s]); + ds += 1; + match.matchlength += 1; + + } else { + return []; + } + + reverseInplace(qmatch); + qmatch.expr = mapExpr(qmatch, function(m) { return m.expr; }); + } + + reverseInplace(match); + + if (p == -1) { + return match; + + } else { + return []; + } +} + +function xpathTokenPrecedence(tag) { + return tag.prec || 2; +} + +function xpathGrammarPrecedence(frame) { + var ret = 0; + + if (frame.rule) { /* normal reduce */ + if (frame.rule.length >= 3 && frame.rule[2] >= 0) { + ret = frame.rule[2]; + + } else { + for (var i = 0; i < frame.rule[1].length; ++i) { + var p = xpathTokenPrecedence(frame.rule[1][i]); + ret = Math.max(ret, p); + } + } + } else if (frame.tag) { /* TOKEN match */ + ret = xpathTokenPrecedence(frame.tag); + + } else if (frame.length) { /* Q_ match */ + for (var j = 0; j < frame.length; ++j) { + var p = xpathGrammarPrecedence(frame[j]); + ret = Math.max(ret, p); + } + } + + return ret; +} + +function stackToString(stack) { + var ret = ''; + for (var i = 0; i < stack.length; ++i) { + if (ret) { + ret += '\n'; + } + ret += stack[i].tag.label; + } + return ret; +} + + +// XPath expression evaluation context. An XPath context consists of a +// DOM node, a list of DOM nodes that contains this node, a number +// that represents the position of the single node in the list, and a +// current set of variable bindings. (See XPath spec.) +// +// The interface of the expression context: +// +// Constructor -- gets the node, its position, the node set it +// belongs to, and a parent context as arguments. The parent context +// is used to implement scoping rules for variables: if a variable +// is not found in the current context, it is looked for in the +// parent context, recursively. Except for node, all arguments have +// default values: default position is 0, default node set is the +// set that contains only the node, and the default parent is null. +// +// Notice that position starts at 0 at the outside interface; +// inside XPath expressions this shows up as position()=1. +// +// clone() -- creates a new context with the current context as +// parent. If passed as argument to clone(), the new context has a +// different node, position, or node set. What is not passed is +// inherited from the cloned context. +// +// setVariable(name, expr) -- binds given XPath expression to the +// name. +// +// getVariable(name) -- what the name says. +// +// setNode(position) -- sets the context to the node at the given +// position. Needed to implement scoping rules for variables in +// XPath. (A variable is visible to all subsequent siblings, not +// only to its children.) +// +// set/isCaseInsensitive -- specifies whether node name tests should +// be case sensitive. If you're executing xpaths against a regular +// HTML DOM, you probably don't want case-sensitivity, because +// browsers tend to disagree about whether elements & attributes +// should be upper/lower case. If you're running xpaths in an +// XSLT instance, you probably DO want case sensitivity, as per the +// XSL spec. +// +// set/isReturnOnFirstMatch -- whether XPath evaluation should quit as soon +// as a result is found. This is an optimization that might make sense if you +// only care about the first result. +// +// set/isIgnoreNonElementNodesForNTA -- whether to ignore non-element nodes +// when evaluating the "node()" any node test. While technically this is +// contrary to the XPath spec, practically it can enhance performance +// significantly, and makes sense if you a) use "node()" when you mean "*", +// and b) use "//" when you mean "/descendant::*/". + +function ExprContext(node, opt_position, opt_nodelist, opt_parent, + opt_caseInsensitive, opt_ignoreAttributesWithoutValue, + opt_returnOnFirstMatch, opt_ignoreNonElementNodesForNTA) +{ + this.node = node; + this.position = opt_position || 0; + this.nodelist = opt_nodelist || [ node ]; + this.variables = {}; + this.parent = opt_parent || null; + this.caseInsensitive = opt_caseInsensitive || false; + this.ignoreAttributesWithoutValue = opt_ignoreAttributesWithoutValue || false; + this.returnOnFirstMatch = opt_returnOnFirstMatch || false; + this.ignoreNonElementNodesForNTA = opt_ignoreNonElementNodesForNTA || false; + if (opt_parent) { + this.root = opt_parent.root; + } else if (this.node.nodeType == DOM_DOCUMENT_NODE) { + // NOTE(mesch): DOM Spec stipulates that the ownerDocument of a + // document is null. Our root, however is the document that we are + // processing, so the initial context is created from its document + // node, which case we must handle here explcitly. + this.root = node; + } else { + this.root = node.ownerDocument; + } +} + +ExprContext.prototype.clone = function(opt_node, opt_position, opt_nodelist) { + return new ExprContext( + opt_node || this.node, + typeof opt_position != 'undefined' ? opt_position : this.position, + opt_nodelist || this.nodelist, this, this.caseInsensitive, + this.ignoreAttributesWithoutValue, this.returnOnFirstMatch, + this.ignoreNonElementNodesForNTA); +}; + +ExprContext.prototype.setVariable = function(name, value) { + if (value instanceof StringValue || value instanceof BooleanValue || + value instanceof NumberValue || value instanceof NodeSetValue) { + this.variables[name] = value; + return; + } + if ('true' === value) { + this.variables[name] = new BooleanValue(true); + } else if ('false' === value) { + this.variables[name] = new BooleanValue(false); + } else if (TOK_NUMBER.re.test(value)) { + this.variables[name] = new NumberValue(value); + } else { + // DGF What if it's null? + this.variables[name] = new StringValue(value); + } +}; + +ExprContext.prototype.getVariable = function(name) { + if (typeof this.variables[name] != 'undefined') { + return this.variables[name]; + + } else if (this.parent) { + return this.parent.getVariable(name); + + } else { + return null; + } +}; + +ExprContext.prototype.setNode = function(position) { + this.node = this.nodelist[position]; + this.position = position; +}; + +ExprContext.prototype.contextSize = function() { + return this.nodelist.length; +}; + +ExprContext.prototype.isCaseInsensitive = function() { + return this.caseInsensitive; +}; + +ExprContext.prototype.setCaseInsensitive = function(caseInsensitive) { + return this.caseInsensitive = caseInsensitive; +}; + +ExprContext.prototype.isIgnoreAttributesWithoutValue = function() { + return this.ignoreAttributesWithoutValue; +}; + +ExprContext.prototype.setIgnoreAttributesWithoutValue = function(ignore) { + return this.ignoreAttributesWithoutValue = ignore; +}; + +ExprContext.prototype.isReturnOnFirstMatch = function() { + return this.returnOnFirstMatch; +}; + +ExprContext.prototype.setReturnOnFirstMatch = function(returnOnFirstMatch) { + return this.returnOnFirstMatch = returnOnFirstMatch; +}; + +ExprContext.prototype.isIgnoreNonElementNodesForNTA = function() { + return this.ignoreNonElementNodesForNTA; +}; + +ExprContext.prototype.setIgnoreNonElementNodesForNTA = function(ignoreNonElementNodesForNTA) { + return this.ignoreNonElementNodesForNTA = ignoreNonElementNodesForNTA; +}; + +// XPath expression values. They are what XPath expressions evaluate +// to. Strangely, the different value types are not specified in the +// XPath syntax, but only in the semantics, so they don't show up as +// nonterminals in the grammar. Yet, some expressions are required to +// evaluate to particular types, and not every type can be coerced +// into every other type. Although the types of XPath values are +// similar to the types present in JavaScript, the type coercion rules +// are a bit peculiar, so we explicitly model XPath types instead of +// mapping them onto JavaScript types. (See XPath spec.) +// +// The four types are: +// +// StringValue +// +// NumberValue +// +// BooleanValue +// +// NodeSetValue +// +// The common interface of the value classes consists of methods that +// implement the XPath type coercion rules: +// +// stringValue() -- returns the value as a JavaScript String, +// +// numberValue() -- returns the value as a JavaScript Number, +// +// booleanValue() -- returns the value as a JavaScript Boolean, +// +// nodeSetValue() -- returns the value as a JavaScript Array of DOM +// Node objects. +// + +function StringValue(value) { + this.value = value; + this.type = 'string'; +} + +StringValue.prototype.stringValue = function() { + return this.value; +} + +StringValue.prototype.booleanValue = function() { + return this.value.length > 0; +} + +StringValue.prototype.numberValue = function() { + return this.value - 0; +} + +StringValue.prototype.nodeSetValue = function() { + throw this; +} + +function BooleanValue(value) { + this.value = value; + this.type = 'boolean'; +} + +BooleanValue.prototype.stringValue = function() { + return '' + this.value; +} + +BooleanValue.prototype.booleanValue = function() { + return this.value; +} + +BooleanValue.prototype.numberValue = function() { + return this.value ? 1 : 0; +} + +BooleanValue.prototype.nodeSetValue = function() { + throw this; +} + +function NumberValue(value) { + this.value = value; + this.type = 'number'; +} + +NumberValue.prototype.stringValue = function() { + return '' + this.value; +} + +NumberValue.prototype.booleanValue = function() { + return !!this.value; +} + +NumberValue.prototype.numberValue = function() { + return this.value - 0; +} + +NumberValue.prototype.nodeSetValue = function() { + throw this; +} + +function NodeSetValue(value) { + this.value = value; + this.type = 'node-set'; +} + +NodeSetValue.prototype.stringValue = function() { + if (this.value.length == 0) { + return ''; + } else { + return xmlValue(this.value[0]); + } +} + +NodeSetValue.prototype.booleanValue = function() { + return this.value.length > 0; +} + +NodeSetValue.prototype.numberValue = function() { + return this.stringValue() - 0; +} + +NodeSetValue.prototype.nodeSetValue = function() { + return this.value; +}; + +// XPath expressions. They are used as nodes in the parse tree and +// possess an evaluate() method to compute an XPath value given an XPath +// context. Expressions are returned from the parser. Teh set of +// expression classes closely mirrors the set of non terminal symbols +// in the grammar. Every non trivial nonterminal symbol has a +// corresponding expression class. +// +// The common expression interface consists of the following methods: +// +// evaluate(context) -- evaluates the expression, returns a value. +// +// toString() -- returns the XPath text representation of the +// expression (defined in xsltdebug.js). +// +// parseTree(indent) -- returns a parse tree representation of the +// expression (defined in xsltdebug.js). + +function TokenExpr(m) { + this.value = m; +} + +TokenExpr.prototype.evaluate = function() { + return new StringValue(this.value); +}; + +function LocationExpr() { + this.absolute = false; + this.steps = []; +} + +LocationExpr.prototype.appendStep = function(s) { + var combinedStep = this._combineSteps(this.steps[this.steps.length-1], s); + if (combinedStep) { + this.steps[this.steps.length-1] = combinedStep; + } else { + this.steps.push(s); + } +} + +LocationExpr.prototype.prependStep = function(s) { + var combinedStep = this._combineSteps(s, this.steps[0]); + if (combinedStep) { + this.steps[0] = combinedStep; + } else { + this.steps.unshift(s); + } +}; + +// DGF try to combine two steps into one step (perf enhancement) +LocationExpr.prototype._combineSteps = function(prevStep, nextStep) { + if (!prevStep) return null; + if (!nextStep) return null; + var hasPredicates = (prevStep.predicates && prevStep.predicates.length > 0); + if (prevStep.nodetest instanceof NodeTestAny && !hasPredicates) { + // maybe suitable to be combined + if (prevStep.axis == xpathAxis.DESCENDANT_OR_SELF) { + if (nextStep.axis == xpathAxis.CHILD) { + // HBC - commenting out, because this is not a valid reduction + //nextStep.axis = xpathAxis.DESCENDANT; + //return nextStep; + } else if (nextStep.axis == xpathAxis.SELF) { + nextStep.axis = xpathAxis.DESCENDANT_OR_SELF; + return nextStep; + } + } else if (prevStep.axis == xpathAxis.DESCENDANT) { + if (nextStep.axis == xpathAxis.SELF) { + nextStep.axis = xpathAxis.DESCENDANT; + return nextStep; + } + } + } + return null; +} + +LocationExpr.prototype.evaluate = function(ctx) { + var start; + if (this.absolute) { + start = ctx.root; + + } else { + start = ctx.node; + } + + var nodes = []; + xPathStep(nodes, this.steps, 0, start, ctx); + return new NodeSetValue(nodes); +}; + +function xPathStep(nodes, steps, step, input, ctx) { + var s = steps[step]; + var ctx2 = ctx.clone(input); + + if (ctx.returnOnFirstMatch && !s.hasPositionalPredicate) { + var nodelist = s.evaluate(ctx2).nodeSetValue(); + // the predicates were not processed in the last evaluate(), so that we can + // process them here with the returnOnFirstMatch optimization. We do a + // depth-first grab at any nodes that pass the predicate tests. There is no + // way to optimize when predicates contain positional selectors, including + // indexes or uses of the last() or position() functions, because they + // typically require the entire nodelist for context. Process without + // optimization if we encounter such selectors. + var nLength = nodelist.length; + var pLength = s.predicate.length; + nodelistLoop: + for (var i = 0; i < nLength; ++i) { + var n = nodelist[i]; + for (var j = 0; j < pLength; ++j) { + if (!s.predicate[j].evaluate(ctx.clone(n, i, nodelist)).booleanValue()) { + continue nodelistLoop; + } + } + // n survived the predicate tests! + if (step == steps.length - 1) { + nodes.push(n); + } + else { + xPathStep(nodes, steps, step + 1, n, ctx); + } + if (nodes.length > 0) { + break; + } + } + } + else { + // set returnOnFirstMatch to false for the cloned ExprContext, because + // behavior in StepExpr.prototype.evaluate is driven off its value. Note + // that the original context may still have true for this value. + ctx2.returnOnFirstMatch = false; + var nodelist = s.evaluate(ctx2).nodeSetValue(); + for (var i = 0; i < nodelist.length; ++i) { + if (step == steps.length - 1) { + nodes.push(nodelist[i]); + } else { + xPathStep(nodes, steps, step + 1, nodelist[i], ctx); + } + } + } +} + +function StepExpr(axis, nodetest, opt_predicate) { + this.axis = axis; + this.nodetest = nodetest; + this.predicate = opt_predicate || []; + this.hasPositionalPredicate = false; + for (var i = 0; i < this.predicate.length; ++i) { + if (predicateExprHasPositionalSelector(this.predicate[i].expr)) { + this.hasPositionalPredicate = true; + break; + } + } +} + +StepExpr.prototype.appendPredicate = function(p) { + this.predicate.push(p); + if (!this.hasPositionalPredicate) { + this.hasPositionalPredicate = predicateExprHasPositionalSelector(p.expr); + } +} + +StepExpr.prototype.evaluate = function(ctx) { + var input = ctx.node; + var nodelist = []; + var skipNodeTest = false; + + if (this.nodetest instanceof NodeTestAny) { + skipNodeTest = true; + } + + // NOTE(mesch): When this was a switch() statement, it didn't work + // in Safari/2.0. Not sure why though; it resulted in the JavaScript + // console output "undefined" (without any line number or so). + + if (this.axis == xpathAxis.ANCESTOR_OR_SELF) { + nodelist.push(input); + for (var n = input.parentNode; n; n = n.parentNode) { + nodelist.push(n); + } + + } else if (this.axis == xpathAxis.ANCESTOR) { + for (var n = input.parentNode; n; n = n.parentNode) { + nodelist.push(n); + } + + } else if (this.axis == xpathAxis.ATTRIBUTE) { + if (this.nodetest.name != undefined) { + // single-attribute step + if (input.attributes) { + if (input.attributes instanceof Array) { + // probably evaluating on document created by xmlParse() + copyArray(nodelist, input.attributes); + } + else { + if (this.nodetest.name == 'style') { + var value = input.getAttribute('style'); + if (value && typeof(value) != 'string') { + // this is the case where indexing into the attributes array + // doesn't give us the attribute node in IE - we create our own + // node instead + nodelist.push(XNode.create(DOM_ATTRIBUTE_NODE, 'style', + value.cssText, document)); + } + else { + nodelist.push(input.attributes[this.nodetest.name]); + } + } + else { + nodelist.push(input.attributes[this.nodetest.name]); + } + } + } + } + else { + // all-attributes step + if (ctx.ignoreAttributesWithoutValue) { + copyArrayIgnoringAttributesWithoutValue(nodelist, input.attributes); + } + else { + copyArray(nodelist, input.attributes); + } + } + + } else if (this.axis == xpathAxis.CHILD) { + copyArray(nodelist, input.childNodes); + + } else if (this.axis == xpathAxis.DESCENDANT_OR_SELF) { + if (this.nodetest.evaluate(ctx).booleanValue()) { + nodelist.push(input); + } + var tagName = xpathExtractTagNameFromNodeTest(this.nodetest, ctx.ignoreNonElementNodesForNTA); + xpathCollectDescendants(nodelist, input, tagName); + if (tagName) skipNodeTest = true; + + } else if (this.axis == xpathAxis.DESCENDANT) { + var tagName = xpathExtractTagNameFromNodeTest(this.nodetest, ctx.ignoreNonElementNodesForNTA); + xpathCollectDescendants(nodelist, input, tagName); + if (tagName) skipNodeTest = true; + + } else if (this.axis == xpathAxis.FOLLOWING) { + for (var n = input; n; n = n.parentNode) { + for (var nn = n.nextSibling; nn; nn = nn.nextSibling) { + nodelist.push(nn); + xpathCollectDescendants(nodelist, nn); + } + } + + } else if (this.axis == xpathAxis.FOLLOWING_SIBLING) { + for (var n = input.nextSibling; n; n = n.nextSibling) { + nodelist.push(n); + } + + } else if (this.axis == xpathAxis.NAMESPACE) { + alert('not implemented: axis namespace'); + + } else if (this.axis == xpathAxis.PARENT) { + if (input.parentNode) { + nodelist.push(input.parentNode); + } + + } else if (this.axis == xpathAxis.PRECEDING) { + for (var n = input; n; n = n.parentNode) { + for (var nn = n.previousSibling; nn; nn = nn.previousSibling) { + nodelist.push(nn); + xpathCollectDescendantsReverse(nodelist, nn); + } + } + + } else if (this.axis == xpathAxis.PRECEDING_SIBLING) { + for (var n = input.previousSibling; n; n = n.previousSibling) { + nodelist.push(n); + } + + } else if (this.axis == xpathAxis.SELF) { + nodelist.push(input); + + } else { + throw 'ERROR -- NO SUCH AXIS: ' + this.axis; + } + + if (!skipNodeTest) { + // process node test + var nodelist0 = nodelist; + nodelist = []; + for (var i = 0; i < nodelist0.length; ++i) { + var n = nodelist0[i]; + if (this.nodetest.evaluate(ctx.clone(n, i, nodelist0)).booleanValue()) { + nodelist.push(n); + } + } + } + + // process predicates + if (!ctx.returnOnFirstMatch) { + for (var i = 0; i < this.predicate.length; ++i) { + var nodelist0 = nodelist; + nodelist = []; + for (var ii = 0; ii < nodelist0.length; ++ii) { + var n = nodelist0[ii]; + if (this.predicate[i].evaluate(ctx.clone(n, ii, nodelist0)).booleanValue()) { + nodelist.push(n); + } + } + } + } + + return new NodeSetValue(nodelist); +}; + +function NodeTestAny() { + this.value = new BooleanValue(true); +} + +NodeTestAny.prototype.evaluate = function(ctx) { + return this.value; +}; + +function NodeTestElementOrAttribute() {} + +NodeTestElementOrAttribute.prototype.evaluate = function(ctx) { + return new BooleanValue( + ctx.node.nodeType == DOM_ELEMENT_NODE || + ctx.node.nodeType == DOM_ATTRIBUTE_NODE); +} + +function NodeTestText() {} + +NodeTestText.prototype.evaluate = function(ctx) { + return new BooleanValue(ctx.node.nodeType == DOM_TEXT_NODE); +} + +function NodeTestComment() {} + +NodeTestComment.prototype.evaluate = function(ctx) { + return new BooleanValue(ctx.node.nodeType == DOM_COMMENT_NODE); +} + +function NodeTestPI(target) { + this.target = target; +} + +NodeTestPI.prototype.evaluate = function(ctx) { + return new + BooleanValue(ctx.node.nodeType == DOM_PROCESSING_INSTRUCTION_NODE && + (!this.target || ctx.node.nodeName == this.target)); +} + +function NodeTestNC(nsprefix) { + this.regex = new RegExp("^" + nsprefix + ":"); + this.nsprefix = nsprefix; +} + +NodeTestNC.prototype.evaluate = function(ctx) { + var n = ctx.node; + return new BooleanValue(this.regex.match(n.nodeName)); +} + +function NodeTestName(name) { + this.name = name; + this.re = new RegExp('^' + name + '$', "i"); +} + +NodeTestName.prototype.evaluate = function(ctx) { + var n = ctx.node; + if (ctx.caseInsensitive) { + if (n.nodeName.length != this.name.length) return new BooleanValue(false); + return new BooleanValue(this.re.test(n.nodeName)); + } else { + return new BooleanValue(n.nodeName == this.name); + } +} + +function PredicateExpr(expr) { + this.expr = expr; +} + +PredicateExpr.prototype.evaluate = function(ctx) { + var v = this.expr.evaluate(ctx); + if (v.type == 'number') { + // NOTE(mesch): Internally, position is represented starting with + // 0, however in XPath position starts with 1. See functions + // position() and last(). + return new BooleanValue(ctx.position == v.numberValue() - 1); + } else { + return new BooleanValue(v.booleanValue()); + } +}; + +function FunctionCallExpr(name) { + this.name = name; + this.args = []; +} + +FunctionCallExpr.prototype.appendArg = function(arg) { + this.args.push(arg); +}; + +FunctionCallExpr.prototype.evaluate = function(ctx) { + var fn = '' + this.name.value; + var f = this.xpathfunctions[fn]; + if (f) { + return f.call(this, ctx); + } else { + xpathLog('XPath NO SUCH FUNCTION ' + fn); + return new BooleanValue(false); + } +}; + +FunctionCallExpr.prototype.xpathfunctions = { + 'last': function(ctx) { + assert(this.args.length == 0); + // NOTE(mesch): XPath position starts at 1. + return new NumberValue(ctx.contextSize()); + }, + + 'position': function(ctx) { + assert(this.args.length == 0); + // NOTE(mesch): XPath position starts at 1. + return new NumberValue(ctx.position + 1); + }, + + 'count': function(ctx) { + assert(this.args.length == 1); + var v = this.args[0].evaluate(ctx); + return new NumberValue(v.nodeSetValue().length); + }, + + 'id': function(ctx) { + assert(this.args.length == 1); + var e = this.args[0].evaluate(ctx); + var ret = []; + var ids; + if (e.type == 'node-set') { + ids = []; + var en = e.nodeSetValue(); + for (var i = 0; i < en.length; ++i) { + var v = xmlValue(en[i]).split(/\s+/); + for (var ii = 0; ii < v.length; ++ii) { + ids.push(v[ii]); + } + } + } else { + ids = e.stringValue().split(/\s+/); + } + var d = ctx.root; + for (var i = 0; i < ids.length; ++i) { + var n = d.getElementById(ids[i]); + if (n) { + ret.push(n); + } + } + return new NodeSetValue(ret); + }, + + 'local-name': function(ctx) { + alert('not implmented yet: XPath function local-name()'); + }, + + 'namespace-uri': function(ctx) { + alert('not implmented yet: XPath function namespace-uri()'); + }, + + 'name': function(ctx) { + assert(this.args.length == 1 || this.args.length == 0); + var n; + if (this.args.length == 0) { + n = [ ctx.node ]; + } else { + n = this.args[0].evaluate(ctx).nodeSetValue(); + } + + if (n.length == 0) { + return new StringValue(''); + } else { + return new StringValue(n[0].nodeName); + } + }, + + 'string': function(ctx) { + assert(this.args.length == 1 || this.args.length == 0); + if (this.args.length == 0) { + return new StringValue(new NodeSetValue([ ctx.node ]).stringValue()); + } else { + return new StringValue(this.args[0].evaluate(ctx).stringValue()); + } + }, + + 'concat': function(ctx) { + var ret = ''; + for (var i = 0; i < this.args.length; ++i) { + ret += this.args[i].evaluate(ctx).stringValue(); + } + return new StringValue(ret); + }, + + 'starts-with': function(ctx) { + assert(this.args.length == 2); + var s0 = this.args[0].evaluate(ctx).stringValue(); + var s1 = this.args[1].evaluate(ctx).stringValue(); + return new BooleanValue(s0.indexOf(s1) == 0); + }, + + 'ends-with': function(ctx) { + assert(this.args.length == 2); + var s0 = this.args[0].evaluate(ctx).stringValue(); + var s1 = this.args[1].evaluate(ctx).stringValue(); + var re = new RegExp(RegExp.escape(s1) + '$'); + return new BooleanValue(re.test(s0)); + }, + + 'contains': function(ctx) { + assert(this.args.length == 2); + var s0 = this.args[0].evaluate(ctx).stringValue(); + var s1 = this.args[1].evaluate(ctx).stringValue(); + return new BooleanValue(s0.indexOf(s1) != -1); + }, + + 'substring-before': function(ctx) { + assert(this.args.length == 2); + var s0 = this.args[0].evaluate(ctx).stringValue(); + var s1 = this.args[1].evaluate(ctx).stringValue(); + var i = s0.indexOf(s1); + var ret; + if (i == -1) { + ret = ''; + } else { + ret = s0.substr(0,i); + } + return new StringValue(ret); + }, + + 'substring-after': function(ctx) { + assert(this.args.length == 2); + var s0 = this.args[0].evaluate(ctx).stringValue(); + var s1 = this.args[1].evaluate(ctx).stringValue(); + var i = s0.indexOf(s1); + var ret; + if (i == -1) { + ret = ''; + } else { + ret = s0.substr(i + s1.length); + } + return new StringValue(ret); + }, + + 'substring': function(ctx) { + // NOTE: XPath defines the position of the first character in a + // string to be 1, in JavaScript this is 0 ([XPATH] Section 4.2). + assert(this.args.length == 2 || this.args.length == 3); + var s0 = this.args[0].evaluate(ctx).stringValue(); + var s1 = this.args[1].evaluate(ctx).numberValue(); + var ret; + if (this.args.length == 2) { + var i1 = Math.max(0, Math.round(s1) - 1); + ret = s0.substr(i1); + + } else { + var s2 = this.args[2].evaluate(ctx).numberValue(); + var i0 = Math.round(s1) - 1; + var i1 = Math.max(0, i0); + var i2 = Math.round(s2) - Math.max(0, -i0); + ret = s0.substr(i1, i2); + } + return new StringValue(ret); + }, + + 'string-length': function(ctx) { + var s; + if (this.args.length > 0) { + s = this.args[0].evaluate(ctx).stringValue(); + } else { + s = new NodeSetValue([ ctx.node ]).stringValue(); + } + return new NumberValue(s.length); + }, + + 'normalize-space': function(ctx) { + var s; + if (this.args.length > 0) { + s = this.args[0].evaluate(ctx).stringValue(); + } else { + s = new NodeSetValue([ ctx.node ]).stringValue(); + } + s = s.replace(/^\s*/,'').replace(/\s*$/,'').replace(/\s+/g, ' '); + return new StringValue(s); + }, + + 'translate': function(ctx) { + assert(this.args.length == 3); + var s0 = this.args[0].evaluate(ctx).stringValue(); + var s1 = this.args[1].evaluate(ctx).stringValue(); + var s2 = this.args[2].evaluate(ctx).stringValue(); + + for (var i = 0; i < s1.length; ++i) { + s0 = s0.replace(new RegExp(s1.charAt(i), 'g'), s2.charAt(i)); + } + return new StringValue(s0); + }, + + 'matches': function(ctx) { + assert(this.args.length >= 2); + var s0 = this.args[0].evaluate(ctx).stringValue(); + var s1 = this.args[1].evaluate(ctx).stringValue(); + if (this.args.length > 2) { + var s2 = this.args[2].evaluate(ctx).stringValue(); + if (/[^mi]/.test(s2)) { + throw 'Invalid regular expression syntax: ' + s2; + } + } + + try { + var re = new RegExp(s1, s2); + } + catch (e) { + throw 'Invalid matches argument: ' + s1; + } + return new BooleanValue(re.test(s0)); + }, + + 'boolean': function(ctx) { + assert(this.args.length == 1); + return new BooleanValue(this.args[0].evaluate(ctx).booleanValue()); + }, + + 'not': function(ctx) { + assert(this.args.length == 1); + var ret = !this.args[0].evaluate(ctx).booleanValue(); + return new BooleanValue(ret); + }, + + 'true': function(ctx) { + assert(this.args.length == 0); + return new BooleanValue(true); + }, + + 'false': function(ctx) { + assert(this.args.length == 0); + return new BooleanValue(false); + }, + + 'lang': function(ctx) { + assert(this.args.length == 1); + var lang = this.args[0].evaluate(ctx).stringValue(); + var xmllang; + var n = ctx.node; + while (n && n != n.parentNode /* just in case ... */) { + xmllang = n.getAttribute('xml:lang'); + if (xmllang) { + break; + } + n = n.parentNode; + } + if (!xmllang) { + return new BooleanValue(false); + } else { + var re = new RegExp('^' + lang + '$', 'i'); + return new BooleanValue(xmllang.match(re) || + xmllang.replace(/_.*$/,'').match(re)); + } + }, + + 'number': function(ctx) { + assert(this.args.length == 1 || this.args.length == 0); + + if (this.args.length == 1) { + return new NumberValue(this.args[0].evaluate(ctx).numberValue()); + } else { + return new NumberValue(new NodeSetValue([ ctx.node ]).numberValue()); + } + }, + + 'sum': function(ctx) { + assert(this.args.length == 1); + var n = this.args[0].evaluate(ctx).nodeSetValue(); + var sum = 0; + for (var i = 0; i < n.length; ++i) { + sum += xmlValue(n[i]) - 0; + } + return new NumberValue(sum); + }, + + 'floor': function(ctx) { + assert(this.args.length == 1); + var num = this.args[0].evaluate(ctx).numberValue(); + return new NumberValue(Math.floor(num)); + }, + + 'ceiling': function(ctx) { + assert(this.args.length == 1); + var num = this.args[0].evaluate(ctx).numberValue(); + return new NumberValue(Math.ceil(num)); + }, + + 'round': function(ctx) { + assert(this.args.length == 1); + var num = this.args[0].evaluate(ctx).numberValue(); + return new NumberValue(Math.round(num)); + }, + + // TODO(mesch): The following functions are custom. There is a + // standard that defines how to add functions, which should be + // applied here. + + 'ext-join': function(ctx) { + assert(this.args.length == 2); + var nodes = this.args[0].evaluate(ctx).nodeSetValue(); + var delim = this.args[1].evaluate(ctx).stringValue(); + var ret = ''; + for (var i = 0; i < nodes.length; ++i) { + if (ret) { + ret += delim; + } + ret += xmlValue(nodes[i]); + } + return new StringValue(ret); + }, + + // ext-if() evaluates and returns its second argument, if the + // boolean value of its first argument is true, otherwise it + // evaluates and returns its third argument. + + 'ext-if': function(ctx) { + assert(this.args.length == 3); + if (this.args[0].evaluate(ctx).booleanValue()) { + return this.args[1].evaluate(ctx); + } else { + return this.args[2].evaluate(ctx); + } + }, + + // ext-cardinal() evaluates its single argument as a number, and + // returns the current node that many times. It can be used in the + // select attribute to iterate over an integer range. + + 'ext-cardinal': function(ctx) { + assert(this.args.length >= 1); + var c = this.args[0].evaluate(ctx).numberValue(); + var ret = []; + for (var i = 0; i < c; ++i) { + ret.push(ctx.node); + } + return new NodeSetValue(ret); + } +}; + +function UnionExpr(expr1, expr2) { + this.expr1 = expr1; + this.expr2 = expr2; +} + +UnionExpr.prototype.evaluate = function(ctx) { + var nodes1 = this.expr1.evaluate(ctx).nodeSetValue(); + var nodes2 = this.expr2.evaluate(ctx).nodeSetValue(); + var I1 = nodes1.length; + for (var i2 = 0; i2 < nodes2.length; ++i2) { + var n = nodes2[i2]; + var inBoth = false; + for (var i1 = 0; i1 < I1; ++i1) { + if (nodes1[i1] == n) { + inBoth = true; + i1 = I1; // break inner loop + } + } + if (!inBoth) { + nodes1.push(n); + } + } + return new NodeSetValue(nodes1); +}; + +function PathExpr(filter, rel) { + this.filter = filter; + this.rel = rel; +} + +PathExpr.prototype.evaluate = function(ctx) { + // the filter expression should be evaluated in its entirety with no + // optimization, as we can't backtrack to it after having moved on to + // evaluating the relative location path + var flag = ctx.returnOnFirstMatch; + ctx.setReturnOnFirstMatch(false); + var nodes = this.filter.evaluate(ctx).nodeSetValue(); + ctx.setReturnOnFirstMatch(flag); + + var nodes1 = []; + if (ctx.returnOnFirstMatch) { + for (var i = 0; i < nodes.length; ++i) { + nodes1 = this.rel.evaluate(ctx.clone(nodes[i], i, nodes)).nodeSetValue(); + if (nodes1.length > 0) { + break; + } + } + return new NodeSetValue(nodes1); + } + else { + for (var i = 0; i < nodes.length; ++i) { + var nodes0 = this.rel.evaluate(ctx.clone(nodes[i], i, nodes)).nodeSetValue(); + for (var ii = 0; ii < nodes0.length; ++ii) { + nodes1.push(nodes0[ii]); + } + } + return new NodeSetValue(nodes1); + } +}; + +function FilterExpr(expr, predicate) { + this.expr = expr; + this.predicate = predicate; +} + +FilterExpr.prototype.evaluate = function(ctx) { + var flag = ctx.returnOnFirstMatch; + ctx.setReturnOnFirstMatch(false); + var nodes = this.expr.evaluate(ctx).nodeSetValue(); + ctx.setReturnOnFirstMatch(flag); + + for (var i = 0; i < this.predicate.length; ++i) { + var nodes0 = nodes; + nodes = []; + for (var j = 0; j < nodes0.length; ++j) { + var n = nodes0[j]; + if (this.predicate[i].evaluate(ctx.clone(n, j, nodes0)).booleanValue()) { + nodes.push(n); + } + } + } + + return new NodeSetValue(nodes); +} + +function UnaryMinusExpr(expr) { + this.expr = expr; +} + +UnaryMinusExpr.prototype.evaluate = function(ctx) { + return new NumberValue(-this.expr.evaluate(ctx).numberValue()); +}; + +function BinaryExpr(expr1, op, expr2) { + this.expr1 = expr1; + this.expr2 = expr2; + this.op = op; +} + +BinaryExpr.prototype.evaluate = function(ctx) { + var ret; + switch (this.op.value) { + case 'or': + ret = new BooleanValue(this.expr1.evaluate(ctx).booleanValue() || + this.expr2.evaluate(ctx).booleanValue()); + break; + + case 'and': + ret = new BooleanValue(this.expr1.evaluate(ctx).booleanValue() && + this.expr2.evaluate(ctx).booleanValue()); + break; + + case '+': + ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() + + this.expr2.evaluate(ctx).numberValue()); + break; + + case '-': + ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() - + this.expr2.evaluate(ctx).numberValue()); + break; + + case '*': + ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() * + this.expr2.evaluate(ctx).numberValue()); + break; + + case 'mod': + ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() % + this.expr2.evaluate(ctx).numberValue()); + break; + + case 'div': + ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() / + this.expr2.evaluate(ctx).numberValue()); + break; + + case '=': + ret = this.compare(ctx, function(x1, x2) { return x1 == x2; }); + break; + + case '!=': + ret = this.compare(ctx, function(x1, x2) { return x1 != x2; }); + break; + + case '<': + ret = this.compare(ctx, function(x1, x2) { return x1 < x2; }); + break; + + case '<=': + ret = this.compare(ctx, function(x1, x2) { return x1 <= x2; }); + break; + + case '>': + ret = this.compare(ctx, function(x1, x2) { return x1 > x2; }); + break; + + case '>=': + ret = this.compare(ctx, function(x1, x2) { return x1 >= x2; }); + break; + + default: + alert('BinaryExpr.evaluate: ' + this.op.value); + } + return ret; +}; + +BinaryExpr.prototype.compare = function(ctx, cmp) { + var v1 = this.expr1.evaluate(ctx); + var v2 = this.expr2.evaluate(ctx); + + var ret; + if (v1.type == 'node-set' && v2.type == 'node-set') { + var n1 = v1.nodeSetValue(); + var n2 = v2.nodeSetValue(); + ret = false; + for (var i1 = 0; i1 < n1.length; ++i1) { + for (var i2 = 0; i2 < n2.length; ++i2) { + if (cmp(xmlValue(n1[i1]), xmlValue(n2[i2]))) { + ret = true; + // Break outer loop. Labels confuse the jscompiler and we + // don't use them. + i2 = n2.length; + i1 = n1.length; + } + } + } + + } else if (v1.type == 'node-set' || v2.type == 'node-set') { + + if (v1.type == 'number') { + var s = v1.numberValue(); + var n = v2.nodeSetValue(); + + ret = false; + for (var i = 0; i < n.length; ++i) { + var nn = xmlValue(n[i]) - 0; + if (cmp(s, nn)) { + ret = true; + break; + } + } + + } else if (v2.type == 'number') { + var n = v1.nodeSetValue(); + var s = v2.numberValue(); + + ret = false; + for (var i = 0; i < n.length; ++i) { + var nn = xmlValue(n[i]) - 0; + if (cmp(nn, s)) { + ret = true; + break; + } + } + + } else if (v1.type == 'string') { + var s = v1.stringValue(); + var n = v2.nodeSetValue(); + + ret = false; + for (var i = 0; i < n.length; ++i) { + var nn = xmlValue(n[i]); + if (cmp(s, nn)) { + ret = true; + break; + } + } + + } else if (v2.type == 'string') { + var n = v1.nodeSetValue(); + var s = v2.stringValue(); + + ret = false; + for (var i = 0; i < n.length; ++i) { + var nn = xmlValue(n[i]); + if (cmp(nn, s)) { + ret = true; + break; + } + } + + } else { + ret = cmp(v1.booleanValue(), v2.booleanValue()); + } + + } else if (v1.type == 'boolean' || v2.type == 'boolean') { + ret = cmp(v1.booleanValue(), v2.booleanValue()); + + } else if (v1.type == 'number' || v2.type == 'number') { + ret = cmp(v1.numberValue(), v2.numberValue()); + + } else { + ret = cmp(v1.stringValue(), v2.stringValue()); + } + + return new BooleanValue(ret); +} + +function LiteralExpr(value) { + this.value = value; +} + +LiteralExpr.prototype.evaluate = function(ctx) { + return new StringValue(this.value); +}; + +function NumberExpr(value) { + this.value = value; +} + +NumberExpr.prototype.evaluate = function(ctx) { + return new NumberValue(this.value); +}; + +function VariableExpr(name) { + this.name = name; +} + +VariableExpr.prototype.evaluate = function(ctx) { + return ctx.getVariable(this.name); +} + +// Factory functions for semantic values (i.e. Expressions) of the +// productions in the grammar. When a production is matched to reduce +// the current parse state stack, the function is called with the +// semantic values of the matched elements as arguments, and returns +// another semantic value. The semantic value is a node of the parse +// tree, an expression object with an evaluate() method that evaluates the +// expression in an actual context. These factory functions are used +// in the specification of the grammar rules, below. + +function makeTokenExpr(m) { + return new TokenExpr(m); +} + +function passExpr(e) { + return e; +} + +function makeLocationExpr1(slash, rel) { + rel.absolute = true; + return rel; +} + +function makeLocationExpr2(dslash, rel) { + rel.absolute = true; + rel.prependStep(makeAbbrevStep(dslash.value)); + return rel; +} + +function makeLocationExpr3(slash) { + var ret = new LocationExpr(); + ret.appendStep(makeAbbrevStep('.')); + ret.absolute = true; + return ret; +} + +function makeLocationExpr4(dslash) { + var ret = new LocationExpr(); + ret.absolute = true; + ret.appendStep(makeAbbrevStep(dslash.value)); + return ret; +} + +function makeLocationExpr5(step) { + var ret = new LocationExpr(); + ret.appendStep(step); + return ret; +} + +function makeLocationExpr6(rel, slash, step) { + rel.appendStep(step); + return rel; +} + +function makeLocationExpr7(rel, dslash, step) { + rel.appendStep(makeAbbrevStep(dslash.value)); + rel.appendStep(step); + return rel; +} + +function makeStepExpr1(dot) { + return makeAbbrevStep(dot.value); +} + +function makeStepExpr2(ddot) { + return makeAbbrevStep(ddot.value); +} + +function makeStepExpr3(axisname, axis, nodetest) { + return new StepExpr(axisname.value, nodetest); +} + +function makeStepExpr4(at, nodetest) { + return new StepExpr('attribute', nodetest); +} + +function makeStepExpr5(nodetest) { + return new StepExpr('child', nodetest); +} + +function makeStepExpr6(step, predicate) { + step.appendPredicate(predicate); + return step; +} + +function makeAbbrevStep(abbrev) { + switch (abbrev) { + case '//': + return new StepExpr('descendant-or-self', new NodeTestAny); + + case '.': + return new StepExpr('self', new NodeTestAny); + + case '..': + return new StepExpr('parent', new NodeTestAny); + } +} + +function makeNodeTestExpr1(asterisk) { + return new NodeTestElementOrAttribute; +} + +function makeNodeTestExpr2(ncname, colon, asterisk) { + return new NodeTestNC(ncname.value); +} + +function makeNodeTestExpr3(qname) { + return new NodeTestName(qname.value); +} + +function makeNodeTestExpr4(typeo, parenc) { + var type = typeo.value.replace(/\s*\($/, ''); + switch(type) { + case 'node': + return new NodeTestAny; + + case 'text': + return new NodeTestText; + + case 'comment': + return new NodeTestComment; + + case 'processing-instruction': + return new NodeTestPI(''); + } +} + +function makeNodeTestExpr5(typeo, target, parenc) { + var type = typeo.replace(/\s*\($/, ''); + if (type != 'processing-instruction') { + throw type; + } + return new NodeTestPI(target.value); +} + +function makePredicateExpr(pareno, expr, parenc) { + return new PredicateExpr(expr); +} + +function makePrimaryExpr(pareno, expr, parenc) { + return expr; +} + +function makeFunctionCallExpr1(name, pareno, parenc) { + return new FunctionCallExpr(name); +} + +function makeFunctionCallExpr2(name, pareno, arg1, args, parenc) { + var ret = new FunctionCallExpr(name); + ret.appendArg(arg1); + for (var i = 0; i < args.length; ++i) { + ret.appendArg(args[i]); + } + return ret; +} + +function makeArgumentExpr(comma, expr) { + return expr; +} + +function makeUnionExpr(expr1, pipe, expr2) { + return new UnionExpr(expr1, expr2); +} + +function makePathExpr1(filter, slash, rel) { + return new PathExpr(filter, rel); +} + +function makePathExpr2(filter, dslash, rel) { + rel.prependStep(makeAbbrevStep(dslash.value)); + return new PathExpr(filter, rel); +} + +function makeFilterExpr(expr, predicates) { + if (predicates.length > 0) { + return new FilterExpr(expr, predicates); + } else { + return expr; + } +} + +function makeUnaryMinusExpr(minus, expr) { + return new UnaryMinusExpr(expr); +} + +function makeBinaryExpr(expr1, op, expr2) { + return new BinaryExpr(expr1, op, expr2); +} + +function makeLiteralExpr(token) { + // remove quotes from the parsed value: + var value = token.value.substring(1, token.value.length - 1); + return new LiteralExpr(value); +} + +function makeNumberExpr(token) { + return new NumberExpr(token.value); +} + +function makeVariableReference(dollar, name) { + return new VariableExpr(name.value); +} + +// Used before parsing for optimization of common simple cases. See +// the begin of xpathParse() for which they are. +function makeSimpleExpr(expr) { + if (expr.charAt(0) == '$') { + return new VariableExpr(expr.substr(1)); + } else if (expr.charAt(0) == '@') { + var a = new NodeTestName(expr.substr(1)); + var b = new StepExpr('attribute', a); + var c = new LocationExpr(); + c.appendStep(b); + return c; + } else if (expr.match(/^[0-9]+$/)) { + return new NumberExpr(expr); + } else { + var a = new NodeTestName(expr); + var b = new StepExpr('child', a); + var c = new LocationExpr(); + c.appendStep(b); + return c; + } +} + +function makeSimpleExpr2(expr) { + var steps = stringSplit(expr, '/'); + var c = new LocationExpr(); + for (var i = 0; i < steps.length; ++i) { + var a = new NodeTestName(steps[i]); + var b = new StepExpr('child', a); + c.appendStep(b); + } + return c; +} + +// The axes of XPath expressions. + +var xpathAxis = { + ANCESTOR_OR_SELF: 'ancestor-or-self', + ANCESTOR: 'ancestor', + ATTRIBUTE: 'attribute', + CHILD: 'child', + DESCENDANT_OR_SELF: 'descendant-or-self', + DESCENDANT: 'descendant', + FOLLOWING_SIBLING: 'following-sibling', + FOLLOWING: 'following', + NAMESPACE: 'namespace', + PARENT: 'parent', + PRECEDING_SIBLING: 'preceding-sibling', + PRECEDING: 'preceding', + SELF: 'self' +}; + +var xpathAxesRe = [ + xpathAxis.ANCESTOR_OR_SELF, + xpathAxis.ANCESTOR, + xpathAxis.ATTRIBUTE, + xpathAxis.CHILD, + xpathAxis.DESCENDANT_OR_SELF, + xpathAxis.DESCENDANT, + xpathAxis.FOLLOWING_SIBLING, + xpathAxis.FOLLOWING, + xpathAxis.NAMESPACE, + xpathAxis.PARENT, + xpathAxis.PRECEDING_SIBLING, + xpathAxis.PRECEDING, + xpathAxis.SELF +].join('|'); + + +// The tokens of the language. The label property is just used for +// generating debug output. The prec property is the precedence used +// for shift/reduce resolution. Default precedence is 0 as a lookahead +// token and 2 on the stack. TODO(mesch): this is certainly not +// necessary and too complicated. Simplify this! + +// NOTE: tabular formatting is the big exception, but here it should +// be OK. + +var TOK_PIPE = { label: "|", prec: 17, re: new RegExp("^\\|") }; +var TOK_DSLASH = { label: "//", prec: 19, re: new RegExp("^//") }; +var TOK_SLASH = { label: "/", prec: 30, re: new RegExp("^/") }; +var TOK_AXIS = { label: "::", prec: 20, re: new RegExp("^::") }; +var TOK_COLON = { label: ":", prec: 1000, re: new RegExp("^:") }; +var TOK_AXISNAME = { label: "[axis]", re: new RegExp('^(' + xpathAxesRe + ')') }; +var TOK_PARENO = { label: "(", prec: 34, re: new RegExp("^\\(") }; +var TOK_PARENC = { label: ")", re: new RegExp("^\\)") }; +var TOK_DDOT = { label: "..", prec: 34, re: new RegExp("^\\.\\.") }; +var TOK_DOT = { label: ".", prec: 34, re: new RegExp("^\\.") }; +var TOK_AT = { label: "@", prec: 34, re: new RegExp("^@") }; + +var TOK_COMMA = { label: ",", re: new RegExp("^,") }; + +var TOK_OR = { label: "or", prec: 10, re: new RegExp("^or\\b") }; +var TOK_AND = { label: "and", prec: 11, re: new RegExp("^and\\b") }; +var TOK_EQ = { label: "=", prec: 12, re: new RegExp("^=") }; +var TOK_NEQ = { label: "!=", prec: 12, re: new RegExp("^!=") }; +var TOK_GE = { label: ">=", prec: 13, re: new RegExp("^>=") }; +var TOK_GT = { label: ">", prec: 13, re: new RegExp("^>") }; +var TOK_LE = { label: "<=", prec: 13, re: new RegExp("^<=") }; +var TOK_LT = { label: "<", prec: 13, re: new RegExp("^<") }; +var TOK_PLUS = { label: "+", prec: 14, re: new RegExp("^\\+"), left: true }; +var TOK_MINUS = { label: "-", prec: 14, re: new RegExp("^\\-"), left: true }; +var TOK_DIV = { label: "div", prec: 15, re: new RegExp("^div\\b"), left: true }; +var TOK_MOD = { label: "mod", prec: 15, re: new RegExp("^mod\\b"), left: true }; + +var TOK_BRACKO = { label: "[", prec: 32, re: new RegExp("^\\[") }; +var TOK_BRACKC = { label: "]", re: new RegExp("^\\]") }; +var TOK_DOLLAR = { label: "$", re: new RegExp("^\\$") }; + +var TOK_NCNAME = { label: "[ncname]", re: new RegExp('^' + XML_NC_NAME) }; + +var TOK_ASTERISK = { label: "*", prec: 15, re: new RegExp("^\\*"), left: true }; +var TOK_LITERALQ = { label: "[litq]", prec: 20, re: new RegExp("^'[^\\']*'") }; +var TOK_LITERALQQ = { + label: "[litqq]", + prec: 20, + re: new RegExp('^"[^\\"]*"') +}; + +var TOK_NUMBER = { + label: "[number]", + prec: 35, + re: new RegExp('^\\d+(\\.\\d*)?') }; + +var TOK_QNAME = { + label: "[qname]", + re: new RegExp('^(' + XML_NC_NAME + ':)?' + XML_NC_NAME) +}; + +var TOK_NODEO = { + label: "[nodetest-start]", + re: new RegExp('^(processing-instruction|comment|text|node)\\(') +}; + +// The table of the tokens of our grammar, used by the lexer: first +// column the tag, second column a regexp to recognize it in the +// input, third column the precedence of the token, fourth column a +// factory function for the semantic value of the token. +// +// NOTE: order of this list is important, because the first match +// counts. Cf. DDOT and DOT, and AXIS and COLON. + +var xpathTokenRules = [ + TOK_DSLASH, + TOK_SLASH, + TOK_DDOT, + TOK_DOT, + TOK_AXIS, + TOK_COLON, + TOK_AXISNAME, + TOK_NODEO, + TOK_PARENO, + TOK_PARENC, + TOK_BRACKO, + TOK_BRACKC, + TOK_AT, + TOK_COMMA, + TOK_OR, + TOK_AND, + TOK_NEQ, + TOK_EQ, + TOK_GE, + TOK_GT, + TOK_LE, + TOK_LT, + TOK_PLUS, + TOK_MINUS, + TOK_ASTERISK, + TOK_PIPE, + TOK_MOD, + TOK_DIV, + TOK_LITERALQ, + TOK_LITERALQQ, + TOK_NUMBER, + TOK_QNAME, + TOK_NCNAME, + TOK_DOLLAR +]; + +// All the nonterminals of the grammar. The nonterminal objects are +// identified by object identity; the labels are used in the debug +// output only. +var XPathLocationPath = { label: "LocationPath" }; +var XPathRelativeLocationPath = { label: "RelativeLocationPath" }; +var XPathAbsoluteLocationPath = { label: "AbsoluteLocationPath" }; +var XPathStep = { label: "Step" }; +var XPathNodeTest = { label: "NodeTest" }; +var XPathPredicate = { label: "Predicate" }; +var XPathLiteral = { label: "Literal" }; +var XPathExpr = { label: "Expr" }; +var XPathPrimaryExpr = { label: "PrimaryExpr" }; +var XPathVariableReference = { label: "Variablereference" }; +var XPathNumber = { label: "Number" }; +var XPathFunctionCall = { label: "FunctionCall" }; +var XPathArgumentRemainder = { label: "ArgumentRemainder" }; +var XPathPathExpr = { label: "PathExpr" }; +var XPathUnionExpr = { label: "UnionExpr" }; +var XPathFilterExpr = { label: "FilterExpr" }; +var XPathDigits = { label: "Digits" }; + +var xpathNonTerminals = [ + XPathLocationPath, + XPathRelativeLocationPath, + XPathAbsoluteLocationPath, + XPathStep, + XPathNodeTest, + XPathPredicate, + XPathLiteral, + XPathExpr, + XPathPrimaryExpr, + XPathVariableReference, + XPathNumber, + XPathFunctionCall, + XPathArgumentRemainder, + XPathPathExpr, + XPathUnionExpr, + XPathFilterExpr, + XPathDigits +]; + +// Quantifiers that are used in the productions of the grammar. +var Q_01 = { label: "?" }; +var Q_MM = { label: "*" }; +var Q_1M = { label: "+" }; + +// Tag for left associativity (right assoc is implied by undefined). +var ASSOC_LEFT = true; + +// The productions of the grammar. Columns of the table: +// +// - target nonterminal, +// - pattern, +// - precedence, +// - semantic value factory +// +// The semantic value factory is a function that receives parse tree +// nodes from the stack frames of the matched symbols as arguments and +// returns an a node of the parse tree. The node is stored in the top +// stack frame along with the target object of the rule. The node in +// the parse tree is an expression object that has an evaluate() method +// and thus evaluates XPath expressions. +// +// The precedence is used to decide between reducing and shifting by +// comparing the precendence of the rule that is candidate for +// reducing with the precedence of the look ahead token. Precedence of +// -1 means that the precedence of the tokens in the pattern is used +// instead. TODO: It shouldn't be necessary to explicitly assign +// precedences to rules. + +// DGF As it stands, these precedences are purely empirical; we're +// not sure they can be made to be consistent at all. + +var xpathGrammarRules = + [ + [ XPathLocationPath, [ XPathRelativeLocationPath ], 18, + passExpr ], + [ XPathLocationPath, [ XPathAbsoluteLocationPath ], 18, + passExpr ], + + [ XPathAbsoluteLocationPath, [ TOK_SLASH, XPathRelativeLocationPath ], 18, + makeLocationExpr1 ], + [ XPathAbsoluteLocationPath, [ TOK_DSLASH, XPathRelativeLocationPath ], 18, + makeLocationExpr2 ], + + [ XPathAbsoluteLocationPath, [ TOK_SLASH ], 0, + makeLocationExpr3 ], + [ XPathAbsoluteLocationPath, [ TOK_DSLASH ], 0, + makeLocationExpr4 ], + + [ XPathRelativeLocationPath, [ XPathStep ], 31, + makeLocationExpr5 ], + [ XPathRelativeLocationPath, + [ XPathRelativeLocationPath, TOK_SLASH, XPathStep ], 31, + makeLocationExpr6 ], + [ XPathRelativeLocationPath, + [ XPathRelativeLocationPath, TOK_DSLASH, XPathStep ], 31, + makeLocationExpr7 ], + + [ XPathStep, [ TOK_DOT ], 33, + makeStepExpr1 ], + [ XPathStep, [ TOK_DDOT ], 33, + makeStepExpr2 ], + [ XPathStep, + [ TOK_AXISNAME, TOK_AXIS, XPathNodeTest ], 33, + makeStepExpr3 ], + [ XPathStep, [ TOK_AT, XPathNodeTest ], 33, + makeStepExpr4 ], + [ XPathStep, [ XPathNodeTest ], 33, + makeStepExpr5 ], + [ XPathStep, [ XPathStep, XPathPredicate ], 33, + makeStepExpr6 ], + + [ XPathNodeTest, [ TOK_ASTERISK ], 33, + makeNodeTestExpr1 ], + [ XPathNodeTest, [ TOK_NCNAME, TOK_COLON, TOK_ASTERISK ], 33, + makeNodeTestExpr2 ], + [ XPathNodeTest, [ TOK_QNAME ], 33, + makeNodeTestExpr3 ], + [ XPathNodeTest, [ TOK_NODEO, TOK_PARENC ], 33, + makeNodeTestExpr4 ], + [ XPathNodeTest, [ TOK_NODEO, XPathLiteral, TOK_PARENC ], 33, + makeNodeTestExpr5 ], + + [ XPathPredicate, [ TOK_BRACKO, XPathExpr, TOK_BRACKC ], 33, + makePredicateExpr ], + + [ XPathPrimaryExpr, [ XPathVariableReference ], 33, + passExpr ], + [ XPathPrimaryExpr, [ TOK_PARENO, XPathExpr, TOK_PARENC ], 33, + makePrimaryExpr ], + [ XPathPrimaryExpr, [ XPathLiteral ], 30, + passExpr ], + [ XPathPrimaryExpr, [ XPathNumber ], 30, + passExpr ], + [ XPathPrimaryExpr, [ XPathFunctionCall ], 31, + passExpr ], + + [ XPathFunctionCall, [ TOK_QNAME, TOK_PARENO, TOK_PARENC ], -1, + makeFunctionCallExpr1 ], + [ XPathFunctionCall, + [ TOK_QNAME, TOK_PARENO, XPathExpr, XPathArgumentRemainder, Q_MM, + TOK_PARENC ], -1, + makeFunctionCallExpr2 ], + [ XPathArgumentRemainder, [ TOK_COMMA, XPathExpr ], -1, + makeArgumentExpr ], + + [ XPathUnionExpr, [ XPathPathExpr ], 20, + passExpr ], + [ XPathUnionExpr, [ XPathUnionExpr, TOK_PIPE, XPathPathExpr ], 20, + makeUnionExpr ], + + [ XPathPathExpr, [ XPathLocationPath ], 20, + passExpr ], + [ XPathPathExpr, [ XPathFilterExpr ], 19, + passExpr ], + [ XPathPathExpr, + [ XPathFilterExpr, TOK_SLASH, XPathRelativeLocationPath ], 19, + makePathExpr1 ], + [ XPathPathExpr, + [ XPathFilterExpr, TOK_DSLASH, XPathRelativeLocationPath ], 19, + makePathExpr2 ], + + [ XPathFilterExpr, [ XPathPrimaryExpr, XPathPredicate, Q_MM ], 31, + makeFilterExpr ], + + [ XPathExpr, [ XPathPrimaryExpr ], 16, + passExpr ], + [ XPathExpr, [ XPathUnionExpr ], 16, + passExpr ], + + [ XPathExpr, [ TOK_MINUS, XPathExpr ], -1, + makeUnaryMinusExpr ], + + [ XPathExpr, [ XPathExpr, TOK_OR, XPathExpr ], -1, + makeBinaryExpr ], + [ XPathExpr, [ XPathExpr, TOK_AND, XPathExpr ], -1, + makeBinaryExpr ], + + [ XPathExpr, [ XPathExpr, TOK_EQ, XPathExpr ], -1, + makeBinaryExpr ], + [ XPathExpr, [ XPathExpr, TOK_NEQ, XPathExpr ], -1, + makeBinaryExpr ], + + [ XPathExpr, [ XPathExpr, TOK_LT, XPathExpr ], -1, + makeBinaryExpr ], + [ XPathExpr, [ XPathExpr, TOK_LE, XPathExpr ], -1, + makeBinaryExpr ], + [ XPathExpr, [ XPathExpr, TOK_GT, XPathExpr ], -1, + makeBinaryExpr ], + [ XPathExpr, [ XPathExpr, TOK_GE, XPathExpr ], -1, + makeBinaryExpr ], + + [ XPathExpr, [ XPathExpr, TOK_PLUS, XPathExpr ], -1, + makeBinaryExpr, ASSOC_LEFT ], + [ XPathExpr, [ XPathExpr, TOK_MINUS, XPathExpr ], -1, + makeBinaryExpr, ASSOC_LEFT ], + + [ XPathExpr, [ XPathExpr, TOK_ASTERISK, XPathExpr ], -1, + makeBinaryExpr, ASSOC_LEFT ], + [ XPathExpr, [ XPathExpr, TOK_DIV, XPathExpr ], -1, + makeBinaryExpr, ASSOC_LEFT ], + [ XPathExpr, [ XPathExpr, TOK_MOD, XPathExpr ], -1, + makeBinaryExpr, ASSOC_LEFT ], + + [ XPathLiteral, [ TOK_LITERALQ ], -1, + makeLiteralExpr ], + [ XPathLiteral, [ TOK_LITERALQQ ], -1, + makeLiteralExpr ], + + [ XPathNumber, [ TOK_NUMBER ], -1, + makeNumberExpr ], + + [ XPathVariableReference, [ TOK_DOLLAR, TOK_QNAME ], 200, + makeVariableReference ] + ]; + +// That function computes some optimizations of the above data +// structures and will be called right here. It merely takes the +// counter variables out of the global scope. + +var xpathRules = []; + +function xpathParseInit() { + if (xpathRules.length) { + return; + } + + // Some simple optimizations for the xpath expression parser: sort + // grammar rules descending by length, so that the longest match is + // first found. + + xpathGrammarRules.sort(function(a,b) { + var la = a[1].length; + var lb = b[1].length; + if (la < lb) { + return 1; + } else if (la > lb) { + return -1; + } else { + return 0; + } + }); + + var k = 1; + for (var i = 0; i < xpathNonTerminals.length; ++i) { + xpathNonTerminals[i].key = k++; + } + + for (i = 0; i < xpathTokenRules.length; ++i) { + xpathTokenRules[i].key = k++; + } + + xpathLog('XPath parse INIT: ' + k + ' rules'); + + // Another slight optimization: sort the rules into bins according + // to the last element (observing quantifiers), so we can restrict + // the match against the stack to the subest of rules that match the + // top of the stack. + // + // TODO(mesch): What we actually want is to compute states as in + // bison, so that we don't have to do any explicit and iterated + // match against the stack. + + function push_(array, position, element) { + if (!array[position]) { + array[position] = []; + } + array[position].push(element); + } + + for (i = 0; i < xpathGrammarRules.length; ++i) { + var rule = xpathGrammarRules[i]; + var pattern = rule[1]; + + for (var j = pattern.length - 1; j >= 0; --j) { + if (pattern[j] == Q_1M) { + push_(xpathRules, pattern[j-1].key, rule); + break; + + } else if (pattern[j] == Q_MM || pattern[j] == Q_01) { + push_(xpathRules, pattern[j-1].key, rule); + --j; + + } else { + push_(xpathRules, pattern[j].key, rule); + break; + } + } + } + + xpathLog('XPath parse INIT: ' + xpathRules.length + ' rule bins'); + + var sum = 0; + mapExec(xpathRules, function(i) { + if (i) { + sum += i.length; + } + }); + + xpathLog('XPath parse INIT: ' + (sum / xpathRules.length) + + ' average bin size'); +} + +// Local utility functions that are used by the lexer or parser. + +function xpathCollectDescendants(nodelist, node, opt_tagName) { + if (opt_tagName && node.getElementsByTagName) { + copyArray(nodelist, node.getElementsByTagName(opt_tagName)); + return; + } + for (var n = node.firstChild; n; n = n.nextSibling) { + nodelist.push(n); + xpathCollectDescendants(nodelist, n); + } +} + +/** + * DGF - extract a tag name suitable for getElementsByTagName + * + * @param nodetest the node test + * @param ignoreNonElementNodesForNTA if true, the node list returned when + * evaluating "node()" will not contain + * non-element nodes. This can boost + * performance. This is false by default. + */ +function xpathExtractTagNameFromNodeTest(nodetest, ignoreNonElementNodesForNTA) { + if (nodetest instanceof NodeTestName) { + return nodetest.name; + } + if ((ignoreNonElementNodesForNTA && nodetest instanceof NodeTestAny) || + nodetest instanceof NodeTestElementOrAttribute) { + return "*"; + } +} + +function xpathCollectDescendantsReverse(nodelist, node) { + for (var n = node.lastChild; n; n = n.previousSibling) { + nodelist.push(n); + xpathCollectDescendantsReverse(nodelist, n); + } +} + + +// The entry point for the library: match an expression against a DOM +// node. Returns an XPath value. +function xpathDomEval(expr, node) { + var expr1 = xpathParse(expr); + var ret = expr1.evaluate(new ExprContext(node)); + return ret; +} + +// Utility function to sort a list of nodes. Used by xsltSort() and +// nxslSelect(). +function xpathSort(input, sort) { + if (sort.length == 0) { + return; + } + + var sortlist = []; + + for (var i = 0; i < input.contextSize(); ++i) { + var node = input.nodelist[i]; + var sortitem = { node: node, key: [] }; + var context = input.clone(node, 0, [ node ]); + + for (var j = 0; j < sort.length; ++j) { + var s = sort[j]; + var value = s.expr.evaluate(context); + + var evalue; + if (s.type == 'text') { + evalue = value.stringValue(); + } else if (s.type == 'number') { + evalue = value.numberValue(); + } + sortitem.key.push({ value: evalue, order: s.order }); + } + + // Make the sort stable by adding a lowest priority sort by + // id. This is very convenient and furthermore required by the + // spec ([XSLT] - Section 10 Sorting). + sortitem.key.push({ value: i, order: 'ascending' }); + + sortlist.push(sortitem); + } + + sortlist.sort(xpathSortByKey); + + var nodes = []; + for (var i = 0; i < sortlist.length; ++i) { + nodes.push(sortlist[i].node); + } + input.nodelist = nodes; + input.setNode(0); +} + + +// Sorts by all order criteria defined. According to the JavaScript +// spec ([ECMA] Section 11.8.5), the compare operators compare strings +// as strings and numbers as numbers. +// +// NOTE: In browsers which do not follow the spec, this breaks only in +// the case that numbers should be sorted as strings, which is very +// uncommon. +function xpathSortByKey(v1, v2) { + // NOTE: Sort key vectors of different length never occur in + // xsltSort. + + for (var i = 0; i < v1.key.length; ++i) { + var o = v1.key[i].order == 'descending' ? -1 : 1; + if (v1.key[i].value > v2.key[i].value) { + return +1 * o; + } else if (v1.key[i].value < v2.key[i].value) { + return -1 * o; + } + } + + return 0; +} + + +// Parses and then evaluates the given XPath expression in the given +// input context. Notice that parsed xpath expressions are cached. +function xpathEval(select, context) { + var expr = xpathParse(select); + var ret = expr.evaluate(context); + return ret; +} diff --git a/tests/selenium/selenium-lib/index.html b/tests/selenium/selenium-lib/index.html deleted file mode 100644 index 959f24e5..00000000 --- a/tests/selenium/selenium-lib/index.html +++ /dev/null @@ -1,63 +0,0 @@ -<html>
-<head>
-<title>Selenium starting points</title>
-<script language="Javascript">
-function fixUnitTestLinks()
-{
- var baseurl = document.location + "";
- baseurl = baseurl.replace(/[^\/]*$/, '');
- var links = document.getElementsByTagName("a");
- for (var i = 0; i < links.length; i++) {
- var link = links[i];
- if (link.className == 'jsUnitSuite') {
- link['href'] = link['href'].replace('BASEURL', baseurl);
- }
- }
-}
-</script>
-<style>
-li { margin-top: 6pt; }
-</style>
-</head>
-<body onload="fixUnitTestLinks();">
-
-<h1>Selenium</h1>
-<p>
- <a href="core/TestRunner.html">Selenium TestRunner</a> - Select a test suite to run in Selenium
-</p>
-
-<p>
-<b>Acceptance tests:</b> These test-suites demonstrate/exercise the
-functionality of Selenium.
-</p>
-
-<ul>
- <li>
- <a href="core/TestRunner.html?test=../tests/TestSuite.html">Selenium TestSuite</a> - functional tests for Selenium. This suite of tests should pass in any supported browser.
- </li>
- <li>
- <a href="core/TestRunner.html?test=../tests/ErrorCheckingTestSuite.html">Error Checking TestSuite</a> - tests for the error verification commands
- </li>
- <li>
- <a href="core/TestRunner.html?test=../tests/dogfood/TestSuite.html&multiWindow=true">Dogfood TestSuite</a> - test Selenium with Selenium
- </li>
-</ul>
-
-<p><b>Unit-tests:</b> Use JsUnit to test Selenium internals.</p>
-
-<ul>
-
- <li>
- <a class="jsUnitSuite" href="jsunit/testRunner.html?testPage=BASEURLunittest/browserbot/suite.html">Selenium BrowserBot unit-tests</a>
- </li>
- <li>
- <a class="jsUnitSuite" href="jsunit/testRunner.html?testPage=BASEURLunittest/htmlrunner/suite.html">Selenium TestRunner UI unit-tests</a>
- </li>
- <li>
- <a class="jsUnitSuite" href="jsunit/testRunner.html?testPage=BASEURLunittest/jsmock/mock-tests.html">JsMock unit-tests</a>
- </li>
-
-</ul>
-
-</body>
-</html>
diff --git a/tests/selenium/selenium-lib/install-readme.txt b/tests/selenium/selenium-lib/install-readme.txt deleted file mode 100644 index 1102361b..00000000 --- a/tests/selenium/selenium-lib/install-readme.txt +++ /dev/null @@ -1,9 +0,0 @@ -Copy the "core" folder to a web accessible directory in the same web server as the application you want to test. -In Apache, this would mean a subdirectory of "htdocs". - -Because of javascript security settings standard in most browsers, Selenium Core needs to be available on the same host and port as your application. (If this doesn't work for you, you may need to try Selenium Remote Control or Selenium IDE.) - -Once deployed to the server, to run Selenium's self-tests, check out: -http://<webservername>:<port>/core/TestRunner.html - -Read the website for more details. (http://www.openqa.org/selenium-core/usage.html) diff --git a/tests/selenium/selenium-lib/reference.html b/tests/selenium/selenium-lib/reference.html index 81b02133..51d20e70 100644 --- a/tests/selenium/selenium-lib/reference.html +++ b/tests/selenium/selenium-lib/reference.html @@ -5,7 +5,39 @@ </head>
<body>
<h1>Selenium Reference</h1>
+<ol>
+<li>
+<a href="#name">Concepts</a>
+</li>
+<ol>
+<li>
+<a href="#locators">Element Locators</a>
+</li>
+<li>
+<a href="#element-filters">Element Filters</a>
+</li>
+<li>
+<a href="#patterns">String-match Patterns</a>
+</li>
+</ol>
+<li>
+<a href="#actions">Selenium Actions</a>
+</li>
+<li>
+<a href="#accessors">Selenium Accessors</a>
+</li>
+<li>
+<a href="#parameter-construction-and-variables">Parameter Construction and Variables</a>
+</li>
+<li>
+<a href="#whitespace">Whitespace Rules</a>
+</li>
+<li>
+<a href="#extending-selenium">Extending Selenium</a>
+</li>
+</ol>
<h2>Concepts</h2>
+<a name="concepts"></a>
<p>A <strong>command</strong> is what tells Selenium what to do. Selenium commands come in three 'flavors': <strong>Actions</strong>, <strong>Accessors</strong> and <strong>Assertions</strong>.
Each command call is one line in the test table of the form:</p>
<blockquote>
@@ -115,6 +147,15 @@ Select the element using css selectors. Please refer to <a href="http://www.w3.o </ul>
<p>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). </p>
</li>
+<li>
+<strong>ui</strong>=<em>uiSpecifierString</em>:
+Locate an element by resolving the UI specifier string to another locator, and evaluating it. See the <a href="http://svn.openqa.org/fisheye/browse/~raw,r=trunk/selenium/trunk/src/main/resources/core/scripts/ui-doc.html">Selenium UI-Element Reference</a> for more details.
+<ul class="first last simple">
+<li>ui=loginPages::loginButton()</li>
+<li>ui=settingsPages::toggle(label=Hide Email)</li>
+<li>ui=forumPages::postBody(index=2)//a[2]</li>
+</ul>
+</li>
</ul>
<p>
Without an explicit locator prefix, Selenium uses the following default
@@ -167,6 +208,9 @@ string.</li> Match a string using a regular-expression. The full power of JavaScript
regular-expressions is available.</li>
<li>
+<strong>regexpi:</strong><em>regexpi</em>:
+Match a string using a case-insensitive regular-expression.</li>
+<li>
<strong>exact:</strong><em>string</em>:
Match a string exactly, verbatim, without any of that fancy wildcard
@@ -176,6 +220,15 @@ stuff.</li> If no pattern prefix is specified, Selenium assumes that it's a "glob"
pattern.
</p>
+<p>
+For commands that return multiple values (such as verifySelectOptions),
+the string being matched is a comma-separated list of the return values,
+where both commas and backslashes in the values are backslash-escaped.
+When providing a pattern, the optional matching syntax (i.e. glob,
+regexp, etc.) is specified once, as usual, at the beginning of the
+pattern.
+</p>
+<a name="actions"></a>
<h2>Selenium Actions</h2>
<dl>
<dt>
@@ -207,6 +260,30 @@ The function must return null if the element can't be found.<p>Arguments:</p> </dd>
<br>
<dt>
+<strong><a name="addScript"></a>addScript
+ (
+ scriptContent,scriptTagId
+ )
+ </strong>
+</dt>
+<dd>Loads script content into a new script tag in the Selenium document. This
+differs from the runScript command in that runScript adds the script tag
+to the document of the AUT, not the Selenium document. The following
+entities in the script content are replaced by the characters they
+represent:
+
+ <
+ >
+ &
+
+The corresponding remove command is removeScript.<p>Arguments:</p>
+<ul>
+<li>scriptContent - the Javascript content of the script to add</li>
+<li>scriptTagId - (optional) the id of the new script tag. If specified, and an element with this id already exists, this operation will fail.</li>
+</ul>
+</dd>
+<br>
+<dt>
<strong><a name="addSelection"></a>addSelection
(
locator,optionLocator
@@ -300,6 +377,32 @@ This command is useful for debugging, but be careful when using it, because it w force automated tests to hang until a user intervenes manually.</dd>
<br>
<dt>
+<strong><a name="captureEntirePageScreenshot"></a>captureEntirePageScreenshot
+ (
+ filename,kwargs
+ )
+ </strong>
+</dt>
+<dd>Saves the entire contents of the current window canvas to a PNG file.
+Contrast this with the captureScreenshot command, which captures the
+contents of the OS viewport (i.e. whatever is currently being displayed
+on the monitor), and is implemented in the RC only. Currently this only
+works in Firefox when running in chrome mode, and in IE non-HTA using
+the EXPERIMENTAL "Snapsie" utility. The Firefox implementation is mostly
+borrowed from the Screengrab! Firefox extension. Please see
+http://www.screengrab.org and http://snapsie.sourceforge.net/ for
+details.<p>Arguments:</p>
+<ul>
+<li>filename - the path to the file to persist the screenshot as. No filename extension will be appended by default. Directories will not be created if they do not exist, and an exception will be thrown, possibly by native code.</li>
+<li>kwargs - a kwargs string that modifies the way the screenshot is captured. Example: "background=#CCFFDD" . Currently valid options: <dl>
+<dt>background</dt>
+<dd>the background CSS for the HTML document. This may be useful to set for capturing screenshots of less-than-ideal layouts, for example where absolute positioning causes the calculation of the canvas dimension to fail and a black background is exposed (possibly obscuring black text).</dd>
+</dl>
+</li>
+</ul>
+</dd>
+<br>
+<dt>
<strong><a name="check"></a>check
(
locator
@@ -320,13 +423,22 @@ force automated tests to hang until a user intervenes manually.</dd> )
</strong>
</dt>
-<dd>By default, Selenium's overridden window.confirm() function will
+<dd>
+<p>
+By default, Selenium's overridden window.confirm() function will
return true, as if the user had manually clicked OK; after running
this command, the next call to confirm() will return false, as if
the user had clicked Cancel. Selenium will then resume using the
default behavior for future confirmations, automatically returning
true (OK) unless/until you explicitly call this command for each
-confirmation.</dd>
+confirmation.
+</p>
+<p>
+Take note - every time a confirmation comes up, you must
+consume it with a corresponding getConfirmation, or else
+the next selenium operation will fail.
+</p>
+</dd>
<br>
<dt>
<strong><a name="chooseOkOnNextConfirmation"></a>chooseOkOnNextConfirmation
@@ -335,14 +447,23 @@ confirmation.</dd> )
</strong>
</dt>
-<dd>Undo the effect of calling chooseCancelOnNextConfirmation. Note
+<dd>
+<p>
+Undo the effect of calling chooseCancelOnNextConfirmation. Note
that Selenium's overridden window.confirm() function will normally automatically
return true, as if the user had manually clicked OK, so you shouldn't
need to use this command unless for some reason you need to change
your mind prior to the next confirmation. After any confirmation, Selenium will resume using the
default behavior for future confirmations, automatically returning
true (OK) unless/until you explicitly call chooseCancelOnNextConfirmation for each
-confirmation.</dd>
+confirmation.
+</p>
+<p>
+Take note - every time a confirmation comes up, you must
+consume it with a corresponding getConfirmation, or else
+the next selenium operation will fail.
+</p>
+</dd>
<br>
<dt>
<strong><a name="click"></a>click
@@ -386,6 +507,33 @@ waitForPageToLoad.<p>Arguments:</p> window or tab.</dd>
<br>
<dt>
+<strong><a name="contextMenu"></a>contextMenu
+ (
+ locator
+ )
+ </strong>
+</dt>
+<dd>Simulates opening the context menu for the specified element (as might happen if the user "right-clicked" on the element).<p>Arguments:</p>
+<ul>
+<li>locator - an element locator</li>
+</ul>
+</dd>
+<br>
+<dt>
+<strong><a name="contextMenuAt"></a>contextMenuAt
+ (
+ locator,coordString
+ )
+ </strong>
+</dt>
+<dd>Simulates opening the context menu for the specified element (as might happen if the user "right-clicked" on the element).<p>Arguments:</p>
+<ul>
+<li>locator - an element locator</li>
+<li>coordString - specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator.</li>
+</ul>
+</dd>
+<br>
+<dt>
<strong><a name="controlKeyDown"></a>controlKeyDown
(
@@ -414,25 +562,56 @@ window or tab.</dd> under test, unless you specified a path for this cookie explicitly.<p>Arguments:</p>
<ul>
<li>nameValuePair - name and value of the cookie in a format "name=value"</li>
-<li>optionsString - options for the cookie. Currently supported options include 'path' and 'max_age'. the optionsString's format is "path=/path/, max_age=60". The order of options are irrelevant, the unit of the value of 'max_age' is second.</li>
+<li>optionsString - options for the cookie. Currently supported options include 'path', 'max_age' and 'domain'. the optionsString's format is "path=/path/, max_age=60, domain=.foo.com". The order of options are irrelevant, the unit of the value of 'max_age' is second. Note that specifying a domain that isn't a subset of the current domain will usually fail.</li>
</ul>
</dd>
<br>
<dt>
+<strong><a name="deleteAllVisibleCookies"></a>deleteAllVisibleCookies
+ (
+
+ )
+ </strong>
+</dt>
+<dd>Calls deleteCookie with recurse=true on all cookies visible to the current page.
+As noted on the documentation for deleteCookie, recurse=true can be much slower
+than simply deleting the cookies using a known domain/path.</dd>
+<br>
+<dt>
<strong><a name="deleteCookie"></a>deleteCookie
(
- name,path
+ name,optionsString
)
</strong>
</dt>
-<dd>Delete a named cookie with specified path.<p>Arguments:</p>
+<dd>Delete a named cookie with specified path and domain. Be careful; to delete a cookie, you
+need to delete it using the exact same path and domain that were used to create the cookie.
+If the path is wrong, or the domain is wrong, the cookie simply won't be deleted. Also
+note that specifying a domain that isn't a subset of the current domain will usually fail.
+
+Since there's no way to discover at runtime the original path and domain of a given cookie,
+we've added an option called 'recurse' to try all sub-domains of the current domain with
+all paths that are a subset of the current path. Beware; this option can be slow. In
+big-O notation, it operates in O(n*m) time, where n is the number of dots in the domain
+name and m is the number of slashes in the path.<p>Arguments:</p>
<ul>
<li>name - the name of the cookie to be deleted</li>
-<li>path - the path property of the cookie to be deleted</li>
+<li>optionsString - options for the cookie. Currently supported options include 'path', 'domain' and 'recurse.' The optionsString's format is "path=/path/, domain=.foo.com, recurse=true". The order of options are irrelevant. Note that specifying a domain that isn't a subset of the current domain will usually fail.</li>
</ul>
</dd>
<br>
<dt>
+<strong><a name="deselectPopUp"></a>deselectPopUp
+ (
+
+ )
+ </strong>
+</dt>
+<dd>Selects the main window. Functionally equivalent to using
+<code>selectWindow()</code> and specifying no value for
+<code>windowID</code>.</dd>
+<br>
+<dt>
<strong><a name="doubleClick"></a>doubleClick
(
locator
@@ -536,16 +715,18 @@ handler.<p>Arguments:</p> </dd>
<br>
<dt>
-<strong><a name="getSpeed"></a>getSpeed
+<strong><a name="focus"></a>focus
(
-
+ locator
)
</strong>
</dt>
-<dd>Get execution speed (i.e., get the millisecond length of the delay following each selenium operation). By default, there is no such delay, i.e.,
-the delay is 0 milliseconds.
-
-See also setSpeed.</dd>
+<dd>Move the focus to the specified element; for example, if the element is an input field, move the cursor to that field.<p>Arguments:</p>
+<ul>
+<li>locator - an <a href="#locators">element locator</a>
+</li>
+</ul>
+</dd>
<br>
<dt>
<strong><a name="goBack"></a>goBack
@@ -571,6 +752,27 @@ See also setSpeed.</dd> </dd>
<br>
<dt>
+<strong><a name="ignoreAttributesWithoutValue"></a>ignoreAttributesWithoutValue
+ (
+ ignore
+ )
+ </strong>
+</dt>
+<dd>Specifies whether Selenium will ignore xpath attributes that have no
+value, i.e. are the empty string, when using the non-native xpath
+evaluation engine. You'd want to do this for performance reasons in IE.
+However, this could break certain xpaths, for example an xpath that looks
+for an attribute whose value is NOT the empty string.
+
+The hope is that such xpaths are relatively rare, but the user should
+have the option of using them. Note that this only influences xpath
+evaluation when using the ajaxslt engine (i.e. not "javascript-xpath").<p>Arguments:</p>
+<ul>
+<li>ignore - boolean, true means we'll ignore attributes without value at the expense of xpath "correctness"; false means we'll sacrifice speed for correctness.</li>
+</ul>
+</dd>
+<br>
+<dt>
<strong><a name="keyDown"></a>keyDown
(
locator,keySequence
@@ -640,7 +842,7 @@ See also setSpeed.</dd> )
</strong>
</dt>
-<dd>Simulates a user pressing the mouse button (without releasing it yet) on
+<dd>Simulates a user pressing the left mouse button (without releasing it yet) on
the specified element.<p>Arguments:</p>
<ul>
<li>locator - an <a href="#locators">element locator</a>
@@ -655,7 +857,38 @@ the specified element.<p>Arguments:</p> )
</strong>
</dt>
-<dd>Simulates a user pressing the mouse button (without releasing it yet) at
+<dd>Simulates a user pressing the left mouse button (without releasing it yet) at
+the specified location.<p>Arguments:</p>
+<ul>
+<li>locator - an <a href="#locators">element locator</a>
+</li>
+<li>coordString - specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator.</li>
+</ul>
+</dd>
+<br>
+<dt>
+<strong><a name="mouseDownRight"></a>mouseDownRight
+ (
+ locator
+ )
+ </strong>
+</dt>
+<dd>Simulates a user pressing the right mouse button (without releasing it yet) on
+the specified element.<p>Arguments:</p>
+<ul>
+<li>locator - an <a href="#locators">element locator</a>
+</li>
+</ul>
+</dd>
+<br>
+<dt>
+<strong><a name="mouseDownRightAt"></a>mouseDownRightAt
+ (
+ locator,coordString
+ )
+ </strong>
+</dt>
+<dd>Simulates a user pressing the right mouse button (without releasing it yet) at
the specified location.<p>Arguments:</p>
<ul>
<li>locator - an <a href="#locators">element locator</a>
@@ -755,6 +988,37 @@ holding the button down) at the specified location.<p>Arguments:</p> </dd>
<br>
<dt>
+<strong><a name="mouseUpRight"></a>mouseUpRight
+ (
+ locator
+ )
+ </strong>
+</dt>
+<dd>Simulates the event that occurs when the user releases the right mouse button (i.e., stops
+holding the button down) on the specified element.<p>Arguments:</p>
+<ul>
+<li>locator - an <a href="#locators">element locator</a>
+</li>
+</ul>
+</dd>
+<br>
+<dt>
+<strong><a name="mouseUpRightAt"></a>mouseUpRightAt
+ (
+ locator,coordString
+ )
+ </strong>
+</dt>
+<dd>Simulates the event that occurs when the user releases the right mouse button (i.e., stops
+holding the button down) at the specified location.<p>Arguments:</p>
+<ul>
+<li>locator - an <a href="#locators">element locator</a>
+</li>
+<li>coordString - specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator.</li>
+</ul>
+</dd>
+<br>
+<dt>
<strong><a name="open"></a>open
(
url
@@ -833,6 +1097,20 @@ an empty (blank) url, like this: openWindow("", "myFunnyWindow").</p> </dd>
<br>
<dt>
+<strong><a name="removeScript"></a>removeScript
+ (
+ scriptTagId
+ )
+ </strong>
+</dt>
+<dd>Removes a script tag from the Selenium document identified by the given
+id. Does nothing if the referenced tag doesn't exist.<p>Arguments:</p>
+<ul>
+<li>scriptTagId - the id of the script element to remove.</li>
+</ul>
+</dd>
+<br>
+<dt>
<strong><a name="removeSelection"></a>removeSelection
(
locator,optionLocator
@@ -849,6 +1127,23 @@ an empty (blank) url, like this: openWindow("", "myFunnyWindow").</p> </dd>
<br>
<dt>
+<strong><a name="rollup"></a>rollup
+ (
+ rollupName,kwargs
+ )
+ </strong>
+</dt>
+<dd>Executes a command rollup, which is a series of commands with a unique
+name, and optionally arguments that control the generation of the set of
+commands. If any one of the rolled-up commands fails, the rollup is
+considered to have failed. Rollups may also contain nested rollups.<p>Arguments:</p>
+<ul>
+<li>rollupName - the name of the rollup command</li>
+<li>kwargs - keyword arguments string that influences how the rollup expands into commands</li>
+</ul>
+</dd>
+<br>
+<dt>
<strong><a name="runScript"></a>runScript
(
script
@@ -947,30 +1242,79 @@ like this: <code>dom=frames["main"].frames["subframe"]</code> </dd>
<br>
<dt>
+<strong><a name="selectPopUp"></a>selectPopUp
+ (
+ windowID
+ )
+ </strong>
+</dt>
+<dd>Simplifies the process of selecting a popup window (and does not offer
+functionality beyond what <code>selectWindow()</code> already provides).
+<ul>
+<li>If <code>windowID</code> is either not specified, or specified as
+"null", the first non-top window is selected. The top window is the one
+that would be selected by <code>selectWindow()</code> without providing a
+<code>windowID</code> . This should not be used when more than one popup
+window is in play.</li>
+<li>Otherwise, the window will be looked up considering
+<code>windowID</code> as the following in order: 1) the "name" of the
+window, as specified to <code>window.open()</code>; 2) a javascript
+variable which is a reference to a window; and 3) the title of the
+window. This is the same ordered lookup performed by
+<code>selectWindow</code> .</li>
+</ul>
+<p>Arguments:</p>
+<ul>
+<li>windowID - an identifier for the popup window, which can take on a number of different meanings</li>
+</ul>
+</dd>
+<br>
+<dt>
<strong><a name="selectWindow"></a>selectWindow
(
windowID
)
</strong>
</dt>
-<dd>Selects a popup window; once a popup window has been selected, all
+<dd>Selects a popup window using a window locator; once a popup window has been selected, all
commands go to that window. To select the main window again, use null
as the target.
-<p>Note that there is a big difference between a window's internal JavaScript "name" property
-and the "title" of a given window's document (which is normally what you actually see, as an end user,
-in the title bar of the window). The "name" is normally invisible to the end-user; it's the second
+<p>
+
+Window locators provide different ways of specifying the window object:
+by title, by internal JavaScript "name," or by JavaScript variable.
+</p>
+<ul>
+<li>
+<strong>title</strong>=<em>My Special Window</em>:
+Finds the window using the text that appears in the title bar. Be careful;
+two windows can share the same title. If that happens, this locator will
+just pick one.
+</li>
+<li>
+<strong>name</strong>=<em>myWindow</em>:
+Finds the window using its internal JavaScript "name" property. This is the second
parameter "windowName" passed to the JavaScript method window.open(url, windowName, windowFeatures, replaceFlag)
-(which selenium intercepts).</p>
-<p>Selenium has several strategies for finding the window object referred to by the "windowID" parameter.</p>
+(which Selenium intercepts).
+</li>
+<li>
+<strong>var</strong>=<em>variableName</em>:
+Some pop-up windows are unnamed (anonymous), but are associated with a JavaScript variable name in the current
+application window, e.g. "window.foo = window.open(url);". In those cases, you can open the window using
+"var=foo".
+</li>
+</ul>
+<p>
+If no window locator prefix is provided, we'll try to guess what you mean like this:</p>
<p>1.) if windowID is null, (or the string "null") then it is assumed the user is referring to the original window instantiated by the browser).</p>
<p>2.) if the value of the "windowID" parameter is a JavaScript variable name in the current application window, then it is assumed
that this variable contains the return value from a call to the JavaScript window.open() method.</p>
<p>3.) Otherwise, selenium looks in a hash it maintains that maps string names to window "names".</p>
-<p>4.) If <i>that</i> fails, we'll try looping over all of the known windows to try to find the appropriate "title".
+<p>4.) If <em>that</em> fails, we'll try looping over all of the known windows to try to find the appropriate "title".
Since "title" is not necessarily unique, this may have unexpected behavior.</p>
-<p>If you're having trouble figuring out what is the name of a window that you want to manipulate, look at the selenium log messages
-which identify the names of windows created via window.open (and therefore intercepted by selenium). You will see messages
+<p>If you're having trouble figuring out the name of a window that you want to manipulate, look at the Selenium log messages
+which identify the names of windows created via window.open (and therefore intercepted by Selenium). You will see messages
like the following for each window as it is opened:</p>
<p>
<code>debug: window.open call intercepted; window ID (which you can use with selectWindow()) is "myNewWindow"</code>
@@ -1169,6 +1513,23 @@ send the keystroke events corresponding to what you just typed.</p> </dd>
<br>
<dt>
+<strong><a name="useXpathLibrary"></a>useXpathLibrary
+ (
+ libraryName
+ )
+ </strong>
+</dt>
+<dd>Allows choice of one of the available libraries.<p>Arguments:</p>
+<ul>
+<li>libraryName - name of the desired library Only the following three can be chosen: <ul>
+<li>"ajaxslt" - Google's library</li>
+<li>"javascript-xpath" - Cybozu Labs' faster library</li>
+<li>"default" - The default library. Currently the default library is "ajaxslt" .</li>
+</ul> If libraryName isn't one of these three, then no change will be made.</li>
+</ul>
+</dd>
+<br>
+<dt>
<strong><a name="waitForCondition"></a>waitForCondition
(
script,timeout
@@ -1239,8 +1600,8 @@ wait immediately after a Selenium command that caused a page-load.</p> </dt>
<dd>Waits for a popup window to appear and load up.<p>Arguments:</p>
<ul>
-<li>windowID - the JavaScript window ID of the window that will appear</li>
-<li>timeout - a timeout in milliseconds, after which the action will return with an error</li>
+<li>windowID - the JavaScript window "name" of the window that will appear (not the text of the title bar) If unspecified, or specified as "null", this command will wait for the first non-top window to appear (don't rely on this if you are working with multiple popups simultaneously).</li>
+<li>timeout - a timeout in milliseconds, after which the action will return with an error. If this value is not specified, the default Selenium timeout will be used. See the setTimeout() command.</li>
</ul>
</dd>
<br>
@@ -1263,6 +1624,7 @@ wait immediately after a Selenium command that caused a page-load.</p> <dd>Resize currently selected window to take up the entire screen</dd>
<br>
</dl>
+<a name="accessors"></a>
<h2>Selenium Accessors</h2>
<dl>
<dt>
@@ -1404,11 +1766,11 @@ wait immediately after a Selenium command that caused a page-load.</p> <dd>Retrieves the message of a JavaScript alert generated during the previous action, or fail if there were no alerts.
<p>Getting an alert has the same effect as manually clicking OK. If an
-alert is generated but you do not get/verify it, the next Selenium action
+alert is generated but you do not consume it with getAlert, the next Selenium action
will fail.</p>
-<p>NOTE: under Selenium, JavaScript alerts will NOT pop up a visible alert
+<p>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
+<p>Selenium does NOT support JavaScript alerts that are generated in a
page's onload() event handler. In this case a visible dialog WILL be
generated and Selenium will hang until someone manually clicks OK.</p>
<p>
@@ -1624,10 +1986,10 @@ generated and Selenium will hang until someone manually clicks OK.</p> </strong>
</dt>
-<dd>Returns the IDs of all windows that the browser knows about.<p>
+<dd>Returns the IDs of all windows that the browser knows about in an array.<p>
<dl>
<dt>Returns: </dt>
-<dd>the IDs of all windows that the browser knows about.</dd>
+<dd>Array of identifiers of all windows that the browser knows about.</dd>
</dl>
</p>
<p>Related Assertions, automatically generated:</p>
@@ -1675,10 +2037,10 @@ generated and Selenium will hang until someone manually clicks OK.</p> </strong>
</dt>
-<dd>Returns the names of all windows that the browser knows about.<p>
+<dd>Returns the names of all windows that the browser knows about in an array.<p>
<dl>
<dt>Returns: </dt>
-<dd>the names of all windows that the browser knows about.</dd>
+<dd>Array of names of all windows that the browser knows about.</dd>
</dl>
</p>
<p>Related Assertions, automatically generated:</p>
@@ -1726,10 +2088,10 @@ generated and Selenium will hang until someone manually clicks OK.</p> </strong>
</dt>
-<dd>Returns the titles of all windows that the browser knows about.<p>
+<dd>Returns the titles of all windows that the browser knows about in an array.<p>
<dl>
<dt>Returns: </dt>
-<dd>the titles of all windows that the browser knows about.</dd>
+<dd>Array of titles of all windows that the browser knows about.</dd>
</dl>
</p>
<p>Related Assertions, automatically generated:</p>
@@ -1778,7 +2140,9 @@ generated and Selenium will hang until someone manually clicks OK.</p> </strong>
</dt>
-<dd>Gets the value of an element attribute.<p>Arguments:</p>
+<dd>Gets the value of an element attribute. The value of the attribute may
+differ across browsers (this is the case for the "style" attribute, for
+example).<p>Arguments:</p>
<ul>
<li>attributeLocator - an element locator followed by an @ sign and then the name of the attribute, e.g. "foo@bar"</li>
<li>variableName -
@@ -1837,7 +2201,7 @@ generated and Selenium will hang until someone manually clicks OK.</p> </strong>
</dt>
-<dd>Returns every instance of some attribute from all known windows.<p>Arguments:</p>
+<dd>Returns an array of JavaScript property values from all known windows having one.<p>Arguments:</p>
<ul>
<li>attributeName - name of an attribute on the windows</li>
<li>variableName -
@@ -1952,8 +2316,11 @@ 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.
+chooseCancelOnNextConfirmation command.
+</p>
+<p>
+If an confirmation is generated but you do not consume it with getConfirmation,
+the next Selenium action will fail.
</p>
<p>
NOTE: under Selenium, JavaScript confirmations will NOT pop up a visible
@@ -2058,6 +2425,65 @@ OK. </dd>
<br>
<dt>
+<strong><a name="storeCookieByName"></a>storeCookieByName
+
+ (
+ name,
+
+ variableName
+ )
+
+ </strong>
+</dt>
+<dd>Returns the value of the cookie with the specified name, or throws an error if the cookie is not present.<p>Arguments:</p>
+<ul>
+<li>name - the name of the cookie</li>
+<li>variableName -
+ the name of a <a href="#storedVars">variable</a> in which the result is to be stored.
+ </li>
+</ul>
+<p>
+<dl>
+<dt>Returns: </dt>
+<dd>the value of the cookie</dd>
+</dl>
+</p>
+<p>Related Assertions, automatically generated:</p>
+<ul>
+<li>assertCookieByName
+ (
+ name, <a href="#patterns">pattern</a>
+ )
+ </li>
+<li>assertNotCookieByName
+ (
+ name, <a href="#patterns">pattern</a>
+ )
+ </li>
+<li>verifyCookieByName
+ (
+ name, <a href="#patterns">pattern</a>
+ )
+ </li>
+<li>verifyNotCookieByName
+ (
+ name, <a href="#patterns">pattern</a>
+ )
+ </li>
+<li>waitForCookieByName
+ (
+ name, <a href="#patterns">pattern</a>
+ )
+ </li>
+<li>waitForNotCookieByName
+ (
+ name, <a href="#patterns">pattern</a>
+ )
+ </li>
+</ul>
+</dd>
+<br>
+<dt>
<strong><a name="storeCursorPosition"></a>storeCursorPosition
(
@@ -3296,6 +3722,60 @@ generated and Selenium will hang until someone manually clicks OK.</p> </dd>
<br>
<dt>
+<strong><a name="storeSpeed"></a>storeSpeed
+
+ (
+
+ variableName
+ )
+
+ </strong>
+</dt>
+<dd>Get execution speed (i.e., get the millisecond length of the delay following each selenium operation). By default, there is no such delay, i.e.,
+the delay is 0 milliseconds.
+
+See also setSpeed.<p>
+<dl>
+<dt>Returns: </dt>
+<dd>the execution speed in milliseconds.</dd>
+</dl>
+</p>
+<p>Related Assertions, automatically generated:</p>
+<ul>
+<li>assertSpeed
+ (
+ <a href="#patterns">pattern</a>
+ )
+ </li>
+<li>assertNotSpeed
+ (
+ <a href="#patterns">pattern</a>
+ )
+ </li>
+<li>verifySpeed
+ (
+ <a href="#patterns">pattern</a>
+ )
+ </li>
+<li>verifyNotSpeed
+ (
+ <a href="#patterns">pattern</a>
+ )
+ </li>
+<li>waitForSpeed
+ (
+ <a href="#patterns">pattern</a>
+ )
+ </li>
+<li>waitForNotSpeed
+ (
+ <a href="#patterns">pattern</a>
+ )
+ </li>
+</ul>
+</dd>
+<br>
+<dt>
<strong><a name="storeTable"></a>storeTable
(
@@ -3899,6 +4379,65 @@ This function never throws an exception </dd>
<br>
<dt>
+<strong><a name="storeCookiePresent"></a>storeCookiePresent
+
+ (
+ name,
+
+ variableName
+ )
+
+ </strong>
+</dt>
+<dd>Returns true if a cookie with the specified name is present, or false otherwise.<p>Arguments:</p>
+<ul>
+<li>name - the name of the cookie</li>
+<li>variableName -
+ the name of a <a href="#storedVars">variable</a> in which the result is to be stored.
+ </li>
+</ul>
+<p>
+<dl>
+<dt>Returns: </dt>
+<dd>true if a cookie with the specified name is present, or false otherwise.</dd>
+</dl>
+</p>
+<p>Related Assertions, automatically generated:</p>
+<ul>
+<li>assertCookiePresent
+ (
+ name
+ )
+ </li>
+<li>assertCookieNotPresent
+ (
+ name
+ )
+ </li>
+<li>verifyCookiePresent
+ (
+ name
+ )
+ </li>
+<li>verifyCookieNotPresent
+ (
+ name
+ )
+ </li>
+<li>waitForCookiePresent
+ (
+ name
+ )
+ </li>
+<li>waitForCookieNotPresent
+ (
+ name
+ )
+ </li>
+</ul>
+</dd>
+<br>
+<dt>
<strong><a name="storeEditable"></a>storeEditable
(
@@ -4391,6 +4930,18 @@ the element is not present.<p>Arguments:</p> </table>
</blockquote>
</blockquote>
+<div class="section" id="whitespace">
+<h2>
+<a name="whitespace">Whitespace Rules</a>
+</h2>
+<blockquote>
+<p>HTML automatically normalizes whitespace within elements, ignoring leading/trailing spaces and converting extra spaces, tabs and newlines into a single space. When Selenium reads text out of the page, it attempts to duplicate this behavior, so you can ignore all the tabs and newlines in your HTML and do assertions based on how the text looks in the browser when rendered. We do this by replacing all non-visible whitespace (including the non-breaking space "&nbsp;") with a single space. All visible newlines (<br>, <p>, and <pre>formatted newlines) should be preserved.</p>
+<p>We use the same normalization logic on the text of HTML Selenese test case tables. This has a number of advantages. First, you don't need to look at the HTML source of the page to figure out what your assertions should be; "&nbsp;" symbols are invisible to the end user, and so you shouldn't have to worry about them when writing Selenese tests. (You don't need to put "&nbsp;" markers in your test case to assertText on a field that contains "&nbsp;".) You may also put extra newlines and spaces in your Selenese <td> tags; since we use the same normalization logic on the test case as we do on the text, we can ensure that assertions and the extracted text will match exactly.</p>
+<p>This creates a bit of a problem on those rare occasions when you really want/need to insert extra whitespace in your test case. For example, you may need to type text in a field like this: "foo ". But if you simply write <td>foo </td> in your Selenese test case, we'll replace your extra spaces with just one space.</p>
+<p>This problem has a simple workaround. We've defined a variable in Selenese, ${space}, whose value is a single space. You can use ${space} to insert a space that won't be automatically trimmed, like this: <td>foo${space}${space}${space}</td>. We've also included a variable ${nbsp}, that you can use to insert a non-breaking space.</p>
+<p>Note that XPaths do <i>not</i> normalize whitespace the way we do. If you need to write an XPath like //div[text()="hello world"] but the HTML of the link is really "hello&nbsp;world", you'll need to insert a real "&nbsp;" into your Selenese test case to get it to match, like this: //div[text()="hello${nbsp}world"].</p>
+</blockquote>
+</div>
<div class="section" id="extending-selenium">
<h2>
<a name="extending-selenium">Extending Selenium</a>
|