Source: SelBlocksGlobal/sel-blocks-fx_xpi/chrome/content/extensions/selblocks.js

  1. /* Copyright 2011 Chris Noe
  2. * Copyright 2011, 2012, 2013, 2014, 2015, 2016 Peter Kehl
  3. * This Source Code Form is subject to the terms of the Mozilla Public
  4. * License, v. 1.1. If a copy of the MPL was not distributed with this file,
  5. * You can obtain one at http://mozilla.org/MPL/1.1/.
  6. */
  7. /**
  8. * SelBlocks Global = SelBlocks with functions callable across test cases.
  9. * Based on SelBlocks 2.1.
  10. *
  11. * SelBlocksGlobal change log, as compared to SelBlocks, in chronological order:
  12. * - made functions (formerly scripts) callable across test cases
  13. * - made it compatible with Javscript strict mode - "use strict";
  14. * -- for that I've removed automatic access to stored variables (without using $). That affects mostly 'for' loop and right side of parameter assignments of 'call'. See http://selite.github.io/SelBlocksGlobal.
  15. * - see http://selite.github.io/EnhancedSelenese.
  16. * - if/while/for, call, EnhancedSelenese now recognise object "window" - just like getEval() did.
  17. * -- therefore evalWithExpandedStoredVars, dropToLoop, returnFromFunction, parseArgs are now a part of Selenium.prototype
  18. * -- other helper functions are now a part of Selenium.prototype, where needed
  19. * - changed 'xyz instanceof Array' to Array.isArray(xyz); this may be needed to recognise Array instances passed from different global scope
  20. * - similarly, changed xyz.constructor==String to xyz.constructor && xyz.constructor.name==='String'
  21. * - removed isOneOf, mapTo and translate()
  22. * - added commands: promise, storePromiseValue, ifPromise...endIfPromise, whilePromise..endWhilePromise
  23. * -----------
  24. * Notes from SelBlocks
  25. *
  26. * Provides commands for Javascript-like looping and callable functions,
  27. * with scoped variables, and JSON/XML driven parameterization.
  28. *
  29. * (SelBlocks installs as a Core Extension, not an IDE Extension, because it manipulates the Selenium object)
  30. *
  31. * Concept of operation:
  32. * - Selenium.reset() is intercepted to initialize the block structures.
  33. * - testCase.nextCommand() is overridden for flow branching.
  34. * - TestLoop.resume() is overridden by exitTest, and by try/catch/finally to manage the outcome of errors.
  35. * - The static structure of command blocks is stored in blockDefs[] by script line number.
  36. * E.g., ifDef has pointers to its corresponding elseIf, else, endIf commands.
  37. * - The state of each function-call is pushed/popped on callStack as it begins/ends execution
  38. * The state of each block is pushed/popped on the blockStack as it begins/ends execution.
  39. * An independent blockStack is associated with each function-call. I.e., stacks stored on a stack.
  40. * (Non-block commands do not appear on the blockStack.)
  41. *
  42. * Limitations:
  43. * - Incompatible with flowControl (and derivatives), because they unilaterally override selenium.reset().
  44. * Known to have this issue:
  45. * selenium_ide__flow_control
  46. * goto_while_for_ide
  47. *
  48. * Acknowledgements:
  49. * SelBlocks reuses bits & parts of extensions: flowControl, datadriven, and include.
  50. *
  51. * Wishlist:
  52. * - show line numbers in the IDE
  53. * - validation of JSON & XML input files
  54. * - highlight a command that is failed-but-caught in blue
  55. *
  56. * Changes since 1.5:
  57. * - added try/catch/finally, elseIf, and exitTest commands
  58. * - block boundaries enforced (jumping in-to and/or out-of the middle of blocks)
  59. * - script/endScript is replaced by function/endFunction
  60. * - implicit initialization of for loop variable(s)
  61. * - improved validation of command expressions
  62. *
  63. * NOTE - The only thing special about SelBlocks parameters is that they are activated and deactivated
  64. * as script execution flows into and out of blocks, (for/endFor, function/endFunction, etc).
  65. * They are implemented as regular Selenium variables, and therefore the progress of an executing
  66. * script can be monitored using the Stored Variables Viewer addon.
  67. **/
  68. "use strict";
  69. // Following two assignments are purely for JSDoc.
  70. /** @class */
  71. Selenium= Selenium;
  72. /** @namespace */
  73. selblocks= selblocks;
  74. // =============== global functions as script helpers ===============
  75. // getEval script helpers
  76. // Find an element via locator independent of any selenium commands
  77. // (findElementOrNull returns the first if there are multiple matches)
  78. function $e(locator) {
  79. return selblocks.unwrapObject(selenium.browserbot.findElementOrNull(locator));
  80. }
  81. // Return the singular XPath result as a value of the appropriate type
  82. function $x(xpath, contextNode, resultType) {
  83. var doc = selenium.browserbot.getDocument();
  84. var node;
  85. if (resultType) {
  86. node = selblocks.xp.selectNode(doc, xpath, contextNode, resultType); // mozilla engine only
  87. }
  88. else {
  89. node = selblocks.xp.selectElement(doc, xpath, contextNode);
  90. }
  91. return node;
  92. }
  93. // Return the XPath result set as an array of elements
  94. function $X(xpath, contextNode, resultType) {
  95. var doc = selenium.browserbot.getDocument();
  96. var nodes;
  97. if (resultType) {
  98. nodes = selblocks.xp.selectNodes(doc, xpath, contextNode, resultType); // mozilla engine only
  99. }
  100. else {
  101. nodes = selblocks.xp.selectElements(doc, xpath, contextNode);
  102. }
  103. return nodes;
  104. }
  105. /** @type {function}
  106. */
  107. var expandStoredVars;
  108. // selbocks name-space
  109. (function($$){
  110. // =============== Javascript extensions as script helpers ===============
  111. // EXTENSION REVIEWERS:
  112. // Global functions are intentional features provided for use by end user's in their Selenium scripts.
  113. //SelBlocksGlobal removed isOneOf|mapTo|translate. Instead of string.isOneOf(first, second...), use [first, second...].indexOf(string)<0.
  114. // eg: "red".mapTo("primary", ["red","green","blue"]) => primary
  115. /*String.prototype.mapTo = function mapTo()// TODO string parameter; then pairs of: string, array
  116. {
  117. var errMsg = " The map function requires pairs of argument: string, array";
  118. assert(arguments.length % 2 === 0, errMsg + "; found " + arguments.length);
  119. var i;
  120. for (i = 0; i < arguments.length; i += 2) {
  121. assert((typeof arguments[i].toLowerCase() === "string") && Array.isArray(arguments[i+1]),
  122. errMsg + "; found " + typeof arguments[i] + ", " + typeof arguments[i+1]);
  123. if ( this.isOneOf(arguments[i+1]) ) {
  124. return arguments[i];
  125. }
  126. }
  127. return this;
  128. };
  129. // Return a translated version of a string
  130. // given string args, translate each occurrence of characters in t1 with the corresponding character from t2
  131. // given array args, if the string occurs in t1, return the corresponding string from t2, else null
  132. String.prototype.translate = function translate(t1, t2)
  133. {
  134. assert(t1.constructor === t2.constructor, "translate() function requires arrays of the same type");
  135. assert(t1.length === t2.length, "translate() function requires arrays of equal size");
  136. var i;
  137. if (t1.constructor && t1.constructor.name==='String' ) {
  138. var buf = "";
  139. for (i = 0; i < this.length; i++) {
  140. var c = this.substr(i,1);
  141. var t;
  142. for (t = 0; t < t1.length; t++) {
  143. if (c === t1.substr(t,1)) {
  144. c = t2.substr(t,1);
  145. break;
  146. }
  147. }
  148. buf += c;
  149. }
  150. return buf;
  151. }
  152. if ( Array.isArray(t1.constructor) ) {
  153. for (i = 0; i < t1.length; i++) {
  154. if (t1[i]===this) {
  155. return t2[i];
  156. }
  157. }
  158. }
  159. else {
  160. assert(false, "translate() function requires arguments of type String or Array");
  161. }
  162. return null;
  163. };*/
  164. // ----- SelBlocksGlobal:
  165. /** @param {object} givenTestCase TestCase optional
  166. * @returns int 0-based index of given test case within the list of test cases
  167. * of the test suite
  168. **/
  169. var testCaseIdx= function testCaseIdx( givenTestCase ) {
  170. givenTestCase= givenTestCase || testCase;
  171. // Must not use assert() here, because that calls notifyFatalHere() which calls idxHere()
  172. // which calls globIdx() which calls testCaseIdx()
  173. if( typeof givenTestCase !=='object' ) {
  174. var msg= "SelBlocks error: in testCaseIdx(), param givenTestCase is not an object, neither global testCase is.";
  175. LOG.error( msg );
  176. throw new Error(msg);
  177. }
  178. if( editor.app.testSuite.tests.length===0 ) {
  179. var msg= "SelBlocks error: in testCaseIdx(), bad editor.app.testSuite.tests.length===0.";
  180. LOG.error( msg );
  181. throw new Error(msg);
  182. }
  183. for( var caseIndex=editor.app.testSuite.tests.length-1; caseIndex>=0; caseIndex-- ) {
  184. // If you loaded a test suite: Before the first run of a command from a test case, or the first switch to that test case, the test case, that test case's .content field is not set yet. Hence the following checks it before comparing to givenTestCase.
  185. if( 'content' in editor.app.testSuite.tests[caseIndex] && editor.app.testSuite.tests[caseIndex].content===givenTestCase ) {
  186. break;
  187. }
  188. }
  189. if( caseIndex<0 ) {
  190. var msg= "SelBlocks error: in testCaseIdx(), givenTestCase was not matched.";
  191. LOG.error( msg );
  192. throw new Error(msg);
  193. }
  194. return caseIndex;
  195. };
  196. var logAndThrow= function logAndThrow(msg) {
  197. var error= new Error(msg);
  198. LOG.error( msg+ "\n" +error.stack );
  199. throw error;
  200. };
  201. /** This serves to generate unique global identifiers for test script commands.
  202. * Results of this functions are usually values of symbols[] and other structures.
  203. * @param {number} localIndex 0-based index within givenTestCase (or within testCase).
  204. * @param {TestCase} [givenTestCase] optional; using (current) testCase by default
  205. // I'd rather use objects, but Javascript doesn't compare objects field by field
  206. // - try javascript:a={first: 1}; b={first: 1}; a==b
  207. @returns {string} global index of the command, in form testCaseIndex/localIndex
  208. */
  209. var globIdx= function globIdx( localIndex, givenTestCase) {
  210. givenTestCase= givenTestCase || testCase;
  211. // Must not use assert() here, because that calls notifyFatalHere() which calls idxHere() which calls globIdx()
  212. if( typeof localIndex !=='number' || localIndex<0 ) {
  213. logAndThrow( "SelBlocks error: in globIdx(), bad type/value of the first parameter localIndex: " +localIndex );
  214. }
  215. if( typeof givenTestCase !=='object' ) {
  216. logAndThrow( "SelBlocks error: in globIdx(), bad type of the optional second parameter givenTestCase (or global testCase)." );
  217. }
  218. var caseIndex= testCaseIdx(givenTestCase);
  219. return '' +caseIndex+ '/' +localIndex;
  220. };
  221. var shiftGlobIdx= function shiftGlobIdx( relativeShift=0, globalIndex ) {
  222. SeLiteMisc.ensureType( relativeShift, 'number', 'relativeShift' );
  223. if( globalIndex===undefined ) {
  224. return idxHere( relativeShift );
  225. }
  226. // @TODO consider expanding code here and making efficient:
  227. return globIdx( localIdx(globalIndex)+relativeShift, localCase(globalIndex) );
  228. };
  229. /** @return {number} (not a Number object) 0-based index of the respective command within its test case
  230. * @param {string} globIdxValue Global index of a test command (test step).
  231. */
  232. var localIdx= function localIdx( globIdxValue ) {
  233. // Can't use assert() here, since assert indirectly calls fmtCmdRef() which calls localIdx() - recursion
  234. SeLiteMisc.ensureType( globIdxValue, 'string', 'globIdxValue' );
  235. if( typeof globIdxValue !== 'string' ) {
  236. SeLiteMisc.fail( 'globIdxValue must be a string, but got ' +(typeof globIdxValue)+ ': ' +globIdxValue );
  237. LOG.error( msg );
  238. throw new Error(msg);
  239. }
  240. var lastSlashIndex= globIdxValue.lastIndexOf('/');
  241. if( lastSlashIndex<=0 ) {
  242. var msg= 'globIdxValue must contain "/" and not as the first character.';
  243. LOG.error( msg );
  244. throw new Error(msg);
  245. }
  246. if( lastSlashIndex>=globIdxValue.length ) {
  247. var msg= 'globIdxValue must contain "/" and not as the last character.';
  248. LOG.error( msg );
  249. throw new Error(msg);
  250. }
  251. var afterSlash= globIdxValue.substr( lastSlashIndex+1 );
  252. var afterSlashNumber= new Number( afterSlash );
  253. if( afterSlash !== ''+afterSlashNumber ) {
  254. var msg= 'The part after "/" must be numeric.';
  255. LOG.error( msg );
  256. throw new Error(msg);
  257. }
  258. var result= afterSlashNumber.valueOf();
  259. //TODO is the following same as testCases[ local... ].commands.length?
  260. //"TODO:"??
  261. if( result<0 || result>=editor.app.testSuite.tests[ localCaseIdxPart(globIdxValue) ].content.commands.length ) {
  262. var msg= 'In localIdx("' +globIdxValue+ '"), result ' +result+ ' is not a valid command index';
  263. LOG.error( msg );
  264. throw new Error(msg);
  265. }
  266. return result;
  267. };
  268. /**@param string result of globIdx() or of labelIdx()
  269. * @retu rn {number} (not a Number object) 0-based index of the test case (for the given global index)
  270. * within the list of test cases (i.e. editor.app.testSuite.tests)
  271. */
  272. var localCaseIdxPart= function localCaseIdxPart( globIdxValue ) {
  273. assert( typeof globIdxValue ==='string', 'globIdxValue must be a string.' );
  274. var lastSlashIndex= globIdxValue.lastIndexOf('/');
  275. assert( lastSlashIndex>0, 'globIdxValue must contain "/" and not as the first character.');
  276. assert( lastSlashIndex<globIdxValue.length-1, 'globIdxValue must contain "/" and not as the last character.');
  277. var beforeSlash= globIdxValue.substring( 0, globIdxValue.lastIndexOf('/') );
  278. var beforeSlashNumber= new Number( beforeSlash );
  279. assert( ''+beforeSlash===''+beforeSlashNumber, 'The part after "/" must be numeric.');
  280. var result= beforeSlashNumber.valueOf();
  281. assert( result>=0 && result<editor.app.testSuite.tests.length, 'result not a valid index into editor.app.testSuite.tests.');
  282. return result;
  283. };
  284. /** global array of _usable_ test cases, set in compileSelBlocks().
  285. * It contains test cases in the same order as in editor.app.testSuite.tests[],
  286. * but here they are as they come from editor.getTestCase()
  287. **/
  288. var testCases= [];
  289. // @return TestCase test case for the given global index
  290. var localCase= function localCase( globIdxValue ) {
  291. var index= localCaseIdxPart(globIdxValue);
  292. assert( index<testCases.length, 'case index: ' +index+ ' but testCases[] has length ' +testCases.length );
  293. return testCases[ index ];
  294. /* Following didn't work:
  295. return editor.app.testSuite.tests[ localCaseIdxPart(globIdxValue) ].content;
  296. */
  297. };
  298. /** @return {Object} Command structure for given global index
  299. * */
  300. var localCommand= function localCommand( globIdxValue ) {
  301. return localCase( globIdxValue ).commands[ localIdx(globIdxValue) ];
  302. };
  303. /** This serves to generate and compare keys in symbols[] for label commands
  304. * @param string label name
  305. * @param TestCase test case where the label is; optional - using testCase by default
  306. * @return string global label identifier in form 'test-case-index/label'
  307. **/
  308. var labelIdx= function labelIdx( label, givenTestCase ) {
  309. assert( typeof label ==='string', 'label must be a string.');
  310. givenTestCase= givenTestCase || testCase;
  311. return ''+testCaseIdx(givenTestCase)+ '/'+ label;
  312. };
  313. // @TODO on insert, validate that function names are unique, i.e. no function overriding
  314. //=============== Call/Scope Stack handling ===============
  315. /** @type {object} symbols Object {
  316. * string equal to function's name => globIdx value
  317. * string 'testCaseIndex:label-name' => globIdx value
  318. * }
  319. */
  320. var symbols = {}; // command indexes stored by name: function names
  321. /** @var {BlockDefs} Static command definitions stored by command index. Global, used for all test cases. */
  322. var blockDefs = null; // static command definitions stored by command index
  323. /** @var {Stack} callStack Command execution stack */
  324. var callStack = null; // command execution stack
  325. /** Solely for selenium-executionloop-handleAsTryBlock.js. */
  326. Selenium.prototype.callStack= function callStackFunc() {
  327. return callStack;
  328. };
  329. // the idx of the currently executing command
  330. // This function existed in SelBlocks. SelBlocksGlobal added param relativeShift and made it return a global, cross-test case index, rather than local (test-case specific) index
  331. /** @param {number} [relativeShift=0] Relative shift to the current command's position
  332. * @return {string} global command index
  333. * */
  334. var idxHere= function idxHere( relativeShift ) {
  335. // Must not use assert() here, because that calls notifyFatalHere() which calls idxHere()
  336. return globIdx( localIdxHere(relativeShift) );
  337. };
  338. /** @param {number} [relativeShift=0] Relative shift to the current command's position
  339. * @return {number} Current command's position (within current test case), adjusted by relativeShift. Depending on relativeShift the result may not be a valid position.
  340. * */
  341. var localIdxHere= function localIdxHere( relativeShift=0 ) {
  342. return testCase.debugContext.debugIndex+relativeShift;
  343. };
  344. // Command structure definitions, stored by command index
  345. // SelBlocksGlobal: stored by command global index - i.e. value of idxHere()
  346. var BlockDefs= function BlockDefs() {
  347. //@TODO use this.xxx=yyy, and define init() on BlockDefs.prototype. Then NetBeans navigation is easier.
  348. /** @var {object} Serving as an associative array {globIdx => object of {any attributes, idx, cmdName}}. SelBlocksGlobal changed this from an array to an object. */
  349. var blkDefs = {};
  350. // initialize an entry in BlockDefs instance at the given command global index
  351. /** @param {string} i Global index, a result of globIdx() function
  352. * @param {Object} [attrs] Extra details to add, depending on the command:
  353. * nature: 'if', 'try', 'loop', 'function'
  354. * elseIfIdxs - array, used for 'if'
  355. * ifIdx - used for 'else', 'elseIf' and 'endIf'; it's a global index of the matching 'if' step
  356. * name - used for 'try', it's 'target' of the step (the 2nd column in Selenium IDE)
  357. * tryIdx - used by 'catch', 'finally' and 'endTry', index of the matching 'try'
  358. * finallyIdx
  359. * beginIdx - used by 'continue', 'break', endWhile, endFor, endForEach, endForJson, endForXml;
  360. * it's an index of the start of the current loop
  361. * endIdx
  362. * funcIdx - used by 'return', 'endfunction', 'endScript'
  363. * @TODO check beginIdx and other fields - set after calls to blkDefFor(), blkDefAt()
  364. * @return {object} A new entry just added to this collection.
  365. * @see variable blkDefs
  366. **/
  367. blkDefs.init = function BlockDefsInit(i, attrs={} ) {
  368. assert( typeof testCase.commands ==='object', 'BlockDefs::init() - testCase.commands is of bad type.');
  369. // @TODO assert regex numeric/numeric
  370. assert( typeof i ==='string', 'BlockDefs::init() - param i must be a globIdx() result.');
  371. // @TODO change to use 'this' instead of 'blkDefs' - it will be clearer.
  372. blkDefs[i] = attrs;
  373. blkDefs[i].idx = i;
  374. // Following line is from classic SelBlocks, here just for documentation
  375. //blkDefs[i].cmdName = testCase.commands[i].command;
  376. blkDefs[i].cmdName = localCase(i).commands[ localIdx(i) ].command.trimLeft(); // trimLeft() is for commands indented with whitespace (when using SeLite ClipboardAndIndent)
  377. return blkDefs[i];
  378. };
  379. return blkDefs;
  380. };
  381. // retrieve the blockDef at the given command idx
  382. /** @param {string} idx Global index of a test step. */
  383. var blkDefAt= function blkDefAt(idx) {
  384. return blockDefs[idx];
  385. };
  386. // retrieve the blockDef for the currently executing command
  387. var blkDefHere= function blkDefHere() {
  388. return blkDefAt(idxHere());
  389. };
  390. // retrieve the blockDef for the given blockDef frame
  391. var blkDefFor= function blkDefFor(stackFrame) {
  392. if (!stackFrame) {
  393. return null;
  394. }
  395. return blkDefAt(stackFrame.idx);
  396. };
  397. // An Array object with stack functionality
  398. var Stack= function Stack() {
  399. var stack = [];
  400. stack.isEmpty = function isEmpty() { return stack.length === 0; };
  401. stack.top = function top() { return stack[stack.length-1]; };
  402. stack.findEnclosing = function findEnclosing(_hasCriteria) { return stack[stack.indexWhere(_hasCriteria)]; };
  403. stack.indexWhere = function indexWhere(_hasCriteria) { // undefined if not found
  404. var i;
  405. for (i = stack.length-1; i >= 0; i--) {
  406. if (_hasCriteria(stack[i])) {
  407. return i;
  408. }
  409. }
  410. };
  411. stack.unwindTo = function unwindTo(_hasCriteria) {
  412. if (stack.length === 0) {
  413. return null;
  414. }
  415. while (!_hasCriteria(stack.top())) {
  416. stack.pop();
  417. }
  418. return stack.top();
  419. };
  420. stack.isHere = function isHere() {
  421. return (stack.length > 0 && stack.top().idx === idxHere());
  422. };
  423. return stack;
  424. };
  425. // Determine if the given stack frame is one of the given block kinds
  426. Stack.isTryBlock = function isTryBlock(stackFrame) { return (blkDefFor(stackFrame).nature === "try"); };
  427. Stack.isLoopBlock = function isLoopBlock(stackFrame) { return (blkDefFor(stackFrame).nature === "loop"); };
  428. Stack.isFunctionBlock = function isFunctionBlock(stackFrame) { return (blkDefFor(stackFrame).nature === "function"); };
  429. // Flow control - we don't just alter debugIndex on the fly, because the command
  430. // preceding the destination would falsely get marked as successfully executed.
  431. // SelBLocksGlobal: This is a global index of the next command - set to a result of globIdx()
  432. var branchIdx = null;
  433. // if testCase.nextCommand() ever changes, this will need to be revisited
  434. // (current as of: selenium-ide-2.9.1)
  435. // See Selenium's {a6fd85ed-e919-4a43-a5af-8da18bda539f}/chrome/content/testCase.js
  436. // This is for a head-intercept of TestCaseDebugContext.prototype.nextCommand(), and it adds support for SelBlocksGlobal branches (across test cases).
  437. // We can't redefine/tail-intercept testCase.debugContext.nextCommand() at the time
  438. // this SelBlocksGlobal source file is loaded, because testCase is not defined yet. Therefore we do it here
  439. // on the first run of the enclosing tail intercept of Selenium.prototype.reset() below.
  440. // And we intercept do it on the prototype, so that it applies to any test cases.
  441. // Other differences to SelBlocks: no support for onServer; no return value.
  442. var nextCommand= function nextCommand() {
  443. if( testCase.calledFromAsync ) {
  444. LOG.debug( 'nextCommand: testCase.calledFromAsync' );
  445. assert( !this.started, "When using callFromAsync(), the test case must have ended - either by the last Selenese command, or by exitTest command." );
  446. this.started = true;
  447. assert( branchIdx===null, "branchIdx should be null when invoking Selenese from Javascript, but it's: " +branchIdx );
  448. // The following means that nextCommand() has a big side-effect of actually running doCall().
  449. LOG.debug( 'nextCommand() invoking doCall()');
  450. selenium.doCall( testCase.calledFromAsync.functionName, testCase.calledFromAsync.seleneseParameters, /*invokedFromJavascript*/true, testCase.calledFromAsync.onSuccess, testCase.calledFromAsync.onFailure, /*callFromAsync*/true );
  451. delete testCase['calledFromAsync'];
  452. }
  453. LOG.debug( 'SelBlocks head-intercept of TestCaseDebugContext.nextCommand()');
  454. if (!this.started) {
  455. this.started = true;
  456. // The following is as from SelBlocks, but -1, because the original nextCommand() increases it (after this head intercept).
  457. this.debugIndex = testCase.startPoint
  458. ? testCase.commands.indexOf(testCase.startPoint)-1
  459. : -1;
  460. }
  461. else {
  462. // SelBlocksGlobal hook for SeLite Bootstrap. @TODO For future: This shouldn't be here, but in testcase-debug-context. However, that would currently be a pain in the neck due to https://github.com/SeleniumHQ/selenium/issues/1537 and https://github.com/SeleniumHQ/selenium/issues/1549 (listed in ThirdPartyIssues.md).
  463. if( typeof Selenium.reloadScripts==='function' ) { // SeLite Bootstrap is loaded
  464. LOG.debug('selblocks calling Selenium.reloadScripts()');
  465. Selenium.reloadScripts();
  466. }
  467. if (branchIdx !== null) {
  468. $$.LOG.info("branch => " + fmtCmdRef(branchIdx));
  469. // Following uses -1 because the original nextCommand() will increase this.debugIndex by 1 when invoked below
  470. this.debugIndex = localIdx(branchIdx)-1;
  471. testCase= this.testCase= localCase(branchIdx);
  472. testCase.debugContext= this;
  473. branchIdx = null;
  474. }
  475. }
  476. //SelBlocksGlobal: No need to skip comments. No return value.
  477. };
  478. /**
  479. * Creates a pointer to the next command to execute. This pointer is used by
  480. * nextCommand when considering what to do next.
  481. * @param {Number} cmdIdx The index of the next command to execute.
  482. */
  483. var setNextCommand= function setNextCommand(cmdIdx) {
  484. var idx= localIdx(cmdIdx);
  485. var localTestCase= localCase(cmdIdx);
  486. // localIdx() already validated cmdIdx to be within the range of its test case's commands; hence here we're skipping the duplicate validation from classic SelBlocks.
  487. // When compared to SelBlocks, the following doesn't use cmdIdx+1, because the original nextCommand() will increase this.debugIndex by 1 when invoked below
  488. branchIdx = cmdIdx;
  489. };
  490. (function () { // wrapper makes testCaseDebugContextWasIntercepted private
  491. var testCaseDebugContextWasIntercepted; // undefined or true
  492. // Selenium calls reset():
  493. // * before each single (double-click) command execution
  494. // * before a testcase is run
  495. // * before each testcase runs in a running testsuite
  496. // TBD: skip during single command execution
  497. // SelBlocksGlobal: leaving the original indentation here, to make mergies easier:
  498. $$.fn.interceptAfter(Selenium.prototype, "reset", function resetInterceptedBySelBlocksGlobal()
  499. {
  500. $$.LOG.debug("In tail intercept :: Selenium.reset()");
  501. // SelBlocksGlobal: no support for onServer
  502. try {
  503. compileSelBlocks();
  504. }
  505. catch (err) {
  506. notifyFatalErr("In " + err.fileName + " @" + err.lineNumber + ": " + err);
  507. }
  508. callStack = new Stack();
  509. callStack.push({ blockStack: new Stack() }); // top-level execution state
  510. $$.tcf = { nestingLevel: -1 }; // try/catch/finally nesting
  511. if( testCaseDebugContextWasIntercepted===undefined ) {
  512. // customize flow control logic
  513. // SelBlocksGlobal: This is a head-intercept, rather than interceptReplace as in SelBlocks 2.0.1. (In SelBlocksGlobal it can't be a tail intercept - contrary to a comment suggestion in SelBlocks.) That's why the injected nextCommand() doesn't increase debugIndex - because the original nextCommand() does it after the injected head intercept.
  514. // SelBlocksGlobal: intercepting TestCaseDebugContext.prototype.nextCommand() rather than testCase.debugContext.nextCommand() as it was in SelBlocks.
  515. $$.LOG.debug("Configuring head intercept: TestCaseDebugContext.prototype.nextCommand()");
  516. $$.fn.interceptBefore(TestCaseDebugContext.prototype, "nextCommand", nextCommand);
  517. testCaseDebugContextWasIntercepted= true;
  518. }
  519. });
  520. }
  521. ) ();
  522. // get the blockStack for the currently active callStack
  523. var activeBlockStack= function activeBlockStack() {
  524. return callStack.top().blockStack;
  525. };
  526. // ================================================================================
  527. // Assemble block relationships and symbol locations
  528. var compileSelBlocks= function compileSelBlocks()
  529. {
  530. symbols= {}; // Let's clear symbols
  531. // Currently, this is called multiple times when Se IDE runs the whole test suite
  532. // - once per each test case. No harm in that, only a bit of wasted CPU.
  533. //alert( 'testCase===editor.suiteTreeView.getCurrentTestCase(): ' +(testCase===editor.suiteTreeView.getCurrentTestCase()) ); // --> false!
  534. //alert( 'testCase===editor.getTestCase(): ' +(testCase===editor.getTestCase()) ); //--> true!
  535. var testCaseOriginal= testCase;
  536. var testCaseOriginal= editor.getTestCase();
  537. var testCaseOriginalIndex= -1;
  538. testCases= [];
  539. //alert( 'editor.app.getTestSuite()===editor.app.testSuite: ' +editor.app.getTestSuite()===editor.app.testSuite ); // => false
  540. //alert( 'editor.app.testSuite.tests.indexOf( testCase): ' +editor.app.testSuite.tests.indexOf( testCase) ); // => -1
  541. // SelBlocksGlobal: I set blockDefs before looping through test cases, because it's global - for all test cases
  542. blockDefs = new BlockDefs();
  543. for( var testCaseIndex=0; testCaseIndex<editor.app.testSuite.tests.length; testCaseIndex++ ) {
  544. var iteratedTestCase= editor.app.getTestSuite().tests[testCaseIndex];
  545. // Following is based on editor.app.setTestCase(), which gets called by editor.app.showTestCaseFromSuite(). I don't call those functions themselves, because setTestCase() triggers testCaseChanged event, which then scrolls up the GUI list of commands, and that is disturbing.
  546. var content= iteratedTestCase.content;
  547. if( !content ) {
  548. content= editor.app._loadTestCase(
  549. iteratedTestCase.getFile(),
  550. function(test) {
  551. test.title = iteratedTestCase.getTitle(); // load title from suite
  552. iteratedTestCase.content = test;
  553. },
  554. true
  555. );
  556. }
  557. editor.app.testCase = content;
  558. var curCase= editor.getTestCase();
  559. if( curCase===testCaseOriginal ) {
  560. testCaseOriginalIndex= testCaseIndex;
  561. }
  562. assert( curCase.debugContext && curCase.debugContext.currentCommand, 'curCase.debugContext.currentCommand not present!' );
  563. testCases.push( curCase );
  564. compileSelBlocksTestCase( curCase );
  565. }
  566. assert( testCaseOriginalIndex>=0, "testCaseOriginalIndex mut be non-negative!");
  567. // In the following, do not pass testCases[testCaseOriginalIndex], since
  568. // it is not the same as editor.app.getTestSuite().tests[testCaseOriginalIndex].
  569. // See notes above for why the following sets editor.app.testCase directly, rather than through editor.app.showTestCaseFromSuite(). Also, by now the test case's .content has been loaded, so I don't re-check it here:
  570. editor.app.testCase = editor.app.getTestSuite().tests[testCaseOriginalIndex].content;
  571. testCase.debugContext.testCase= testCase;
  572. };
  573. // end of compileSelBlocks()
  574. // SelBlocksGlobal: Following three functions were inside compileSelBlocksTestCase(), but that doesn't follow JS strict mode. Two of them didn't have to be closures. assertBlockIsPending() used to be a closure, now it received parameter lexStack and is not a closure anymore.
  575. //- command validation
  576. var assertNotAndWaitSuffix= function assertNotAndWaitSuffix(cmdIdx) {
  577. assertCmd(cmdIdx, localCase(cmdIdx).commands[ localIdx(cmdIdx) ].command.indexOf("AndWait") === -1,
  578. ", AndWait suffix is not valid for SelBlocks commands");
  579. };
  580. //- active block validation
  581. var assertBlockIsPending= function assertBlockIsPending(lexStack, expectedCmd, cmdIdx, desc=", without an beginning [" + expectedCmd + "]" ) {
  582. assertCmd(cmdIdx, !lexStack.isEmpty(), desc);
  583. };
  584. //- command-pairing validation
  585. var assertMatching= function assertMatching(curCmd, expectedCmd, cmdIdx, pendIdx) {
  586. assertCmd(cmdIdx, curCmd === expectedCmd, ", does not match command " + fmtCmdRef(pendIdx));
  587. };
  588. // SelBlocksGlobal factored the following out of SelBlocks' compileSelBlocks(), to make 'testCase' not refer
  589. // to global testCase
  590. var compileSelBlocksTestCase= function compileSelBlocksTestCase( testCase ) {
  591. // SelBlocksGlobal: I set lexStack here, since it's local stack per testCase (and only used during compilation)
  592. var lexStack = new Stack();
  593. // SelBlocksGlobal: Following loop variable commandIndex was renamed from 'i' in SelBlocks.
  594. // I set variable 'i' to globIdx() value of commandIndex (i.e. of the original intended value of 'i'). This way
  595. // the classic SelBlocks code still uses variable 'i', so there are less merge conflicts.
  596. var commandIndex;
  597. for (commandIndex = 0; commandIndex < testCase.commands.length; commandIndex++)
  598. {
  599. if (testCase.commands[commandIndex].type === "command")
  600. {
  601. var curCmd = testCase.commands[commandIndex].command.trimLeft(); // trimLeft() is for commands indented with whitespace (when using SeLite ClipboardAndIndent)
  602. var aw = curCmd.indexOf("AndWait");
  603. if (aw !== -1) {
  604. // just ignore the suffix for now, this may or may not be a SelBlocks commands
  605. curCmd = curCmd.substring(0, aw);
  606. }
  607. var cmdTarget = testCase.commands[commandIndex].target;
  608. var i= globIdx(commandIndex, testCase);
  609. var ifDef;
  610. var tryDef;
  611. var expectedCmd;
  612. switch(curCmd)
  613. {
  614. case "label":
  615. assertNotAndWaitSuffix(i);
  616. symbols[ labelIdx(cmdTarget, testCase) ] = i;
  617. break;
  618. case "goto": case "gotoIf": case "skipNext":
  619. assertNotAndWaitSuffix(i);
  620. break;
  621. case "if":
  622. case "ifPromise":
  623. assertNotAndWaitSuffix(i);
  624. lexStack.push(blockDefs.init(i, { nature: curCmd, elseIfIdxs: [] }));
  625. break;
  626. case "elseIf":
  627. case "elseIfPromise"://@TODO
  628. assertNotAndWaitSuffix(i);
  629. assertBlockIsPending(lexStack, curCmd, i, ", is not valid outside of an if/endIf block");
  630. ifDef = lexStack.top();
  631. assertMatching( ifDef.cmdName,
  632. curCmd==="elseIf"
  633. ? "if"
  634. : "ifPromise",
  635. i,
  636. ifDef.idx
  637. );
  638. var eIdx = blkDefFor(ifDef).elseIdx;
  639. if (eIdx) {
  640. notifyFatal(fmtCmdRef(eIdx) + " An else/elsePromise has to come after all elseIf/elseIfPromise commands.");
  641. }
  642. blockDefs.init(i, { ifIdx: ifDef.idx }); // elseIf -> if
  643. blkDefFor(ifDef).elseIfIdxs.push(i); // if -> elseIf(s)
  644. break;
  645. case "else":
  646. case "elsePromise":
  647. assertNotAndWaitSuffix(i);
  648. var openerCommand= curCmd==="else"
  649. ? "if"
  650. : "ifPromise";
  651. assertBlockIsPending( lexStack, openerCommand, i, ", is not valid outside of an if/endIf block" );
  652. ifDef = lexStack.top();
  653. assertMatching(ifDef.cmdName, openerCommand, i, ifDef.idx);
  654. if (blkDefFor(ifDef).elseIdx) {
  655. notifyFatal(fmtCmdRef(i) + " There can only be one else associated with a given if.");
  656. }
  657. blockDefs.init(i, { ifIdx: ifDef.idx }); // else -> if
  658. blkDefFor(ifDef).elseIdx = i; // if -> else
  659. break;
  660. case "endIf":
  661. case "endIfPromise":
  662. assertNotAndWaitSuffix(i);
  663. var openerCommand= curCmd==="endIf"
  664. ? "if"
  665. : "ifPromise";
  666. assertBlockIsPending(lexStack, openerCommand, i);
  667. ifDef = lexStack.pop();
  668. assertMatching(ifDef.cmdName, openerCommand, i, ifDef.idx);
  669. blockDefs.init(i, { ifIdx: ifDef.idx }); // endIf -> if
  670. blkDefFor(ifDef).endIdx = i; // if -> endif
  671. if (ifDef.elseIdx) {
  672. blkDefAt(ifDef.elseIdx).endIdx = i; // else -> endif
  673. }
  674. break;
  675. case "try":
  676. assertNotAndWaitSuffix(i);
  677. lexStack.push(blockDefs.init(i, { nature: "try", name: cmdTarget }));
  678. break;
  679. case "catch":
  680. assertNotAndWaitSuffix(i);
  681. assertBlockIsPending(lexStack, "try", i, ", is not valid without a try block");
  682. tryDef = lexStack.top();
  683. assertMatching(tryDef.cmdName, "try", i, tryDef.idx);
  684. if (blkDefFor(tryDef).catchIdx) {
  685. notifyFatal(fmtCmdRef(i) + " There can only be one catch-block associated with a given try.");
  686. }
  687. var fIdx = blkDefFor(tryDef).finallyIdx;
  688. if (fIdx) {
  689. notifyFatal(fmtCmdRef(fIdx) + " A finally-block has to be last in a try section.");
  690. }
  691. blockDefs.init(i, { tryIdx: tryDef.idx }); // catch -> try
  692. blkDefFor(tryDef).catchIdx = i; // try -> catch
  693. break;
  694. case "finally":
  695. assertNotAndWaitSuffix(i);
  696. assertBlockIsPending(lexStack, "try", i);
  697. tryDef = lexStack.top();
  698. assertMatching(tryDef.cmdName, "try", i, tryDef.idx);
  699. if (blkDefFor(tryDef).finallyIdx) {
  700. notifyFatal(fmtCmdRef(i) + " There can only be one finally-block associated with a given try.");
  701. }
  702. blockDefs.init(i, { tryIdx: tryDef.idx }); // finally -> try
  703. blkDefFor(tryDef).finallyIdx = i; // try -> finally
  704. if (tryDef.catchIdx) {
  705. blkDefAt(tryDef.catchIdx).finallyIdx = i; // catch -> finally
  706. }
  707. break;
  708. case "endTry":
  709. assertNotAndWaitSuffix(i);
  710. assertBlockIsPending(lexStack, "try", i);
  711. tryDef = lexStack.pop();
  712. assertMatching(tryDef.cmdName, "try", i, tryDef.idx);
  713. if (cmdTarget) {
  714. assertMatching(tryDef.name, cmdTarget, i, tryDef.idx); // pair-up on try-name
  715. }
  716. blockDefs.init(i, { tryIdx: tryDef.idx }); // endTry -> try
  717. blkDefFor(tryDef).endIdx = i; // try -> endTry
  718. if (tryDef.catchIdx) {
  719. blkDefAt(tryDef.catchIdx).endIdx = i; // catch -> endTry
  720. }
  721. break;
  722. case "while": case "for": case "forEach": case "foreach": case "forIterator": case "forIterable": case "forJson": case "forXml":
  723. assertNotAndWaitSuffix(i);
  724. lexStack.push(blockDefs.init(i, { nature: "loop" }));
  725. break;
  726. case "continue": case "break":
  727. assertNotAndWaitSuffix(i);
  728. assertCmd(i, lexStack.findEnclosing(Stack.isLoopBlock), ", is not valid outside of a loop");
  729. blockDefs.init(i, { beginIdx: lexStack.top().idx }); // -> begin
  730. break;
  731. case "endWhile": case "endFor": case "endForeach": case "endForEach": case "endForIterator": case "endForIterable": case "endForJson": case "endForXml":
  732. assertNotAndWaitSuffix(i);
  733. expectedCmd = curCmd.substr(3).toLowerCase();
  734. assertBlockIsPending(lexStack, expectedCmd, i);
  735. var beginDef = lexStack.pop();
  736. assertMatching(beginDef.cmdName.toLowerCase(), expectedCmd, i, beginDef.idx);
  737. blkDefFor(beginDef).endIdx = i; // begin -> end
  738. blockDefs.init(i, { beginIdx: beginDef.idx }); // end -> begin
  739. break;
  740. case "loadJsonVars": case "loadXmlVars":
  741. assertNotAndWaitSuffix(i);
  742. break;
  743. case "call":
  744. assertNotAndWaitSuffix(i);
  745. blockDefs.init(i);
  746. break;
  747. case "function": case "script":
  748. assertNotAndWaitSuffix(i);
  749. symbols[cmdTarget] = i;
  750. lexStack.push(blockDefs.init(i, { nature: "function", name: cmdTarget }));
  751. break;
  752. case "return":
  753. assertNotAndWaitSuffix(i);
  754. assertBlockIsPending(lexStack, "function", i, ", is not valid outside of a function/endFunction block");
  755. var funcCmd = lexStack.findEnclosing(Stack.isFunctionBlock);
  756. blockDefs.init(i, { funcIdx: funcCmd.idx }); // return -> function
  757. break;
  758. case "endFunction": case "endScript":
  759. assertNotAndWaitSuffix(i);
  760. expectedCmd = curCmd.substr(3).toLowerCase();
  761. assertBlockIsPending(lexStack, expectedCmd, i);
  762. var funcDef = lexStack.pop();
  763. assertMatching(funcDef.cmdName.toLowerCase(), expectedCmd, i, funcDef.idx);
  764. if (cmdTarget) {
  765. assertMatching(funcDef.name, cmdTarget, i, funcDef.idx); // pair-up on function name
  766. }
  767. blkDefFor(funcDef).endIdx = i; // function -> endFunction
  768. blockDefs.init(i, { funcIdx: funcDef.idx }); // endFunction -> function
  769. break;
  770. case "exitTest":
  771. assertNotAndWaitSuffix(i);
  772. break;
  773. default:
  774. }
  775. }
  776. }
  777. if (!lexStack.isEmpty()) {
  778. // unterminated block(s)
  779. var cmdErrors = [];
  780. while (!lexStack.isEmpty()) {
  781. var pend = lexStack.pop();
  782. cmdErrors.unshift(fmtCmdRef(pend.idx) + " without a terminating "
  783. + "'end" + pend.cmdName.substr(0, 1).toUpperCase() + pend.cmdName.substr(1) + "'"
  784. );
  785. }
  786. throw new SyntaxError(cmdErrors.join("; "));
  787. }
  788. }; // end of compileSelBlocksTestCase()
  789. // --------------------------------------------------------------------------------
  790. // prevent jumping in-to and/or out-of loop/function/try blocks
  791. var assertIntraBlockJumpRestriction= function assertIntraBlockJumpRestriction(fromIdx, toIdx) {
  792. var fromRange = findBlockRange(fromIdx);
  793. var toRange = findBlockRange(toIdx);
  794. if (fromRange || toRange) {
  795. var msg = " Attempt to jump";
  796. if (fromRange) { msg += " out of " + fromRange.desc + fromRange.fmt(); }
  797. if (toRange) { msg += " into " + toRange.desc + toRange.fmt(); }
  798. assert(fromRange && fromRange.equals(toRange), msg
  799. + ". You cannot jump into, or out of: loops, functions, or try blocks.");
  800. }
  801. };
  802. // ascertain in which, if any, block that an locusIdx occurs
  803. var findBlockRange= function findBlockRange(locusIdx) {
  804. var idx;
  805. for (idx = locusIdx-1; idx >= 0; idx--) {
  806. var blk = blkDefAt(idx);
  807. if (blk) {
  808. if (locusIdx > blk.endIdx) { // ignore blocks that are inside this same block
  809. continue;
  810. }
  811. switch (blk.nature) {
  812. case "loop": return new CmdRange(blk.idx, blk.endIdx, blk.cmdName + " loop");
  813. case "function": return new CmdRange(blk.idx, blk.endIdx, "function '" + blk.name + "'");
  814. case "try": return isolateTcfRange(locusIdx, blk);
  815. }
  816. }
  817. }
  818. // return as undefined (no enclosing block at all)
  819. };
  820. // pin-point in which sub-block, (try, catch or finally), that the idx occurs
  821. var isolateTcfRange= function isolateTcfRange(idx, tryDef) {
  822. // assumptions: idx is known to be between try & endTry, and catch always precedes finally
  823. var RANGES = [
  824. { ifr: tryDef.finallyIdx, ito: tryDef.endIdx, desc: "finally", desc2: "end" }
  825. ,{ ifr: tryDef.catchIdx, ito: tryDef.finallyIdx, desc: "catch", desc2: "finally" }
  826. ,{ ifr: tryDef.catchIdx, ito: tryDef.endIdx, desc: "catch", desc2: "end" }
  827. ,{ ifr: tryDef.idx, ito: tryDef.catchIdx, desc: "try", desc2: "catch" }
  828. ,{ ifr: tryDef.idx, ito: tryDef.finallyIdx, desc: "try", desc2: "finally" }
  829. ,{ ifr: tryDef.idx, ito: tryDef.endIdx, desc: "try", desc2: "end" }
  830. ];
  831. var i;
  832. for (i = 0; i < RANGES.length; i++) {
  833. var rng = RANGES[i];
  834. if (rng.ifr <= idx && idx < rng.ito) {
  835. var desc = rng.desc + "-block";
  836. if (rng.desc !== "try") { desc += " for"; }
  837. if (tryDef.name) { desc += " '" + tryDef.name + "'"; }
  838. return new CmdRange(rng.ifr, rng.ito, desc);
  839. }
  840. }
  841. };
  842. // represents a range of script lines
  843. var CmdRange= function CmdRange(topIdx, bottomIdx, desc) {
  844. this.topIdx = topIdx;
  845. this.bottomIdx = bottomIdx;
  846. this.desc = desc;
  847. this.equals = function equals(cmdRange) {
  848. return (cmdRange && cmdRange.topIdx === this.topIdx && cmdRange.bottomIdx === this.bottomIdx);
  849. };
  850. this.fmt = function fmt() {
  851. return " @[" + (this.topIdx+1) + "-" + (this.bottomIdx+1) + "]";
  852. };
  853. };
  854. // ==================== SelBlocks Commands (Custom Selenium Actions) ====================
  855. var iexpr = Object.create($$.InfixExpressionParser);
  856. // SelBlocks Global doesn't use validateNames()
  857. // validate declared variable/parameter name (without $ prefix)
  858. var validateName= function validateName(name, desc) {
  859. var match = name.match(/^[a-zA-Z]\w*$/);
  860. if (!match) {
  861. notifyFatal("Invalid character(s) in " + desc + " name: '" + name + "'");
  862. }
  863. };
  864. Selenium.prototype.doLabel = function doLabel() {
  865. // noop
  866. };
  867. // SelBlocksGlobal
  868. var expandStoredVarsRegex= /\$(\w[a-zA-Z_0-9]*)/g;
  869. /** @param {string} expression
  870. * @return {string} expression, with any $xyz replaced by storedVars.xyz
  871. * */
  872. expandStoredVars= function expandStoredVars( expression ) {
  873. return expression.replace( expandStoredVarsRegex, 'storedVars.$1' );
  874. };
  875. // Skip the next N commands (default is 1)
  876. Selenium.prototype.doSkipNext = function doSkipNext(spec)
  877. {
  878. assertRunning();
  879. var n = parseInt(this.evalWithExpandedStoredVars(spec), 10);
  880. if (isNaN(n)) {
  881. if (spec.trim() === "") { n = 1; }
  882. else { notifyFatalHere(" Requires a numeric value"); }
  883. }
  884. else if (n < 0) {
  885. notifyFatalHere(" Requires a number > 1");
  886. }
  887. if (n !== 0) { // if n=0, execute the next command as usual
  888. var destIdx = globIdx(localIdxHere()+n+1);
  889. assertIntraBlockJumpRestriction(localIdxHere(), localIdxHere()+n+1);
  890. setNextCommand(destIdx);
  891. }
  892. };
  893. Selenium.prototype.doGoto = function doGoto(label)
  894. {
  895. assertRunning();
  896. var symbolIndex= labelIdx(label);
  897. assert(symbols[symbolIndex]!==undefined, " Target label '" + label + "' is not found.");
  898. assertIntraBlockJumpRestriction(localIdxHere(), localIdx(symbols[symbolIndex]));
  899. setNextCommand(symbols[symbolIndex]);
  900. };
  901. Selenium.prototype.doGotoIf = function doGotoIf(condExpr, label)
  902. {
  903. assertRunning();
  904. if (this.evalWithExpandedStoredVars(condExpr))
  905. this.doGoto(label);
  906. };
  907. // ================================================================================
  908. /* Some control flow commands have Promise-based and non-Promise-based alternatives. Their common implementation is in Selenium.prototype.actionXyz(). Technically Selenium.prototype.actionXyz could be the same as Selenium.prototype.doXyz with an optional second parameter 'withPromise'. However, a user could call action 'xyz' with a second Selenese parameter by mistake, and this would not report it. Hence this separation, which enables more precise validation.
  909. */
  910. Selenium.prototype.actionIf = function actionIf(condExpr, withPromise=false)
  911. {
  912. assertRunning();
  913. var ifDef = blkDefHere();
  914. var ifState = { idx: idxHere(), elseIfItr: arrayIterator(ifDef.elseIfIdxs), withPromise };
  915. activeBlockStack().push(ifState);
  916. return this.cascadeElseIf(ifState, condExpr, withPromise);//@TODO pass withPromise and handle deferred - add decoration w/ timeout
  917. };
  918. Selenium.prototype.doIf = function doIf(condExpr) {
  919. this.actionIf( condExpr );
  920. };
  921. Selenium.prototype.doIfPromise = function doIfPromise(condExpr) {
  922. return this.actionIf( condExpr, true );
  923. };
  924. Selenium.prototype.actionElseIf = function actionElseIf(condExpr, withPromise=false)
  925. {
  926. assertRunning();
  927. assertActiveScope(blkDefHere().ifIdx);
  928. var ifState = activeBlockStack().top();
  929. ifState.withPromise===withPromise || SeLiteMisc.fail( "Please use 'if' with 'elseIf' and 'ifPromise' with 'elseIfPromise'." );
  930. if (ifState.skipElseBlocks) { // if, or previous elseIf, has already been met
  931. setNextCommand(blkDefAt(blkDefHere().ifIdx).endIdx);
  932. }
  933. else {
  934. return this.cascadeElseIf(ifState, condExpr, withPromise);
  935. }
  936. };
  937. Selenium.prototype.doElseIf = function doElseIf(condExpr) {
  938. this.actionElseIf( condExpr );
  939. };
  940. Selenium.prototype.doElseIfPromise= function doElseIfPromise( condExpr ) {
  941. return this.actionElseIf( condExpr, true );
  942. };
  943. Selenium.prototype.actionElse = function actionElse( withPromise=false )
  944. {
  945. assertRunning();
  946. assertActiveScope(blkDefHere().ifIdx);
  947. var ifState = activeBlockStack().top();
  948. ifState.withPromise===withPromise || SeLiteMisc.fail( "Please use 'if' with 'else' and 'ifPromise' with 'elsePromise'." );
  949. if (ifState.skipElseBlocks) { // if, or previous elseIf, has already been met
  950. setNextCommand(blkDefHere().endIdx);
  951. }
  952. // else continue into else-block
  953. };
  954. Selenium.prototype.doElse = function doElse() {
  955. this.actionElse();
  956. };
  957. Selenium.prototype.doElsePromise = function doElsePromise() {
  958. this.actionElse( true );
  959. };
  960. Selenium.prototype.actionEndIf = function actionEndIf( withPromise=false ) {
  961. assertRunning();
  962. assertActiveScope(blkDefHere().ifIdx);
  963. var ifState = activeBlockStack().top();
  964. ifState.withPromise===withPromise || SeLiteMisc.fail( "Please use 'if' with 'endIf' and 'ifPromise' with 'endIfPromise'." );
  965. activeBlockStack().pop();
  966. // fall out of if-endIf
  967. };
  968. Selenium.prototype.doEndIf = function doEndIf() {
  969. this.actionEndIf();
  970. };
  971. Selenium.prototype.doEndIfPromise = function doEndIfPromise() {
  972. this.actionEndIf( true );
  973. };
  974. /** @return {undefined|function} Return a function exactly when withPromise==true. Otherwise return undefined.
  975. * */
  976. Selenium.prototype.cascadeElseIf= function cascadeElseIf(ifState, condExpr, withPromise=false ) {
  977. this.assertCompilable("", condExpr, ";", "Invalid condition");
  978. var promiseOrResult= this.evalWithExpandedStoredVars(condExpr);
  979. return this.handlePotentialPromise(
  980. promiseOrResult,
  981. (value) => {
  982. if( !value ) {
  983. // jump to next elseIf or else or endif
  984. var ifDef = blkDefFor(ifState);
  985. if (ifState.elseIfItr.hasNext()) { setNextCommand(ifState.elseIfItr.next()); }
  986. else if (ifDef.elseIdx) { setNextCommand(ifDef.elseIdx); }
  987. else { setNextCommand(ifDef.endIdx); }
  988. }
  989. else {
  990. ifState.skipElseBlocks = true;
  991. // continue into if/elseIf block
  992. }
  993. },
  994. withPromise
  995. );
  996. };
  997. // ================================================================================
  998. // throw the given Error
  999. Selenium.prototype.doThrow = function doThrow(err) {
  1000. err = this.evalWithExpandedStoredVars(err);
  1001. // @TODO consider using SeLiteMisc.isInstance().
  1002. if (!(err instanceof Error)) {
  1003. err = new SelblocksError(idxHere(), err);
  1004. }
  1005. throw err;
  1006. };
  1007. // TBD: failed locators/timeouts/asserts ?
  1008. Selenium.prototype.doTry = function doTry(tryName)
  1009. {
  1010. assertRunning();
  1011. var tryState = { idx: idxHere(), name: tryName };
  1012. activeBlockStack().push(tryState);
  1013. var tryDef = blkDefHere();
  1014. if (!tryDef.catchIdx && !tryDef.finallyIdx) {
  1015. $$.LOG.warn(fmtCurCmd() + " does not have a catch-block nor a finally-block, and therefore serves no purpose");
  1016. if ($$.tcf.nestingLevel === -1) {
  1017. return; // continue into try-block without any special error handling
  1018. }
  1019. }
  1020. // log an advisory about the active catch block
  1021. if (tryDef.catchIdx) {
  1022. var errDcl = localCommand( tryDef.catchIdx ).target;
  1023. $$.LOG.debug(tryName + " catchable: " + (errDcl || "ANY"));
  1024. }
  1025. $$.tcf.nestingLevel++;
  1026. tryState.execPhase = "trying";
  1027. if ($$.tcf.nestingLevel === 0) {
  1028. // enable special command handling
  1029. var self= this;
  1030. // Original SelBlocks overrode resume() on $$.seleniumTestRunner.currentTest.
  1031. $$.fn.interceptPush(editor, "testLoopResumeHandleFailedResult", $$.testLoopResumeHandleFailedResult );
  1032. // Override testLoopResumeHandleFailedResult first and testLoopResumeHandleError second, because the overriden testLoopResumeHandleError() expects the top intercepted function to be itself, so it can call $$.fn.getInterceptTop().attrs.manageError(e).
  1033. $$.fn.interceptPush(editor, "testLoopResumeHandleError",
  1034. $$.testLoopResumeHandleError, {
  1035. manageError: function manageError(err) {
  1036. return self.handleCommandError(err);
  1037. }
  1038. });
  1039. }
  1040. $$.LOG.debug("++ try nesting: " + $$.tcf.nestingLevel);
  1041. // continue into try-block
  1042. };
  1043. Selenium.prototype.doCatch = function doCatch()
  1044. {
  1045. assertRunning();
  1046. assertActiveScope(blkDefHere().tryIdx);
  1047. var tryState = activeBlockStack().top();
  1048. if (tryState.execPhase !== "catching") {
  1049. // skip over unused catch-block
  1050. var tryDef = blkDefFor(tryState);
  1051. if (tryDef.finallyIdx) {
  1052. setNextCommand(tryDef.finallyIdx);
  1053. }
  1054. else {
  1055. setNextCommand(tryDef.endIdx);
  1056. }
  1057. }
  1058. $$.LOG.debug("entering catch block");
  1059. // else continue into catch-block
  1060. };
  1061. Selenium.prototype.doFinally = function doFinally() {
  1062. assertRunning();
  1063. assertActiveScope(blkDefHere().tryIdx);
  1064. delete storedVars._error;
  1065. $$.LOG.debug("entering finally block");
  1066. // continue into finally-block
  1067. };
  1068. Selenium.prototype.doEndTry = function doEndTry(tryName)
  1069. {
  1070. assertRunning();
  1071. assertActiveScope(blkDefHere().tryIdx);
  1072. delete storedVars._error;
  1073. var tryState = activeBlockStack().pop();
  1074. if (tryState.execPhase) { // ie, it DOES have a catch and/or a finally block
  1075. $$.tcf.nestingLevel--;
  1076. $$.LOG.debug("-- try nesting: " + $$.tcf.nestingLevel);
  1077. if ($$.tcf.nestingLevel < 0) {
  1078. // discontinue try-block handling
  1079. $$.fn.interceptPop(); // Fpr testLoopResumeHandleError
  1080. $$.fn.interceptPop(); // For testLoopResumeHandleFailedResult
  1081. // $$.tcf.bubbling = null;
  1082. }
  1083. if ($$.tcf.bubbling) {
  1084. this.reBubble();
  1085. }
  1086. else {
  1087. $$.LOG.debug("no bubbling in process");
  1088. }
  1089. }
  1090. var tryDef = blkDefFor(tryState);
  1091. $$.LOG.debug("end of try '" + tryDef.name + "'");
  1092. // fall out of endTry
  1093. };
  1094. // --------------------------------------------------------------------------------
  1095. // alter the behavior of Selenium error handling
  1096. // returns true if error is being managed
  1097. Selenium.prototype.handleCommandError= function handleCommandError(err)
  1098. {
  1099. var tryState = bubbleToTryBlock(Stack.isTryBlock);
  1100. var tryDef;
  1101. if( tryState ) {
  1102. if( tryState.stateFromAsync ) {
  1103. LOG.debug( 'handleCommandError(): stateFromAsync' );
  1104. $$.tcf.bubbling = null;
  1105. return false;
  1106. }
  1107. tryDef = blkDefFor(tryState);
  1108. $$.LOG.debug("error encountered while: " + tryState.execPhase);
  1109. if (hasUnspentCatch(tryState)) {
  1110. if (this.isMatchingCatch(err, tryDef.catchIdx)) {
  1111. // an expected kind of error has been caught
  1112. $$.LOG.info("@" + (idxHere(+1)) + ", error has been caught" + fmtCatching(tryState));
  1113. tryState.hasCaught = true;
  1114. tryState.execPhase = "catching";
  1115. storedVars._error = err;
  1116. $$.tcf.bubbling = null;
  1117. setNextCommand(tryDef.catchIdx);
  1118. return true;
  1119. }
  1120. }
  1121. }
  1122. // error not caught .. instigate bubbling
  1123. $$.LOG.debug("error not caught, bubbling error: '" + err.message + "'");
  1124. $$.tcf.bubbling = { mode: "error", error: err, srcIdx: idxHere() };
  1125. if (tryState ) {
  1126. if( hasUnspentFinally(tryState)) {
  1127. $$.LOG.info("Bubbling suspended while finally block runs");
  1128. tryState.execPhase = "finallying";
  1129. tryState.hasFinaled = true;
  1130. setNextCommand(tryDef.finallyIdx);
  1131. return true;
  1132. }
  1133. if ($$.tcf.nestingLevel > 0) {
  1134. $$.LOG.info("No further handling, error bubbling will continue outside of this try.");
  1135. setNextCommand(tryDef.endIdx);
  1136. return true;
  1137. }
  1138. }
  1139. $$.LOG.info("No handling provided in this try section for this error: '" + err.message + "'");
  1140. return false; // stop test
  1141. };
  1142. // execute any enclosing finally block(s) until reaching the given type of enclosing block
  1143. Selenium.prototype.bubbleCommand= function bubbleCommand(cmdIdx, _isContextBlockType)
  1144. {
  1145. var self= this;
  1146. //- determine if catch matches an error, or there is a finally, or the ceiling block has been reached
  1147. var isTryWithMatchingOrFinally= function isTryWithMatchingOrFinally(stackFrame) {
  1148. if (_isContextBlockType && _isContextBlockType(stackFrame)) {
  1149. return;
  1150. }
  1151. if ($$.tcf.bubbling && $$.tcf.bubbling.mode === "error" && hasUnspentCatch(stackFrame)) {
  1152. var tryDef = blkDefFor(stackFrame);
  1153. if (self.isMatchingCatch($$.tcf.bubbling.error, tryDef.catchIdx)) {
  1154. return;
  1155. }
  1156. }
  1157. return hasUnspentFinally(stackFrame);
  1158. };
  1159. var tryState = bubbleToTryBlock(isTryWithMatchingOrFinally);
  1160. var tryDef = blkDefFor(tryState);
  1161. $$.tcf.bubbling = { mode: "command", srcIdx: cmdIdx, _isStopCriteria: _isContextBlockType };
  1162. if (hasUnspentFinally(tryState)) {
  1163. $$.LOG.info("Command " + fmtCmdRef(cmdIdx) + ", suspended while finally block runs");
  1164. tryState.execPhase = "finallying";
  1165. tryState.hasFinaled = true;
  1166. setNextCommand(tryDef.finallyIdx);
  1167. // begin finally block
  1168. }
  1169. else {
  1170. $$.LOG.info("No further handling, bubbling continuing outside of this try.");
  1171. setNextCommand(tryDef.endIdx);
  1172. // jump out of try section
  1173. }
  1174. };
  1175. //- error message matcher
  1176. Selenium.prototype.isMatchingCatch= function isMatchingCatch(e, catchIdx) {
  1177. var errDcl = localCommand( catchIdx ).target;
  1178. if (!errDcl) {
  1179. return true; // no error specified means catch all errors
  1180. }
  1181. var errExpr = this.evalWithExpandedStoredVars(errDcl);
  1182. var errMsg = e.message;
  1183. if (errExpr.constructor && errExpr.constructor.name==='RegExp') {
  1184. return (errMsg.match(errExpr));
  1185. }
  1186. return (errMsg.indexOf(errExpr) !== -1);
  1187. };
  1188. // SelBlocksGlobal:
  1189. /** @param {object} callFrame
  1190. * @param {bool} [ignoreReturnIdx=false] If true, then this doesn't expect & doesn't process callFrame.returnIdx. To be used in returnFromFunction for frameFromAsync.
  1191. * */
  1192. var restoreCallFrame= function restoreCallFrame( callFrame, ignoreReturnIdx=false ) {
  1193. var _result= storedVars._result;
  1194. storedVars= callFrame.savedVars;
  1195. storedVars._result= _result;
  1196. testCase= callFrame.testCase;
  1197. testCase.debugContext.testCase= testCase;
  1198. ignoreReturnIdx || (testCase.debugContext.debugIndex= localIdx( callFrame.returnIdx ) );
  1199. restoreVarState(callFrame.savedVars);
  1200. };
  1201. // unwind the blockStack, and callStack (ie, aborting functions), until reaching the given criteria
  1202. /** @return {null|false} if there is no appropriate try block. Return trystate object. */
  1203. var bubbleToTryBlock= function bubbleToTryBlock(_hasCriteria) {
  1204. if ($$.tcf.nestingLevel < 0) {
  1205. $$.LOG.warn("bubbleToTryBlock() called outside of any try nesting");
  1206. }
  1207. var callFrame = callStack.top();
  1208. var tryState = unwindToBlock(_hasCriteria);
  1209. if( !tryState && callFrame.frameFromAsync ) {
  1210. LOG.debug('bubbleToTryBlock(): frameFromAsync. Popping callStack');
  1211. callStack.pop();
  1212. selenium.invokedFromAsync= false;
  1213. return {stateFromAsync: true};
  1214. }
  1215. while (!tryState && $$.tcf.nestingLevel > -1 && callStack.length > 1) {
  1216. LOG.warn( 'bubbleToTryBlock: popping callStack from within while() loop.');
  1217. callFrame = callStack.pop();
  1218. restoreCallFrame( callFrame );
  1219. $$.LOG.info("function '" + callFrame.name + "' aborting due to error");
  1220. tryState = unwindToBlock(_hasCriteria);
  1221. if( !tryState && callFrame.frameFromAsync ) {
  1222. LOG.warn('bubbleToTryBlock: deeper level invokedFromJavascript. popping callStack');
  1223. callStack.pop();
  1224. selenium.invokedFromAsync= false;
  1225. return {stateFromAsync: true};
  1226. }
  1227. }
  1228. return tryState;
  1229. };
  1230. // unwind the blockStack until reaching the given criteria
  1231. var unwindToBlock= function unwindToBlock(_hasCriteria) {
  1232. var tryState = activeBlockStack().unwindTo(_hasCriteria);
  1233. if (tryState) {
  1234. $$.LOG.debug("unwound to: " + fmtTry(tryState));
  1235. }
  1236. return tryState;
  1237. };
  1238. // resume or conclude command/error bubbling
  1239. Selenium.prototype.reBubble= function reBubble() {
  1240. if ($$.tcf.bubbling.mode === "error") {
  1241. if ($$.tcf.nestingLevel > -1) {
  1242. $$.LOG.debug("error-bubbling continuing...");
  1243. this.handleCommandError($$.tcf.bubbling.error);
  1244. }
  1245. else {
  1246. $$.LOG.error("Error was not caught: '" + $$.tcf.bubbling.error.message + "'");
  1247. try { throw $$.tcf.bubbling.error; }
  1248. finally { $$.tcf.bubbling = null; }
  1249. }
  1250. }
  1251. else { // mode === "command"
  1252. if (isBubblable()) {
  1253. $$.LOG.debug("command-bubbling continuing...");
  1254. this.bubbleCommand($$.tcf.bubbling.srcIdx, $$.tcf.bubbling._isStopCriteria);
  1255. }
  1256. else {
  1257. $$.LOG.info("command-bubbling complete - suspended command executing now " + fmtCmdRef($$.tcf.bubbling.srcIdx));
  1258. setNextCommand($$.tcf.bubbling.srcIdx);
  1259. $$.tcf.bubbling = null;
  1260. }
  1261. }
  1262. };
  1263. // instigate or transform bubbling, as appropriate
  1264. Selenium.prototype.transitionBubbling= function transitionBubbling(_isContextBlockType)
  1265. {
  1266. if ($$.tcf.bubbling) { // transform bubbling
  1267. if ($$.tcf.bubbling.mode === "error") {
  1268. $$.LOG.debug("Bubbling error: '" + $$.tcf.bubbling.error.message + "'"
  1269. + ", replaced with command " + fmtCmdRef(idxHere()));
  1270. $$.tcf.bubbling = { mode: "command", srcIdx: idxHere(), _isStopCriteria: _isContextBlockType };
  1271. return true;
  1272. }
  1273. // mode === "command"
  1274. $$.LOG.debug("Command suspension " + fmtCmdRef($$.tcf.bubbling.srcIdx)
  1275. + ", replaced with " + fmtCmdRef(idxHere()));
  1276. $$.tcf.bubbling.srcIdx = idxHere();
  1277. return true;
  1278. }
  1279. if (isBubblable(_isContextBlockType)) { // instigate bubbling
  1280. this.bubbleCommand(idxHere(), _isContextBlockType);
  1281. return true;
  1282. }
  1283. // no change to bubbling
  1284. return false;
  1285. };
  1286. // determine if bubbling is possible from this point outward
  1287. var isBubblable= function isBubblable(_isContextBlockType) {
  1288. var canBubble = ($$.tcf.nestingLevel > -1);
  1289. if (canBubble) {
  1290. var blkState = activeBlockStack().findEnclosing(
  1291. //- determine if stackFrame is a try-block or the given type of block
  1292. function isTryOrContextBlockType(stackFrame) {
  1293. if (_isContextBlockType && _isContextBlockType(stackFrame)) {
  1294. return true;
  1295. }
  1296. return Stack.isTryBlock(stackFrame);
  1297. }
  1298. );
  1299. return (blkDefFor(blkState).nature === "try");
  1300. }
  1301. return canBubble;
  1302. };
  1303. var hasUnspentCatch= function hasUnspentCatch(tryState) {
  1304. return (tryState && blkDefFor(tryState) && blkDefFor(tryState).catchIdx && !tryState.hasCaught);
  1305. };
  1306. var hasUnspentFinally= function hasUnspentFinally(tryState) {
  1307. return (tryState && blkDefFor(tryState) && blkDefFor(tryState).finallyIdx && !tryState.hasFinaled);
  1308. };
  1309. var fmtTry= function fmtTry(tryState)
  1310. {
  1311. var tryDef = blkDefFor(tryState);
  1312. return (
  1313. (tryDef.name ? "try '" + tryDef.name + "' " : "")
  1314. + "@" + (tryState.idx+1)
  1315. + ", " + tryState.execPhase + ".."
  1316. + " " + $$.tcf.nestingLevel + "n"
  1317. );
  1318. };
  1319. var fmtCatching= function fmtCatching(tryState)
  1320. {
  1321. if (!tryState) {
  1322. return "";
  1323. }
  1324. var bbl = "";
  1325. if ($$.tcf.bubbling) {
  1326. bbl = "@" + ($$.tcf.bubbling.srcIdx+1) + " ";
  1327. }
  1328. var tryDef = blkDefFor(tryState);
  1329. var catchDcl = localCommand( tryDef.catchIdx ).target;
  1330. return " :: " + bbl + catchDcl;
  1331. };
  1332. // ================================================================================
  1333. Selenium.prototype.actionWhile = function actionWhile( condExpr, withPromise=false )
  1334. {
  1335. var self= this;
  1336. return enterLoop(
  1337. function doWhileValidate() { // validate
  1338. assert(condExpr, " 'while' requires a condition expression.");
  1339. self.assertCompilable("", condExpr, ";", "Invalid condition");
  1340. return null;
  1341. }
  1342. ,function doWhileInitialize() { } // initialize
  1343. ,function doWhileContinueCheck() { return self.evalWithExpandedStoredVars(condExpr); } // continue?
  1344. ,function doWhileIterate() { } // iterate
  1345. ,withPromise
  1346. );
  1347. };
  1348. Selenium.prototype.doWhile = function doWhile( condExpr ) {
  1349. return this.actionWhile( condExpr );
  1350. };
  1351. Selenium.prototype.doWhilePromise = function doWhilePromise( condExpr ) {
  1352. return this.actionWhile( condExpr, true );
  1353. };
  1354. Selenium.prototype.doEndWhile = Selenium.prototype.doEndWhilePromise = function doEndWhile() {
  1355. iterateLoop();
  1356. };
  1357. // ================================================================================
  1358. Selenium.prototype.doFor = function doFor(forSpec)
  1359. {
  1360. var self= this;
  1361. enterLoop(
  1362. function doForValidate(loop) { // validate
  1363. assert(forSpec, " 'for' requires: <initial-val>; <condition>; <iter-stmt>.");
  1364. self.assertCompilable("for ( ", forSpec, " );", "Invalid loop parameters");
  1365. var specs = iexpr.splitList(forSpec, ";");
  1366. assert(specs.length === 3, " 'for' requires <init-stmt>; <condition>; <iter-stmt>.");
  1367. loop.initStmt = specs[0];
  1368. loop.condExpr = specs[1];
  1369. loop.iterStmt = specs[2];
  1370. var localVarNames = [];
  1371. // Part of the following comes from classic SelBlocks' parseVarNames(), which was removed in SelBlocksGlobal.
  1372. if (loop.initStmt) {
  1373. var vInits = iexpr.splitList( loop.initStmt, "," );
  1374. for ( var i = 0; i < vInits.length; i++) { //@TODO for(.. of..) loop once NetBeans support it.
  1375. var vInit = iexpr.splitList( vInits[i], "=" );
  1376. var variableName= vInit[0];
  1377. variableName.length>1 && variableName[0]==='$' || notifyFatal( "For loop's "+(i+1)+ 'th variable name must start with $ and have at least one character right of $.' );
  1378. variableName= variableName.substring( 1 ); // Remove the leading dollar $
  1379. validateName( variableName, 'For loop ' +(i+1)+ 'th variable name' );
  1380. localVarNames.push( variableName );
  1381. }
  1382. }
  1383. $$.LOG.debug("localVarNames: " + localVarNames.join(','));
  1384. return localVarNames;
  1385. }
  1386. ,function doForInitialize(loop) { self.evalWithExpandedStoredVars(loop.initStmt); } // initialize
  1387. ,function doForContinueCheck(loop) { return self.evalWithExpandedStoredVars(loop.condExpr); } // continue?
  1388. ,function doForIterate(loop) { self.evalWithExpandedStoredVars(loop.iterStmt); } // iterate
  1389. );
  1390. };
  1391. Selenium.prototype.doEndFor = function doEndFor() {
  1392. iterateLoop( true );
  1393. };
  1394. // ================================================================================
  1395. Selenium.prototype.doForEach = function doForEach(varName, valueExpr)
  1396. {
  1397. var self= this;
  1398. enterLoop(
  1399. function doForEachValidate(loop) { // validate
  1400. //@TODO accept an array object, too
  1401. assert(varName, " 'forEach' requires a variable name.");
  1402. assert(valueExpr, " 'forEach' requires comma-separated values.");
  1403. self.assertCompilable("[ ", valueExpr, " ];", "Invalid value list");
  1404. loop.values = self.evalWithExpandedStoredVars("[" + valueExpr + "]");
  1405. if (loop.values.length === 1 && Array.isArray(loop.values[0])) {
  1406. loop.values = loop.values[0]; // if sole element is an array, than use it
  1407. }
  1408. return [varName, "_i"];
  1409. }
  1410. ,function doForEachInitialize(loop) { loop.i = 0; storedVars[varName] = loop.values[loop.i]; } // initialize
  1411. ,function doForEachContinue(loop) { storedVars._i = loop.i; return (loop.i < loop.values.length);} // continue?
  1412. ,function doForEachIterate(loop) { // iterate
  1413. if (++(loop.i) < loop.values.length) {
  1414. storedVars[varName] = loop.values[loop.i];
  1415. }
  1416. }
  1417. );
  1418. };
  1419. Selenium.prototype.doEndForEach = function doEndForEach() {
  1420. iterateLoop();
  1421. };
  1422. Selenium.prototype.doForeach= Selenium.prototype.doForEach;
  1423. Selenium.prototype.doEndForeach = Selenium.prototype.doEndForEach;
  1424. // ================================================================================
  1425. /** @param {string} varName
  1426. * @param {function} extraValidationAndIterator Extra validation to run. No parameters. It must return an iterator object (not iterable, neither GeneratorFunction).
  1427. * */
  1428. Selenium.prototype.actionForIteratorObject = function actionForIteratorObject( varName, extraValidationAndIterator )
  1429. {
  1430. var self= this;
  1431. enterLoop(
  1432. function doForIteratorValidate(loop) { // validate
  1433. //@TODO accept an array object, too
  1434. assert(varName, " 'forIterator' and its alternatives require a variable name.");
  1435. loop.iterator= extraValidationAndIterator();
  1436. return [varName];
  1437. }
  1438. ,function doForIteratorInitialize(loop) {}
  1439. ,function doForIteratorContinue(loop) {
  1440. var step= loop.iterator.next();
  1441. if( !step.done ) {
  1442. storedVars[varName]= step.value;
  1443. }
  1444. // When finished, we leave the last value in varName - similar to JS for(..of..)
  1445. return !step.done;
  1446. }
  1447. ,function doForIteratorIterate(loop) {}
  1448. );
  1449. };
  1450. Selenium.prototype.doForIterator = function doForIterator( varName, iteratorExpr/* A Javascript expression that evaluates to an iterator object (not iterable, neither GeneratorFunction). */ )
  1451. {
  1452. this.actionForIteratorObject( varName, () => {
  1453. this.assertCompilable( '', iteratorExpr, ';', "Invalid iterator expression.");
  1454. return this.evalWithExpandedStoredVars( iteratorExpr );
  1455. } );
  1456. };
  1457. Selenium.prototype.doEndForIterator = function doEndForIterator() {
  1458. iterateLoop();
  1459. };
  1460. Selenium.prototype.doForIterable = function doForIterable( varName, iterableExpr/* A Javascript expression that evaluates to an iterable object (not an iterator, neither GeneratorFunction). */ )
  1461. {
  1462. this.actionForIteratorObject( varName, () => {
  1463. this.assertCompilable( '', iterableExpr, ';', "Invalid iterable expression.");
  1464. return this.evalWithExpandedStoredVars( iterableExpr )[Symbol.iterator]();
  1465. } );
  1466. };
  1467. Selenium.prototype.doEndForIterable = function doEndForIterable() {
  1468. iterateLoop();
  1469. };
  1470. // ================================================================================
  1471. Selenium.prototype.doLoadJsonVars = function doLoadJsonVars(filepath, selector)
  1472. {
  1473. assert(filepath, " Requires a JSON file path or URL.");
  1474. var jsonReader = new JSONReader(filepath);
  1475. this.loadVars(jsonReader, "JSON object", filepath, selector);
  1476. };
  1477. Selenium.prototype.doLoadXmlVars = function doLoadXmlVars(filepath, selector)
  1478. {
  1479. assert(filepath, " Requires an XML file path or URL.");
  1480. var xmlReader = new XmlReader(filepath);
  1481. this.loadVars(xmlReader, "XML element", filepath, selector);
  1482. };
  1483. Selenium.prototype.doLoadVars = function doLoadVars(filepath, selector)
  1484. {
  1485. $$.LOG.warn("The loadVars command has been deprecated as of SelBlocks 2.0.2 and will be removed in future releases."
  1486. + " Please use loadXmlVars instead.");
  1487. Selenium.prototype.doLoadXmlVars(filepath, selector);
  1488. };
  1489. Selenium.prototype.loadVars= function loadVars(reader, desc, filepath, selector)
  1490. {
  1491. if (selector) {
  1492. this.assertCompilable("", selector, ";", "Invalid selector condition");
  1493. }
  1494. reader.load(filepath);
  1495. reader.next(); // read first varset and set values on storedVars
  1496. if (!selector && !reader.EOF()) {
  1497. notifyFatalHere(" Multiple " + desc + "s are not valid for this command."
  1498. + ' (A specific ' + desc + ' can be selected by specifying: name="value".)');
  1499. }
  1500. var result = this.evalWithExpandedStoredVars(selector);
  1501. if (typeof result !== "boolean") {
  1502. notifyFatalHere(", " + selector + " is not a boolean expression");
  1503. }
  1504. // read until specified set found
  1505. var isEof = reader.EOF();
  1506. while (!isEof && this.evalWithExpandedStoredVars(selector) !== true) {
  1507. reader.next(); // read next varset and set values on storedVars
  1508. isEof = reader.EOF();
  1509. }
  1510. if (!this.evalWithExpandedStoredVars(selector)) {
  1511. notifyFatalHere(desc + " not found for selector expression: " + selector
  1512. + "; in input file " + filepath);
  1513. }
  1514. };
  1515. // ================================================================================
  1516. Selenium.prototype.doForJson = function doForJson(jsonpath)
  1517. {
  1518. enterLoop(
  1519. function doForJsonValidate(loop) { // validate
  1520. assert(jsonpath, " Requires a JSON file path or URL.");
  1521. loop.jsonReader = new $$.fn.JSONReader();
  1522. var localVarNames = loop.jsonReader.load(jsonpath);
  1523. return localVarNames;
  1524. }
  1525. ,function doForJsonInitialize() { } // initialize
  1526. ,function doForJsonContinue(loop) { // continue?
  1527. var isEof = loop.jsonReader.EOF();
  1528. if (!isEof) { loop.jsonReader.next(); }
  1529. return !isEof;
  1530. }
  1531. ,function doForJsonIterate() { }
  1532. );
  1533. };
  1534. Selenium.prototype.doEndForJson = function doEndForJson() {
  1535. iterateLoop();
  1536. };
  1537. Selenium.prototype.doForXml = function doForXml(xmlpath)
  1538. {
  1539. enterLoop(
  1540. function doForXmlValidate(loop) { // validate
  1541. assert(xmlpath, " 'forXml' requires an XML file path or URL.");
  1542. loop.xmlReader = new $$.fn.XmlReader();
  1543. var localVarNames = loop.xmlReader.load(xmlpath);
  1544. return localVarNames;
  1545. }
  1546. ,function doForXmlInitialize() { } // initialize
  1547. ,function doForXmlContinue(loop) { // continue?
  1548. var isEof = loop.xmlReader.EOF();
  1549. if (!isEof) { loop.xmlReader.next(); }
  1550. return !isEof;
  1551. }
  1552. ,function doForXmlIterate() { }
  1553. );
  1554. };
  1555. Selenium.prototype.doEndForXml = function doEndForXml() {
  1556. iterateLoop();
  1557. };
  1558. // --------------------------------------------------------------------------------
  1559. // Note: Selenium variable expansion occurs before command processing, therefore we re-execute
  1560. // commands that *may* contain ${} variables. Bottom line, we can't just keep a copy
  1561. // of parameters and then iterate back to the first command inside the body of a loop.
  1562. /** Only parameter _condFunc can return a Promise, which indicates potential deferral. For that pass withPromise=true.
  1563. * Other three parameters that are functions don't support Promise deferral.
  1564. * @return {undefined|function} Return a function for whilePromise loop, undefined for non-promise loops. */
  1565. var enterLoop= function enterLoop(_validateFunc, _initFunc, _condFunc, _iterFunc, withPromise=false )
  1566. {
  1567. assertRunning();
  1568. var loopState;
  1569. if (!activeBlockStack().isHere()) {
  1570. // loop begins
  1571. loopState = { idx: idxHere() };
  1572. activeBlockStack().push(loopState);
  1573. var localVars = _validateFunc(loopState);
  1574. loopState.savedVars = getVarState(localVars);
  1575. initVarState(localVars); // because with-scope can reference storedVars only once they exist
  1576. _initFunc(loopState);
  1577. }
  1578. else {
  1579. // iteration
  1580. loopState = activeBlockStack().top();
  1581. _iterFunc(loopState);
  1582. }
  1583. return Selenium.prototype.handlePotentialPromise(
  1584. _condFunc(loopState),
  1585. value => {
  1586. if( !value ) {
  1587. loopState.isComplete = true;
  1588. // jump to bottom of loop for exit
  1589. setNextCommand(blkDefHere().endIdx);
  1590. }
  1591. // else continue into body of loop
  1592. },
  1593. withPromise
  1594. );
  1595. };
  1596. var iterateLoop= function iterateLoop( dontRestoreVars=false )
  1597. {
  1598. assertRunning();
  1599. assertActiveScope(blkDefHere().beginIdx);
  1600. var loopState = activeBlockStack().top();
  1601. if (loopState.isComplete) {
  1602. dontRestoreVars || restoreVarState(loopState.savedVars);
  1603. activeBlockStack().pop();
  1604. // done, fall out of loop
  1605. }
  1606. else {
  1607. // jump back to top of loop
  1608. setNextCommand(blkDefHere().beginIdx);
  1609. }
  1610. };
  1611. // ================================================================================
  1612. Selenium.prototype.doContinue = function doContinue(condExpr) {
  1613. var loopState = this.dropToLoop(condExpr);
  1614. if (loopState) {
  1615. // jump back to top of loop for next iteration, if any
  1616. var endCmd = blkDefFor(loopState);
  1617. setNextCommand(blkDefAt(endCmd.endIdx).beginIdx);
  1618. }
  1619. };
  1620. Selenium.prototype.doBreak = function doBreak(condExpr) {
  1621. var loopState = this.dropToLoop(condExpr);
  1622. if (loopState) {
  1623. loopState.isComplete = true;
  1624. // jump to bottom of loop for exit
  1625. setNextCommand(blkDefFor(loopState).endIdx);
  1626. }
  1627. };
  1628. // Unwind the command stack to the inner-most active loop block
  1629. // (unless the optional condition evaluates to false)
  1630. Selenium.prototype.dropToLoop= function dropToLoop(condExpr)
  1631. {
  1632. assertRunning();
  1633. if (condExpr) {
  1634. this.assertCompilable("", condExpr, ";", "Invalid condition");
  1635. }
  1636. if (this.transitionBubbling(Stack.isLoopBlock)) {
  1637. return;
  1638. }
  1639. if (condExpr && !this.evalWithExpandedStoredVars(condExpr)) {
  1640. return;
  1641. }
  1642. var loopState = activeBlockStack().unwindTo(Stack.isLoopBlock);
  1643. return loopState;
  1644. };
  1645. // ================================================================================
  1646. /** Note: See also ThirdPartyIssues.md > https://github.com/SeleniumHQ/selenium/issues/1635
  1647. * If callFromAsync, then either onSuccess or onFailure will be called on success or failure, respectively. It will be invoked asynchronously, *after* returning back to Javascript caller (i.e. to a non-Selenese layer that invoked this doCall()).
  1648. * @param {string} funcName
  1649. * @param {string|object} argSpec Comma-separated assignments of Selenese function parameters. Or an object (easily passed within =<>...<> as per http://selite.github.io/EnhancedSelenese) - then its fields define the Selenese function parameters. See reference.xml.
  1650. * @param {boolean} [invokedFromJavascript=false] Whether invoked from Javascript (rather than directly from Selenese)
  1651. * @param {function} [onSuccess] Callback function. Only used if invokedFromJavascript==true.
  1652. * @param {function} [onFailure] Callback function. Only used if invokedFromJavascript==true.
  1653. * */
  1654. Selenium.prototype.doCall = function doCall(funcName, argSpec, invokedFromJavascript=false, onSuccess, onFailure, callFromAsync=false )
  1655. {
  1656. !callFromAsync || assert(invokedFromJavascript, "Since callFromAsync is set, you must also set invokedFromJavascript.");
  1657. !onSuccess && !onFailure || assert( callFromAsync, "Only use onSuccess or onFailure with callFromAsync()." );
  1658. var loop = currentTest || htmlTestRunner.currentTest; // See Selenium.prototype.doRollup()
  1659. assertRunning(); // TBD: can we do single execution, ie, run from this point then break on return?
  1660. if( argSpec && typeof argSpec!=='object' ) {
  1661. this.assertCompilable("var ", argSpec, ";", "Invalid call parameter(s)");
  1662. }
  1663. var funcIdx = symbols[funcName];
  1664. assert(funcIdx!==undefined, " Function does not exist: " + funcName + ".");
  1665. var activeCallFrame = callStack.top();
  1666. if (activeCallFrame.isReturning && activeCallFrame.returnIdx === idxHere()) {
  1667. assert( !invokedFromJavascript, "Should have invokedFromJavascript undefined/false." );
  1668. LOG.warn( 'doCall: isReturning: popping call Stack');
  1669. // returning from completed function
  1670. var popped= callStack.pop();
  1671. loop.commandError= popped.originalCommandError;
  1672. var _result= storedVars._result;
  1673. storedVars= popped.savedVars; //restoreVarState( popped.savedVars );
  1674. storedVars._result= _result;
  1675. assert( testCase===popped.testCase, "The popped testCase is different." ); // Not sure why, but this seems to be true.
  1676. }
  1677. else {
  1678. LOG.debug('doCall invokedFromJavascript: ' +invokedFromJavascript+ ', callFromAsync: ' +callFromAsync);
  1679. if( callFromAsync ) {
  1680. assert(!this.invokedFromAsync, "invokedFromAsync is already set. Do not use callFromAsync() until the previous flow ends." );
  1681. this.invokedFromAsync= true;
  1682. }
  1683. var args;
  1684. if( typeof argSpec==='object' ) {
  1685. !( 'seLiteExtra' in argSpec ) || SeLiteMisc.fail( "Don't use @<>...<> with Selenese call command." );
  1686. // Parameters were passed in an object.
  1687. args= argSpec;
  1688. }
  1689. else {
  1690. // Support $stored-variablename
  1691. argSpec= expandStoredVars(argSpec);
  1692. // save existing variable state and set args as local variables
  1693. args = this.parseArgs(argSpec);
  1694. }
  1695. var savedVars= storedVars; //var savedVars = getVarStateFor(args);
  1696. storedVars= args; //args= setVars(args);
  1697. var originalCommandError= loop.commandError;
  1698. // There can be several cascading layers of these calls - one per function call level.
  1699. loop.commandError= function doCallCommandError( result ) {
  1700. this.commandError= originalCommandError;
  1701. // See also bubbleToTryBlock(..)
  1702. editor.selDebugger.pause();
  1703. originalCommandError.call( this, result ); // I've already restored this.commandError above *before* calling originalCommandError() here, because: if this was a deeper Selenese function call (i.e. a cascade of call -> function..endFunction) then originalCommandError() will restore any previous version of this.commandError, and I don't want to step on its feet here
  1704. };
  1705. LOG.debug( 'doCall: pushing callStack');
  1706. callStack.push( {
  1707. funcIdx,
  1708. name: funcName,
  1709. args,
  1710. returnIdx: !invokedFromJavascript
  1711. ? idxHere()
  1712. : (callFromAsync
  1713. ? undefined
  1714. : shiftGlobIdx(1)
  1715. ),
  1716. savedVars,
  1717. blockStack: new Stack(),
  1718. testCase,
  1719. originalCommandError,
  1720. invokedFromJavascript,
  1721. /* Following fields are only used when invokedFromJavascript is true.
  1722. * If invokedFromJavascript is true, after we finish/return/throw/bubble from the function body, we don't set next Selenese command, because
  1723. * - if this doCall() was invoked from Javascript (with no Selenese on the call stack, e.g. from GUI via a callback closure method), then we don't set the next command, since we're returning back to Javascript layer.
  1724. * - if this doCall() was invoked from getEval(), then Selenium sets the next command to be the one after that getEval()
  1725. * - do not invoke doCall() from javascript{...} or from EnhancedSyntax <>...<>
  1726. */
  1727. frameFromAsync: callFromAsync,
  1728. onSuccess,
  1729. onFailure,
  1730. //branchIdx: branchIdx,
  1731. testCase,
  1732. debugIndex: testCase.debugContext.debugIndex
  1733. } );
  1734. // jump to function body
  1735. setNextCommand(funcIdx);
  1736. }
  1737. };
  1738. /** 'Synchronous' - i.e. for Javascript that is invoked from a Selenese script that is already running (via getEval or via custom Selenese command). It runs SelBlocks Global 'call' command for given Selenese function *after* the current Selenese command (i.e. getVal or custom Selenese command) finishes.
  1739. * @param {string} seleneseFunctionName Selenese function name.
  1740. * @param {string|object} seleneseParameters Selenese function parameters. See doCall().
  1741. * @TODO test stored var _result
  1742. * */
  1743. Selenium.prototype.callBackInFlow= function callBackInFlow( seleneseFunctionName, seleneseParameters={} ) {
  1744. localIdxHere()+1<testCase.commands.length || SeLiteMisc.fail( "Do not invoke selenium.callBackInFlow() from the very last command if a test case." );
  1745. this.doCall( seleneseFunctionName, seleneseParameters, /*invokedFromJavascript*/true );
  1746. };
  1747. /** 'Asynchronous'- i.e. for Javascript invoked through e.g. SeLite Preview after a Selenese run finished.
  1748. * @param {string} seleneseFunctionName Selenese function name.
  1749. * @param {string|object} seleneseParameters Selenese function parameters. See doCall().
  1750. * @return {Promise} A Promise object that will receive a Selenese return value on success, or an exception on failure. (It will be called after the Selenese run flow finished - i.e. through window.setTimeout().)
  1751. */
  1752. Selenium.prototype.callBackOutFlow= function callBackOutFlow( seleneseFunctionName, seleneseParameters='') {
  1753. var funcIdx= symbols[seleneseFunctionName];
  1754. testCase= localCase( funcIdx );
  1755. LOG.debug('callBackOutFlow()');
  1756. return new Promise( (resolve, reject)=>{
  1757. testCase.calledFromAsync= {
  1758. functionName: seleneseFunctionName,
  1759. seleneseParameters,
  1760. onSuccess: resolve,
  1761. onFailure: reject
  1762. };
  1763. editor.suiteTreeView.currentTestCase==testCase || editor.app.notify( 'testCaseChanged', testCase );
  1764. // Following increases The Runs/Failures count by 1, rather than resetting it. This code depends on Selenium internals.
  1765. // Following invokes nextCommand(), which then uses the fields set on testCase above. It also validates that there is no test being run already.
  1766. editor.playCurrentTestCase( false, editor.testSuiteProgress.runs, editor.testSuiteProgress.total+1 );
  1767. } );
  1768. };
  1769. Selenium.prototype.doFunction = function doFunction(funcName)
  1770. {
  1771. assertRunning();
  1772. var funcDef = blkDefHere();
  1773. var activeCallFrame = callStack.top();
  1774. if (activeCallFrame.funcIdx === idxHere()) {
  1775. //SelBlocks used to call setVars(activeCallFrame.args); here. But this was already handled in doCall().
  1776. }
  1777. else {
  1778. // no active call, skip around function body
  1779. setNextCommand(funcDef.endIdx);
  1780. }
  1781. };
  1782. Selenium.prototype.doReturn = function doReturn(value) {
  1783. this.returnFromFunction(null, value);
  1784. };
  1785. Selenium.prototype.doEndFunction = function doEndFunction(funcName) {
  1786. this.returnFromFunction(funcName);
  1787. };
  1788. Selenium.prototype.doEndScript = function doEndScript(scrName) {
  1789. this.returnFromFunction(scrName);
  1790. };
  1791. Selenium.prototype.returnFromFunction= function returnFromFunction(funcName, returnVal)
  1792. {
  1793. assertRunning();
  1794. if (this.transitionBubbling(Stack.isFunctionBlock)) {
  1795. return;
  1796. }
  1797. var endDef = blkDefHere();
  1798. var activeCallFrame = callStack.top();
  1799. if (activeCallFrame.funcIdx !== endDef.funcIdx) {
  1800. // no active call, we're just skipping around a function block
  1801. }
  1802. else {
  1803. if (returnVal) {
  1804. storedVars._result= typeof returnVal==='object'
  1805. ? returnVal // Enable returning objects via SeLite EnhancedSelenese notation
  1806. : this.evalWithExpandedStoredVars(returnVal);
  1807. }
  1808. // jump back to call command
  1809. if( !activeCallFrame.invokedFromJavascript ) { // See a comment in doCall()
  1810. activeCallFrame.isReturning = true;
  1811. }
  1812. if( !activeCallFrame.invokedFromJavascript || !activeCallFrame.frameFromAsync ) {
  1813. setNextCommand(activeCallFrame.returnIdx);
  1814. // Don't callStack.pop() here - doCall() does it instead (in its second run)
  1815. }
  1816. if( activeCallFrame.invokedFromJavascript ) {
  1817. LOG.debug('returnFromFunction: invokedFromJavascript; frameFromAsync: ' +activeCallFrame.frameFromAsync);
  1818. //setNextCommand( activeCallFrame.branchIdx ); This failed when branchIdx wasn't set yet
  1819. // When using invokedFromJavascript, then the flow control doesn't go to a separate invoker 'call' Selenese command,
  1820. // since there wasn't any. Hence we handle the stack here.
  1821. testCase= activeCallFrame.testCase;
  1822. testCase.debugContext.debugIndex= activeCallFrame.debugIndex;
  1823. editor.selDebugger.runner.currentTest.commandComplete= () => {};
  1824. !activeCallFrame.onSuccess || window.setTimeout( ()=>activeCallFrame.onSuccess(storedVars._result), 0 );
  1825. $$.fn.interceptOnce(editor.selDebugger.runner.IDETestLoop.prototype, "resume", $$.handleAsExitTest);
  1826. this.invokedFromAsync= false;
  1827. LOG.debug( 'returnFromFunction: pop callStack');
  1828. restoreCallFrame( callStack.pop(), activeCallFrame.frameFromAsync );
  1829. }
  1830. }
  1831. };
  1832. // ================================================================================
  1833. Selenium.prototype.doExitTest = function doExitTest() {
  1834. if (this.transitionBubbling()) {
  1835. return;
  1836. }
  1837. // intercept command processing and simply stop test execution instead of executing the next command
  1838. //Following has same effect as intercepting "resume" on editor.selDebugger.runner.IDETestLoop.prototype. Classic SelBlocks overrode it on $$.seleniumTestRunner.currentTest, but that is not available here.
  1839. $$.fn.interceptOnce(editor.selDebugger.runner.currentTest, "resume", $$.handleAsExitTest);
  1840. };
  1841. // ========= storedVars management =========
  1842. /** SelBlocksGlobal: This is used instead of SelBlocks' evalWithVars(expr)
  1843. * @member {function}
  1844. */
  1845. Selenium.prototype.evalWithExpandedStoredVars= function evalWithExpandedStoredVars(expr) {
  1846. try {
  1847. typeof expr==='string' || expr===undefined || SeLiteMisc.fail( 'expr must be a string or undefined' );
  1848. var expanded= expr!==undefined
  1849. ? expandStoredVars(expr)
  1850. : undefined;
  1851. LOG.debug( 'Selenium.prototype.evalWithExpandedStoredVars(): ' +expr+ ' expanded to: ' +expanded );
  1852. var window = this.browserbot.getCurrentWindow();
  1853. /* Firefox eval() doesn't return values of some expressions as expected, e.g.
  1854. // '{field: "value"}'. That's why I assign to local variable 'evalWithExpandedStoredVarsResult' first, and then I return it.
  1855. I add () parenthesis, so that if the the expr contains multiple expressions separated by comma, then this uses the value of the last expression.
  1856. */
  1857. // EXTENSION REVIEWERS: Use of eval is consistent with the Selenium extension itself.
  1858. // Scripted expressions run in the Selenium window, separate from browser windows.
  1859. // Global functions are intentional features provided for use by end user's in their Selenium scripts.
  1860. var result = eval( "var evalWithExpandedStoredVarsResult= (" +expanded+ "); evalWithExpandedStoredVarsResult" );
  1861. LOG.debug( 'result: ' +typeof result+ ': ' +SeLiteMisc.objectToString(result, 2) );
  1862. return result;
  1863. }
  1864. catch (err) {
  1865. notifyFatalErr(" While evaluating Javascript expression: " + expr+ " expanded as " +expanded, err);
  1866. }
  1867. };
  1868. // This is not related to parseArgs(str) in chrome/content/selenium-core/test/RemoteRunnerTest.js
  1869. Selenium.prototype.parseArgs= function parseArgs(argSpec) { // comma-sep -> new prop-set
  1870. var args = {};
  1871. /* See preprocessParameter() in this file. It implements http://selite.github.io/EnhancedSelenese.
  1872. Split argSpec if it is in format fieldA=valueA,fieldB=..<>...<>,fieldC=..<>..<>,..
  1873. @TODO Also support commas within '' or ""?
  1874. original from SelBlocks:*/
  1875. var parms = iexpr.splitList(argSpec, ",");
  1876. // var parms = argSpec.split(","); //before SelBlocks 2
  1877. for (var i = 0; i < parms.length; i++) {
  1878. // var keyValue = parms[i].split("="); //before SelBlocks 2
  1879. var keyValue = iexpr.splitList(parms[i], "=");
  1880. validateName(keyValue[0], "parameter");
  1881. args[ keyValue[0].trim() ] = this.evalWithExpandedStoredVars(keyValue[1]);
  1882. }
  1883. return args;
  1884. };
  1885. var initVarState= function initVarState(names) { // new -> storedVars(names)
  1886. if (names) {
  1887. var i;
  1888. for (i = 0; i < names.length; i++) {
  1889. if (!storedVars[names[i]]) {
  1890. storedVars[names[i]] = null;
  1891. }
  1892. }
  1893. }
  1894. };
  1895. var getVarStateFor= function getVarStateFor(args) { // storedVars(prop-set) -> new prop-set
  1896. var savedVars = {};
  1897. var varname;
  1898. for (varname in args) {
  1899. savedVars[varname] = storedVars[varname];
  1900. }
  1901. return savedVars;
  1902. };
  1903. var getVarState= function getVarState(names) { // storedVars(names) -> new prop-set
  1904. var savedVars = {};
  1905. if (names) {
  1906. var i;
  1907. for (i = 0; i < names.length; i++) {
  1908. savedVars[names[i]] = storedVars[names[i]];
  1909. }
  1910. }
  1911. return savedVars;
  1912. };
  1913. var setVars= function setVars(args) { // prop-set -> storedVars
  1914. var varname;
  1915. for (varname in args) {
  1916. storedVars[varname] = args[varname];
  1917. }
  1918. };
  1919. var restoreVarState= function restoreVarState(savedVars) { // prop-set --> storedVars
  1920. var varname;
  1921. for (varname in savedVars) {
  1922. if (savedVars[varname] === undefined) {
  1923. delete storedVars[varname];
  1924. }
  1925. else {
  1926. storedVars[varname] = savedVars[varname];
  1927. }
  1928. }
  1929. };
  1930. // ========= error handling =========
  1931. var SelblocksError= function SelblocksError(idx, message='') {
  1932. this.name = "SelblocksError";
  1933. this.message = message;
  1934. this.idx = idx;
  1935. };
  1936. SelblocksError.prototype = Error.prototype;
  1937. // TBD: make into throwable Errors
  1938. var notifyFatalErr= function notifyFatalErr(msg, err) {
  1939. $$.LOG.error("Error " + msg);
  1940. $$.LOG.logStackTrace(err);
  1941. throw err;
  1942. };
  1943. var notifyFatal= function notifyFatal(msg) {
  1944. var err = new Error(msg);
  1945. $$.LOG.error("Error " + msg);
  1946. $$.LOG.logStackTrace(err);
  1947. throw err;
  1948. };
  1949. var notifyFatalCmdRef= function notifyFatalCmdRef(idx, msg) { notifyFatal(fmtCmdRef(idx) + msg); };
  1950. var notifyFatalHere= function notifyFatalHere(msg) {
  1951. // This may be called before testCase is set
  1952. var commandRef;
  1953. if( testCase===undefined ) {
  1954. commandRef= 'unknown step: ';
  1955. }
  1956. else {
  1957. // SelBlocks used fmtCurCmd() here. However, this
  1958. // may be called right after TestCaseDebugContext's nextCommand(), which (as intercepted by SelBlocksGlobal) sets testCase.debugContext.debugIndex to -1. Then
  1959. // fmtCurCmd() would fail (as it invokes idxHere() -> globIdx(-1).
  1960. var stepLocalIdx= localIdxHere();
  1961. commandRef= fmtCmdRef( globIdx( Math.max(stepLocalIdx, 0) ) )+ ': ';
  1962. }
  1963. notifyFatal( commandRef+msg );
  1964. };
  1965. var assertCmd= function assertCmd(idx, cond, msg) { if (!cond) { notifyFatalCmdRef(idx, msg); } };
  1966. var assert= function assert(cond, msg) { if (!cond) { notifyFatalHere(msg); } };
  1967. // TBD: can we at least show result of expressions?
  1968. var assertRunning= function assertRunning() {
  1969. assert(testCase.debugContext.started, " Command is only valid in a running script,"
  1970. + " i.e., cannot be executed via double-click, or via 'Execute this command'.");
  1971. };
  1972. var assertActiveScope= function assertActiveScope(expectedIdx) {
  1973. var activeIdx = activeBlockStack().top().idx;
  1974. assert(activeIdx === expectedIdx, " unexpected command, active command was " + fmtCmdRef(activeIdx));
  1975. };
  1976. Selenium.prototype.assertCompilable= function assertCompilable(left, stmt, right, explanation) {
  1977. try {
  1978. this.evalWithExpandedStoredVars("function selblocksTemp() { " + left + stmt + right + " }");
  1979. }
  1980. catch (e) {
  1981. throw new SyntaxError(fmtCmdRef(idxHere()) + " " + explanation + " '" + stmt + "': " + e.message);
  1982. }
  1983. };
  1984. var fmtCurCmd= function fmtCurCmd() {
  1985. return fmtCmdRef(idxHere());
  1986. };
  1987. var fmtCmdRef= function fmtCmdRef(idx) {
  1988. var test= localCase(idx);
  1989. var commandIdx= localIdx(idx);
  1990. return "@" +test.file.path+ ': ' +(commandIdx+1) + ": [" + $$.fmtCmd( test.commands[commandIdx] )+ "]";
  1991. };
  1992. //================= utils ===============
  1993. // Elapsed time, optional duration provides expiration
  1994. var IntervalTimer= function IntervalTimer(msDuration) {
  1995. this.msStart = +new Date();
  1996. this.getElapsed = function getElapsed() { return (+new Date() - this.msStart); };
  1997. this.hasExpired = function hasExpired() { return (msDuration && this.getElapsed() > msDuration); };
  1998. this.reset = function reset() { this.msStart = +new Date(); };
  1999. };
  2000. // produce an iterator object for the given array
  2001. var arrayIterator= function arrayIterator(arrayObject) {
  2002. return new function arrayIteratorClosure(ary) {
  2003. var cur = 0;
  2004. this.hasNext = function hasNext() { return (cur < ary.length); };
  2005. this.next = function next() { if (this.hasNext()) { return ary[cur++]; } };
  2006. }(arrayObject);
  2007. };
  2008. // ==================== Data Files ====================
  2009. // Adapted from the datadriven plugin
  2010. // http://web.archive.org/web/20120928080130/http://wiki.openqa.org/display/SEL/datadriven
  2011. var XmlReader= function XmlReader()
  2012. {
  2013. var varsets = null;
  2014. var varNames = null;
  2015. var curVars = null;
  2016. var varsetIdx = 0;
  2017. // load XML file and return the list of var names found in the first <VARS> element
  2018. this.load = function load(filepath)
  2019. {
  2020. var fileReader = new FileReader();
  2021. var fileUrl;
  2022. // in order to not break existing tests the IDE will still use urlFor,
  2023. // on the server it just breaks things. Data can be anywhere on the net,
  2024. // accessible through proper CORS headers.
  2025. // SelBlocksGlobal: no support for globalContext.onServer
  2026. fileUrl = urlFor(filepath);
  2027. var xmlHttpReq = fileReader.getDocumentSynchronous(fileUrl);
  2028. $$.LOG.info("Reading from: " + fileUrl);
  2029. var fileObj = xmlHttpReq.responseXML; // XML DOM
  2030. varsets = fileObj.getElementsByTagName("vars"); // HTMLCollection
  2031. if (varsets === null || varsets.length === 0) {
  2032. throw new Error("A <vars> element could not be loaded, or <testdata> was empty.");
  2033. }
  2034. curVars = 0;
  2035. varNames = XmlReader.attrNamesFor(varsets[0]);
  2036. return varNames;
  2037. };
  2038. this.EOF = function EOF() {
  2039. return (curVars === null || curVars >= varsets.length);
  2040. };
  2041. this.next = function next()
  2042. {
  2043. if (this.EOF()) {
  2044. $$.LOG.error("No more <vars> elements to read after element #" + varsetIdx);
  2045. return;
  2046. }
  2047. varsetIdx++;
  2048. $$.LOG.debug(varsetIdx + ") " + XmlReader.serialize(varsets[curVars])); // log each name & value
  2049. var expected = XmlReader.countAttrs(varsets[0]);
  2050. var found = XmlReader.countAttrs(varsets[curVars]);
  2051. if (found !== expected) {
  2052. throw new Error("Inconsistent <testdata> at <vars> element #" + varsetIdx
  2053. + "; expected " + expected + " attributes, but found " + found + "."
  2054. + " Each <vars> element must have the same set of attributes."
  2055. );
  2056. }
  2057. XmlReader.setupStoredVars(varsets, varsetIdx, varsets[curVars]);
  2058. curVars++;
  2059. };
  2060. }; // end of XmlReader
  2061. //- retrieve the names of each attribute on the given XML node
  2062. XmlReader.attrNamesFor= function attrNamesFor(node) {
  2063. var attrNames = [];
  2064. var varAttrs = node.attributes; // NamedNodeMap
  2065. var v;
  2066. for (v = 0; v < varAttrs.length; v++) {
  2067. attrNames.push(varAttrs[v].nodeName);
  2068. }
  2069. return attrNames;
  2070. };
  2071. //- determine how many attributes are present on the given node
  2072. XmlReader.countAttrs= function countAttrs(node) {
  2073. return node.attributes.length;
  2074. };
  2075. //- set selenium variables from given XML attributes
  2076. XmlReader.setupStoredVars= function setupStoredVars(varsets, varsetIdx, node) {
  2077. var varAttrs = node.attributes; // NamedNodeMap
  2078. var v;
  2079. for (v = 0; v < varAttrs.length; v++) {
  2080. var attr = varAttrs[v];
  2081. if (null === varsets[0].getAttribute(attr.nodeName)) {
  2082. throw new Error("Inconsistent <testdata> at <vars> element #" + varsetIdx
  2083. + "; found attribute " + attr.nodeName + ", which does not appear in the first <vars> element."
  2084. + " Each <vars> element must have the same set of attributes."
  2085. );
  2086. }
  2087. storedVars[attr.nodeName] = attr.nodeValue;
  2088. }
  2089. };
  2090. //- format the given XML node for display
  2091. XmlReader.serialize= function serialize(node) {
  2092. if (XMLSerializer !== "undefined") {
  2093. return (new XMLSerializer()).serializeToString(node) ;
  2094. }
  2095. if (node.xml) { return node.xml; }
  2096. throw "XMLSerializer is not supported or can't serialize " + node;
  2097. };
  2098. var JSONReader= function JSONReader()
  2099. {
  2100. var varsets = null;
  2101. var varNames = null;
  2102. var curVars = null;
  2103. var varsetIdx = 0;
  2104. // load JSON file and return the list of var names found in the first object
  2105. this.load = function load(filepath)
  2106. {
  2107. var fileReader = new FileReader();
  2108. var fileUrl;
  2109. // in order to not break existing tests the IDE will still use urlFor,
  2110. // on the server it just breaks things. Data can be anywhere on the net,
  2111. // accessible through proper CORS headers.
  2112. // SelBlocksGlobal: no support for globalContext.onServer
  2113. fileUrl = urlFor(filepath);
  2114. // Following steps generate a false-positive error 'syntax error varset.json:1'. See http://selite.github.io/ThirdPartyIssues > https://bugzilla.mozilla.org/show_bug.cgi?id=1031985
  2115. var xmlHttpReq = fileReader.getDocumentSynchronous(fileUrl);
  2116. $$.LOG.info("Reading from: " + fileUrl);
  2117. var fileObj = xmlHttpReq.responseText;
  2118. fileObj = fileObj.replace("/\uFFFD/g", "").replace(/\0/g, "");
  2119. $$.LOG.info('evaluating JSON file' );
  2120. $$.LOG.info(fileObj);
  2121. try {
  2122. varsets= JSON.parse(fileObj);
  2123. }
  2124. catch(e) {
  2125. // This is for .json files that don't have keys (field names) in quotes - i.e. they have them in apostrophes or unquoted. Extension reviewers: There is no other way around this. The user's intention is to load the file as an array of objects, so we do it.
  2126. varsets= eval( 'var result=' +fileObj+ '; result' );
  2127. }
  2128. if (varsets === null || varsets.length === 0) {
  2129. throw new Error("A JSON object could not be loaded, or the file was empty.");
  2130. }
  2131. $$.LOG.info('Has successfully read the JSON file');
  2132. curVars = 0;
  2133. varNames = JSONReader.attrNamesFor(varsets[0]);
  2134. return varNames;
  2135. };
  2136. this.EOF = function EOF() {
  2137. return (curVars === null || curVars >= varsets.length);
  2138. };
  2139. this.next = function next()
  2140. {
  2141. if (this.EOF()) {
  2142. $$.LOG.error("No more JSON objects to read after object #" + varsetIdx);
  2143. return;
  2144. }
  2145. varsetIdx++;
  2146. $$.LOG.debug(varsetIdx + ") " + JSONReader.serialize(varsets[curVars])); // log each name & value
  2147. var expected = JSONReader.countAttrs(varsets[0]);
  2148. var found = JSONReader.countAttrs(varsets[curVars]);
  2149. if (found !== expected) {
  2150. throw new Error("Inconsistent JSON object #" + varsetIdx
  2151. + "; expected " + expected + " attributes, but found " + found + "."
  2152. + " Each JSON object must have the same set of attributes."
  2153. );
  2154. }
  2155. JSONReader.setupStoredVars(varsets, varsetIdx, varsets[curVars]);
  2156. curVars++;
  2157. };
  2158. }; // end of JSONReader
  2159. //- retrieve the names of each attribute on the given object
  2160. JSONReader.attrNamesFor= function attrNamesFor(obj) {
  2161. var attrNames = [];
  2162. var attrName;
  2163. for (attrName in obj) {
  2164. attrNames.push(attrName);
  2165. }
  2166. return attrNames;
  2167. };
  2168. //- determine how many attributes are present on the given obj
  2169. JSONReader.countAttrs= function countAttrs(obj) {
  2170. var n = 0;
  2171. var attrName;
  2172. for (attrName in obj) {
  2173. n++;
  2174. }
  2175. return n;
  2176. };
  2177. //- set selenium variables from given JSON attributes
  2178. JSONReader.setupStoredVars= function setupStoredVars(varsets, varsetIdx, obj) {
  2179. var attrName;
  2180. for (attrName in obj) {
  2181. if (null === varsets[0][attrName]) {
  2182. throw new Error("Inconsistent JSON at object #" + varsetIdx
  2183. + "; found attribute " + attrName + ", which does not appear in the first JSON object."
  2184. + " Each JSON object must have the same set of attributes."
  2185. );
  2186. }
  2187. storedVars[attrName] = obj[attrName];
  2188. }
  2189. };
  2190. //- format the given JSON object for display
  2191. JSONReader.serialize= function serialize(obj) {
  2192. var json = uneval(obj);
  2193. return json.substring(1, json.length-1);
  2194. };
  2195. /** Match an absolute filepath, after any backslashes \ were converted to forward slashes /. */
  2196. var absoluteFilePathPrefix= /^(\/|\/?[a-z]:\/)/i;
  2197. var backSlashGlobal= /\\/g;
  2198. /** Convert a filepath to URL. If filepath doesn't start with http, data: or file://, then treat it as a filepath. If it's not an absolute filepath (i.e. not starting with /, [a-z]:\ or /[a-z]:\), then convert it to absolute. Convert directory separators / or \ as appropriate. (data: meta protocol is primarily for SeLite Preview.)
  2199. * @param {boolean} [relativeToTestSuite=false] If true, then require SeLite Settings and treat filepath as relative to test suite. Otherwise treat it as relative to test case (classic SelBlocks behaviour).
  2200. * @TODO consider making it work as if always relativeToTestSuite=true
  2201. * */
  2202. var urlFor= function urlFor( filepath, relativeToTestSuite=false ) {
  2203. if( filepath.indexOf("http")===0 || filepath.indexOf("data:")===0 ) {
  2204. return filepath;
  2205. }
  2206. filepath= filepath.replace( backSlashGlobal, "/" );
  2207. var URL_PFX = "file://";
  2208. if (filepath.substring(0, URL_PFX.length).toLowerCase() !== URL_PFX) {
  2209. if( filepath.match(absoluteFilePathPrefix) ) {
  2210. if( filepath[0]!=='/' ) { // Matching /^[a-z]:/i
  2211. filepath= '/' +filepath;
  2212. }
  2213. return URL_PFX+ filepath;
  2214. }
  2215. else {
  2216. var relativeToFolder;
  2217. if( relativeToTestSuite ) {
  2218. relativeToFolder= SeLiteSettings.getTestSuiteFolder().replace( backSlashGlobal, "/" );
  2219. }
  2220. else {
  2221. relativeToFolder= testCase.file.path.replace( backSlashGlobal, "/" );
  2222. var i = relativeToFolder.lastIndexOf("/");
  2223. relativeToFolder= relativeToFolder.substr(0, i);
  2224. }
  2225. filepath = URL_PFX +relativeToFolder + "/" + filepath;
  2226. }
  2227. }
  2228. return filepath;
  2229. };
  2230. Selenium.urlFor= urlFor;
  2231. // ==================== File Reader ====================
  2232. // Adapted from the include4ide plugin
  2233. function FileReader() {}
  2234. FileReader.prototype.prepareUrl = function prepareUrl(url) {
  2235. var absUrl;
  2236. // htmlSuite mode of SRC? TODO is there a better way to decide whether in SRC mode?
  2237. if (window.location.href.indexOf("selenium-server") >= 0) {
  2238. $$.LOG.debug("FileReader() is running in SRC mode");
  2239. // there's no need to absolutify the url, the browser will do that for you
  2240. // when you make the request. The data may reside anywhere on the site, or
  2241. // within the "virtual directory" created by the selenium server proxy.
  2242. // I don't want to limit the ability to parse files that actually exist on
  2243. // the site, like sitemaps or JSON responses to api calls.
  2244. absUrl = url;
  2245. }
  2246. else {
  2247. absUrl = absolutify(url, selenium.browserbot.baseUrl);
  2248. }
  2249. $$.LOG.debug("FileReader() using URL to get file '" + absUrl + "'");
  2250. return absUrl;
  2251. };
  2252. FileReader.prototype.getDocumentSynchronous = function getDocumentSynchronous(url) {
  2253. var absUrl = this.prepareUrl(url);
  2254. var requester = this.newXMLHttpRequest();
  2255. if (!requester) {
  2256. throw new Error("XMLHttp requester object not initialized");
  2257. }
  2258. requester.open("GET", absUrl, false); // synchronous (we don't want selenium to go ahead)
  2259. try {
  2260. //following generates 'syntax error' in Browser Console. See http://selite.github.io/ThirdPartyIssues > https://bugzilla.mozilla.org/show_bug.cgi?id=1031985
  2261. requester.send(null);
  2262. }
  2263. catch(e) {
  2264. throw new Error("Error while fetching URL '" + absUrl + "':: " + e);
  2265. }
  2266. if (requester.status !== 200 && requester.status !== 0) {
  2267. throw new Error("Error while fetching " + absUrl
  2268. + " server response has status = " + requester.status + ", " + requester.statusText );
  2269. }
  2270. return requester;
  2271. };
  2272. FileReader.prototype.newXMLHttpRequest = function newXMLHttpRequest() {
  2273. var requester = 0;
  2274. try {
  2275. // for IE/ActiveX
  2276. if (window.ActiveXObject) {
  2277. try { requester = new ActiveXObject("Msxml2.XMLHTTP"); }
  2278. catch(ee) { requester = new ActiveXObject("Microsoft.XMLHTTP"); }
  2279. }
  2280. // Native XMLHttp
  2281. else if (window.XMLHttpRequest) {
  2282. requester = new XMLHttpRequest();
  2283. }
  2284. }
  2285. catch(e) {
  2286. throw new Error("Your browser has to support XMLHttpRequest in order to read data files\n" + e);
  2287. }
  2288. return requester;
  2289. };
  2290. }(selblocks));
  2291. (function() {
  2292. // Assume single-line string only.
  2293. // Following offers 80/20 support for ECMAScript 6 Template Literals (http://es6-features.org/#StringInterpolation), which use `...${javascript expression}...`.
  2294. // It restricts substitution of ${...} in Selenese parameters. It doesn't substitute those within template literals `...`.
  2295. // Affected documentation: Selenium IDE > Store Commands and Selenium Variables(http://docs.seleniumhq.org/docs/02_selenium_ide.jsp#store-commands-and-selenium-variables)
  2296. // No special support for `...\${..}...`. That is not an ES6 Template Literal (because of the backslash). However, Selenese doesn't substitue that ${...} with any stored value.
  2297. // No need for special support for template literals `...` that include any comments /*...*/ that include back apostrophe `. E.g. `hi ${ /*the following back apostrophe should be ignored`*/ 'man'}` wouldn't match. However, that would be OK, since the inner part of ${...} would consist non-word characters, hence original Selenium.prototype.replaceVariables() wouldn't modify it.
  2298. // Split a (deemed) Javascript expression by comments.
  2299. // Skip comments /*...*/ that are outside any string literals
  2300. // Group meanings: ((a classic '" or template string ` literal or other char.) (comment /*..*/ )
  2301. // Capturing Groups:123 4 5 6 7 8
  2302. // Parenthesis lev: 12 3 3 3 3 3 3 34 4 3 2 2 34 4 3 21
  2303. // Challenge: how to get all matches, rather than just the last.
  2304. //var commentOrOtherRegex= /^(('([^']|\\')*'|"([^"]|\\")*"|`([^`]|\\`)*`|((?!\/\*)[^'"])+)|(\/\*((?!\*\/).)*\*\/))+$/;
  2305. // Group meanings: (not a comment /*...*/ )(not a classic string literal )
  2306. // Captured group levels:
  2307. // Parenthesis levels: 1 23 3 2 11 2 2 11
  2308. var templateLiteralRegex= /(?!\/\*((?!\*\/).)*\*\/)(?!'([^']|\\')*')(?!"([^""]|\\")*')/;
  2309. var originalReplaceVariables= Selenium.prototype.replaceVariables;
  2310. Selenium.prototype.replaceVariables = function replaceVariables(str) {
  2311. var result= '';
  2312. for( var i=0; i<str.length; i++ ) {
  2313. var slice= '';
  2314. while( i<str.length && "'`\"".indexOf(str[i])<0 ) { // Support for strings other than string literals/template literals
  2315. slice+= str[i];
  2316. if( str[i]==='/' && i+1<str.length && str[i+1]==='*' ) { // Support for comment /*...*/
  2317. slice+= str[++i];
  2318. while( ++i<str.length ) {
  2319. slice+= str[i];
  2320. if( str[i]==='*' && i+1<str.length && str[i+1]==='/' ) {
  2321. slice+= str[++i];
  2322. break;
  2323. }
  2324. }
  2325. }
  2326. i++; // It may become str.length or str.length+1, but that doesn't matter
  2327. }
  2328. result+= originalReplaceVariables.call( null, slice );
  2329. slice= '';
  2330. if( i<str.length && "'`\"".indexOf(str[i])>=0 ) { // Support for string literals/template literals
  2331. var delimiter= str[i];
  2332. slice+= delimiter;
  2333. while( ++i<str.length ) {
  2334. slice+= str[i];
  2335. if( str[i]==='\\' ) { // append the character after \
  2336. if( ++i<str.length ) {
  2337. slice+= str[i];
  2338. }
  2339. continue;
  2340. }
  2341. if( str[i]===delimiter ) {
  2342. break;
  2343. }
  2344. }
  2345. result+= delimiter!=="`"
  2346. ? originalReplaceVariables.call( null, slice )
  2347. : slice;
  2348. }
  2349. }
  2350. return result;
  2351. };
  2352. var originalPreprocessParameter= Selenium.prototype.preprocessParameter;
  2353. // For handling =<>...<>
  2354. var enclosedByEqualsSpecialPairs= /^=<>(((?!<>).)*)<>$/g;
  2355. /**This adds support for javascript expressions enclosed with <>...<>, \<>...<> or @<>...<>
  2356. as documented at http://selite.github.io/EnhancedSelenese.
  2357. If the user wants to actually pass a string '<>' to the result, she or he has to work around this (e.g. by generating it in a Javascript expression).
  2358. The 3rd captured group - the postfix - is guaranteed not to end with # or @ that would be just before the next occurrence of <>...<> (if any)
  2359. Levels of regex. parenthesis 12 3 3 2 1 12 3 3 2 1 12 3 3 3 32 1
  2360. */
  2361. var enclosedBySpecialPairs= /((?:(?!<>).)*)<>((?:(?!<>).)+)<>((?:(?!<>)[^#@]|[#@](?!<>))*)/g;
  2362. /** A head intercept of preprocessParameter() from chrome/content/selenium-core/scripts/selenium-api.js. It implements http://selite.github.io/EnhancedSelenese. */
  2363. Selenium.prototype.preprocessParameter = function selBlocksGlobalPreprocessParameter(whole) {
  2364. /** javascript{..} doesn't replace ${variableName}.
  2365. Selenese ${variableName} requires {}, which is good because it separates it from the rest of the target/value,
  2366. so it's robust yet easy to use.
  2367. <>...<> replaces $xxx by the symbol/reference to the stored variable, so it is typed and it doesn't need to be quoted for Javascript processing.
  2368. Hence EnhancedSelenese can access stored variables, their fields/subfields and methods using $xyz.
  2369. */
  2370. LOG.debug('SeLite SelBlocks Global head override of preprocessParameter(): ' +whole );
  2371. var numberOfSpecialPairs= 0;
  2372. for( var i=1; i<whole.length; i++ ) {
  2373. if( whole[i-1]==='<' && whole[i]==='>' ) {
  2374. numberOfSpecialPairs++;
  2375. i++;
  2376. }
  2377. }
  2378. numberOfSpecialPairs%2===0 || SeLiteMisc.fail( "SeLite SelBlocks Global and its http://selite.github.io/EnhancedSelenese doesn't allow Selenese parameters to contain an odd number of character pairs <>. The parameter value was: " +whole );
  2379. /**Match <>...<>, \<>...<>, =<>...<> and @<>...<>. Replace $xx parts with respective stored variables. Evaluate. If it was \<>...<>, then escape it as an XPath string. If it was @<>...<>, then make the rest a String object (rather than a string primitive) and store the result of Javascript in field seLiteExtra on that String object.
  2380. I don't replace through a callback function - e.g. whole.replace( enclosedBySpecialPairs, function replacer(match, field) {..} ) - because that would always cast the replacement result as string.
  2381. */
  2382. enclosedByEqualsSpecialPairs.lastIndex= 0;
  2383. var match= enclosedByEqualsSpecialPairs.exec(whole);
  2384. if( match ) {
  2385. return this.evalWithExpandedStoredVars( this.replaceVariables(match[1]) );
  2386. }
  2387. else {
  2388. enclosedBySpecialPairs.lastIndex=0;
  2389. var hasExtra= false; // Whether there is @<>...<>
  2390. var extra; // The extra value: result of Javascript from @<>...<>
  2391. /** @type {boolean} Whether <code>result</code> contains a result of at least one <>...<> or its variations.
  2392. * */
  2393. var alreadyProcessedDoubledSpecialPairs= false;
  2394. var result= '';
  2395. for(match= enclosedBySpecialPairs.exec(whole); match; match= enclosedBySpecialPairs.exec(whole) ) {
  2396. var prefix= originalPreprocessParameter.call( this, match[1] ); // That calls Selenium.prototype.replaceVariables()
  2397. var postfix= originalPreprocessParameter.call( this, match[3] ); // That calls Selenium.prototype.replaceVariables()
  2398. var value= this.evalWithExpandedStoredVars( this.replaceVariables(match[2]) );
  2399. !prefix.endsWith('=') || SeLiteMisc.fail( "You can only use =<>...<> with no prefix/postfix, but you've passed parameter value " +whole );
  2400. if( prefix.endsWith('@') ) {
  2401. !hasExtra || SeLiteMisc.fail( "Selenese parameter contains multiple occurrences of @<>...<>, but it should have a maximum one. The parameter value was: " +whole );
  2402. hasExtra= true;
  2403. extra= value;
  2404. value= '';
  2405. prefix= prefix.substring( 0, prefix.length-1 );
  2406. }
  2407. else {
  2408. if( prefix.endsWith("\\") ) {
  2409. value= ( ''+value ).quoteForXPath(); // From Selenium Core
  2410. prefix= prefix.substring( 0, prefix.length-1 );
  2411. }
  2412. }
  2413. alreadyProcessedDoubledSpecialPairs= true;
  2414. result+= prefix+value+postfix;
  2415. }
  2416. if( !alreadyProcessedDoubledSpecialPairs ) {
  2417. // There was no <>...<> (neither its alternatives)
  2418. result= originalPreprocessParameter.call( this, whole ); // That calls Selenium.prototype.replaceVariables()
  2419. }
  2420. if( hasExtra ) {
  2421. result= new String(result);
  2422. result.seLiteExtra= extra;
  2423. }
  2424. return result;
  2425. }
  2426. };
  2427. var originalGetEval= Selenium.prototype.getEval;
  2428. Selenium.prototype.getEval = function getEval(script) {
  2429. // Parameter script should be a primitive string. If it is an object, it is a result of exactly one expression within <>...<> (with no prefix & postfix) yeilding an object, or a result of @<>...<> (with an optional prefix/postfix) as processed by Selenium.prototype.preprocessParameter() as overriden by SelBlocksGlobal. Such parameters should not be used with getEval.
  2430. if( typeof script==='object' ) {
  2431. var msg= "You must call getEval/getEvalPromise (and their derivatives such as storeEval/storeEvalPromise) with a primitive (non-object) string in Target parameter.";
  2432. msg+= script.seLiteExtra!==undefined
  2433. ? " Don't use enhanced syntax @<>...<> for Target."
  2434. : " However, you've used enhanced syntax =<>...<> for Target, which resulted in an object of class " +SeLiteMisc.classNameOf(script)+ ". Alternatively, if you'd really like to pass the string value of this object as a parameter to command getEval (or storeEval...), which would then evaluate it as a Javascript expression (again), use <>...<> instead.";
  2435. msg+= " See http://selite.github.io/EnhancedSelenese"
  2436. SeLiteMisc.fail( msg );
  2437. }
  2438. return originalGetEval.call( this, expandStoredVars(script) );
  2439. };
  2440. /* We don't define Selenium.prototype.getPromise. When it used similar code to the following (i.e. returning an anonymous object with terminationCondition), then automatically-generated storePromise couldn't store the success result. Instead, it stored the anonymous object that contained terminationCondition.
  2441. Selenium.prototype.getPromiseViaGet= function getOrStorePromise( script ) {
  2442. var promise= this.getEval( script );
  2443. if( !promise || !('then' in promise) ) {
  2444. throw new SeleniumError( "getPromiseViaGet( '" +script+ "' ) didn't return an object having .then() method. Instead, it returned: " + promise );
  2445. }
  2446. var succeeded, failed;
  2447. var result;
  2448. promise.then(
  2449. value=> { debugger; succeeded= true; result= value; },
  2450. failure=> { failed= true; result= failure; }
  2451. );
  2452. return {
  2453. terminationCondition: function terminationCondition() {
  2454. debugger;
  2455. if( succeeded ) {
  2456. debugger;
  2457. // 'this' will be AccessorResult
  2458. this.result= result; // This has no effect. If there is terminationCondition, then Selenium doesn't use .result field of AccessorResult object!
  2459. return true; //or: return result - no difference
  2460. }
  2461. if( failed ) {
  2462. throw result;
  2463. }
  2464. return false;
  2465. }
  2466. };
  2467. };/**/
  2468. Selenium.ensureThenable= function ensureThenable( promise ) {
  2469. if( !promise || !('then' in promise) || typeof promise.then!=='function' ) {
  2470. throw new SeleniumError( "Expecting a Promise, or an object having .then() method. Instead, received: " + promise );
  2471. }
  2472. };
  2473. Selenium.ensureThenableOrNot= function ensureThenableOrNot( promise, shouldBePromise ) {
  2474. if( shouldBePromise ) {
  2475. Selenium.ensureThenable( promise );
  2476. }
  2477. else {
  2478. // Following needs to check for typeof promise==='object'. Otherwise, if promise===true, expression 'then' in true fails.
  2479. if( promise && typeof promise==='object' && 'then' in promise && typeof promise.then==='function' ) {
  2480. throw new SeleniumError( "Expecting a non-Promise, non-thenable. Instead, received an object having .then() method: " + promise );
  2481. }
  2482. }
  2483. };
  2484. /** This functions streamlines handling of Promise (or potentially Promise) results in Selenese commands.
  2485. * Call it from Selenium.prototype.doYourFunction(first, second) and return its result value. That ensures the mechanism works for promiseOrResult being either a Promise or a non-Promise.
  2486. * @param {(*|Promise)} promiseOrResult
  2487. * @param {function} [handler] A callback function. If present, this will invoke it either
  2488. * - immediately if !withPromise, or
  2489. * - once the promise resolved (which can also be immediately), but not if it resolved after timing out
  2490. * @param {boolean} [withPromise=false] Whether promiseOrResult should be a Promise, or not. This function validated promiseOrResult accordingly..
  2491. * @return {(function|undefined)} Exactly if withPromise, then return a function to return back to Selenium (that will be used as continuation test) that checks the promise status of and promiseOrResult and it throws on timeout. Otherwise (i.e. !withPromise) return undefined (i.e. no need for a continuation test).
  2492. * */
  2493. Selenium.prototype.handlePotentialPromise= function handlePotentialPromise( promiseOrResult, handler=undefined, withPromise=false ) {
  2494. Selenium.ensureThenableOrNot( promiseOrResult, withPromise );
  2495. if( !withPromise ) {
  2496. if( handler ) {
  2497. handler( promiseOrResult );
  2498. }
  2499. }
  2500. else {
  2501. var succeeded, failed;
  2502. var result;
  2503. promiseOrResult.then(
  2504. value=> { succeeded= true; result= value; },
  2505. failure=> { failed= true; result= failure; }
  2506. );
  2507. // Check the timeout. Otherwise, if the promise never resolved, then Selenium would call the termination function indefinitely in the background, without any error about it!
  2508. return Selenium.decorateFunctionWithTimeout(
  2509. () => {
  2510. if( succeeded ) {
  2511. if( handler ) {
  2512. handler( result );
  2513. }
  2514. return true;
  2515. }
  2516. if( failed ) {
  2517. throw new SeleniumError( result );
  2518. }
  2519. },
  2520. this.defaultTimeout
  2521. );
  2522. }
  2523. };
  2524. Selenium.prototype.actionStorePromiseValue= function actionStorePromiseValue( script, variableName ) {
  2525. return this.handlePotentialPromise(
  2526. this.getEval( script ), // promise
  2527. value => {
  2528. if( variableName!==undefined ) {
  2529. storedVars[variableName]= value;
  2530. }
  2531. },
  2532. true
  2533. );
  2534. };
  2535. Selenium.prototype.doStorePromiseValue= function doStorePromiseValue( script, variableName ) {
  2536. return this.actionStorePromiseValue( script, variableName );
  2537. };
  2538. Selenium.prototype.doPromise= function doPromise( script ) {
  2539. return this.actionStorePromiseValue( script );
  2540. };
  2541. Selenium.prototype.doPromise= function doPromise( script ) {
  2542. return this.actionStorePromiseValue( script );
  2543. };
  2544. var OS= Components.utils.import("resource://gre/modules/osfile.jsm", {}).OS;
  2545. var Downloads= Components.utils.import("resource://gre/modules/Downloads.jsm", {} ).Downloads;
  2546. /** Start downloading a file.
  2547. * @param {string] url Full URL.
  2548. * @return {Promise} As per https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Downloads.jsm#fetch%28%29.
  2549. * */
  2550. Selenium.download= function download( url, folder ) {
  2551. var lastIndexOfSlash= url.lastIndexOf('/');
  2552. var fileName= url.substring( lastIndexOfSlash+1 );
  2553. var indexOfQuestionmark= fileName.indexOf('?');
  2554. if( indexOfQuestionmark>0 ) {
  2555. fileName= fileName.substring( 0, indexOfQuestionmark );
  2556. }
  2557. var filePath= OS.Path.join( folder, fileName );
  2558. filePath= OS.Path.normalize( filePath );
  2559. return Downloads.fetch( url, filePath );
  2560. };
  2561. //@TODO Override/disable promiseAndWait, storePromiseValueAndWait
  2562. })();