diff --git a/packages/selenium-ide/src/content/selenium-api.js b/packages/selenium-ide/src/content/selenium-api.js index cc6480479..173c70f28 100644 --- a/packages/selenium-ide/src/content/selenium-api.js +++ b/packages/selenium-ide/src/content/selenium-api.js @@ -428,6 +428,47 @@ Selenium.prototype.doVerifyElementPresent = function(locator) { } }; +Selenium.prototype.doIf = function(string) { + return eval(string); +}; + +Selenium.prototype.doWhile = function(string) { + return eval(string); +}; + +Selenium.prototype.doTimes = function(counter) { + // return to it when 'eval' and stored variables properly figured out + return true; +}; + +Selenium.prototype.doElseIf = function(string) { + return eval(string); +}; + +Selenium.prototype.doRepeatIf = function(string) { + return eval(string); +}; + +Selenium.prototype.doElse = function() { + return true; +}; + +Selenium.prototype.doEnd = function() { + return true; +}; + +Selenium.prototype.doDo = function() { + return true; +}; + +Selenium.prototype.doContinue = function() { + return true; +}; + +Selenium.prototype.doBreak = function() { + return true; +}; + Selenium.prototype.doVerifyElementNotPresent = function(locator) { try { this.browserbot.findElement(locator); diff --git a/packages/selenium-ide/src/manifest.json b/packages/selenium-ide/src/manifest.json index 0790a93ec..0e6132102 100644 --- a/packages/selenium-ide/src/manifest.json +++ b/packages/selenium-ide/src/manifest.json @@ -51,5 +51,8 @@ "background": { "scripts": ["assets/background.js"] + }, + "sandbox": { + "pages": ["assets/sandbox.html"] } } diff --git a/packages/selenium-ide/src/neo/IO/SideeX/playback.js b/packages/selenium-ide/src/neo/IO/SideeX/playback.js index f57e7d56c..5261246e1 100644 --- a/packages/selenium-ide/src/neo/IO/SideeX/playback.js +++ b/packages/selenium-ide/src/neo/IO/SideeX/playback.js @@ -20,8 +20,11 @@ import PlaybackState, { PlaybackStates } from "../../stores/view/PlaybackState"; import UiState from "../../stores/view/UiState"; import ExtCommand, { isExtCommand } from "./ext-command"; import { xlateArgument } from "./formatCommand"; +import PlaybackTree from "./playbackTree"; +import SandboxCommand from "./sandboxCommand"; export const extCommand = new ExtCommand(); +const sandboxCommand = new SandboxCommand(); // In order to not break the separation of the execution loop from the state of the playback // I will set doSetSpeed here so that extCommand will not be aware of the state extCommand.doSetSpeed = (speed) => { @@ -37,6 +40,7 @@ let ignoreBreakpoint = false; function play(currUrl) { baseUrl = currUrl; + PlaybackTree.processCommands(PlaybackState.runningQueue); prepareToPlay() .then(executionLoop) .then(finishPlaying) @@ -51,7 +55,9 @@ function playAfterConnectionFailed() { } function executionLoop() { - (PlaybackState.currentPlayingIndex < 0) ? PlaybackState.setPlayingIndex(0) : PlaybackState.setPlayingIndex(PlaybackState.currentPlayingIndex + 1); + if (PlaybackState.currentPlayingIndex < 0) { + PlaybackState.setPlayingIndex(0); + } // reached the end if (PlaybackState.currentPlayingIndex >= PlaybackState.runningQueue.length && PlaybackState.isPlaying) return false; const { id, command, target, value, isBreakpoint } = PlaybackState.runningQueue[PlaybackState.currentPlayingIndex]; @@ -68,11 +74,14 @@ function executionLoop() { (extCommand["do" + upperCase](parsedTarget, value)) .then(() => { PlaybackState.setCommandState(id, PlaybackStates.Passed); - }).then(executionLoop) + return doControlFlow(true); + }).then(doDelay).then(executionLoop) )); } else if (isImplicitWait(command)) { notifyWaitDeprecation(command); - return executionLoop(); + return doControlFlow(true) + .then(doDelay) + .then(executionLoop); } else { return doPreparation() .then(doPrePageWait) @@ -81,10 +90,32 @@ function executionLoop() { .then(doDomWait) .then(doDelay) .then(doCommand) + .then(doControlFlow) .then(executionLoop); } } +function doControlFlow(result) { + let nextPlayingIndex; + if (result) { + // right + if (PlaybackState.runningQueue[PlaybackState.currentPlayingIndex].right) { + nextPlayingIndex = PlaybackState.runningQueue.indexOf(PlaybackState.runningQueue[PlaybackState.currentPlayingIndex].right); + } else { + nextPlayingIndex = PlaybackState.runningQueue.length + 1; + } + } else { + // left + if (PlaybackState.runningQueue[PlaybackState.currentPlayingIndex].left) { + nextPlayingIndex = PlaybackState.runningQueue.indexOf(PlaybackState.runningQueue[PlaybackState.currentPlayingIndex].left); + } else { + nextPlayingIndex = PlaybackState.runningQueue.length + 1; + } + } + PlaybackState.setPlayingIndex(nextPlayingIndex); + return Promise.resolve(); +} + function prepareToPlay() { PlaybackState.setPlayingIndex(PlaybackState.currentPlayingIndex - 1); return extCommand.init(); @@ -101,7 +132,6 @@ function finishPlaying() { function catchPlayingError(message) { if (isReceivingEndError(message)) { setTimeout(function() { - PlaybackState.setPlayingIndex(PlaybackState.currentPlayingIndex - 1); playAfterConnectionFailed(); }, 100); } else { @@ -142,7 +172,7 @@ reaction( function doPreparation() { return extCommand.sendMessage("waitPreparation", "", "") .then(function() { - return true; + return Promise.resolve(); }); } @@ -232,13 +262,21 @@ function doCommand(res, implicitTime = Date.now(), implicitCount = 0) { }, 500); }); - return p.then(() => ( - command !== "type" - ? extCommand.sendMessage(command, xlateArgument(target), xlateArgument(value), isWindowMethodCommand(command)) - : extCommand.doType(xlateArgument(target), xlateArgument(value)) - )) + return p.then(() => { + if (command === "type") { + return extCommand.doType(xlateArgument(target), xlateArgument(value)); + } else if (isConditinal(command)) { + return sandboxCommand.evalExpression(target); + } else { + return extCommand.sendMessage(command, xlateArgument(target), xlateArgument(value), isWindowMethodCommand(command)); + } + }) .then(function(result) { - if (result.result !== "success") { + if (result.result === "truthy") { + return true; + } else if (result.result === "falsy") { + return false; + } else if (result.result !== "success") { // implicit if (result.result.match(/Element[\s\S]*?not found/)) { if (implicitTime && (Date.now() - implicitTime > 30000)) { @@ -258,10 +296,15 @@ function doCommand(res, implicitTime = Date.now(), implicitCount = 0) { PlaybackState.setCommandState(id, PlaybackStates.Failed, result.result); } else { PlaybackState.setCommandState(id, PlaybackStates.Passed); + return true; } }); } +function isConditinal(commandName) { + return ["if", "while", "elseIf", "repeatIf", "times"].includes(commandName); +} + function doDelay() { return new Promise((res) => { if (PlaybackState.currentPlayingIndex + 1 === PlaybackState.runningQueue.length) { diff --git a/packages/selenium-ide/src/neo/IO/SideeX/playbackTree.js b/packages/selenium-ide/src/neo/IO/SideeX/playbackTree.js new file mode 100644 index 000000000..30029f54c --- /dev/null +++ b/packages/selenium-ide/src/neo/IO/SideeX/playbackTree.js @@ -0,0 +1,216 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + + +// For now it iterates through commands array twice. Not sure how to avoid that for now + +export default class PlaybackTree{ + constructor(commandsArray) { + this.commands = commandsArray; + this.process(); + } + + process() { + // TODO: Make sure to clone commands in order to skip re-renders on UI + let endIndexes = []; + let endBlockIndexes = []; + let blocks = []; + let doBlockIndexes = []; + + // for continue + let whileTimesIndexes = []; + let doIndexes = []; + let ifIndexes = []; + let nextRepeatIfContinueIndex = {index: undefined}; + for(let i = this.commands.length-1; i>=0; i--) { + this.inverseExtCommandSwitcher(i, endIndexes); + if (this.isControlFlowFunction(this.commands[i].command)) { + this.inverseControlFlowSwitcher(i, endBlockIndexes); + } + } + + for(let i = 0; i < this.commands.length; i++) { + if (this.isControlFlowFunction(this.commands[i].command)) { + this.controlFlowSwitcher(i, blocks, doBlockIndexes); + this.continueProcessor(i, whileTimesIndexes, doIndexes, ifIndexes, nextRepeatIfContinueIndex); + } + } + + this.executionNodes = this.commands; + } + + // won't work with embedded loops following continue. TODO: redo that + continueProcessor(i, loopIndexes, doIndexes, ifIndexes, nextRepeatIfContinueIndex) { + let loops = ["times", "while"]; + if (loops.includes(this.com(i).command)) { + loopIndexes.push(i); + } else if (this.com(i).command === "do") { + doIndexes.push(i); + } else if (this.com(i).command === "if") { + ifIndexes.push(i); + } else if (this.com(i).command === "continue") { + if (doIndexes.length === 0) { + this.com(i).setRight(this.com(loopIndexes[loopIndexes.length-1])); + this.com(i).setLeft(undefined); + } + if (loopIndexes[loopIndexes.length-1] > doIndexes[doIndexes.length-1]) { + this.com(i).setRight(this.com(loopIndexes[loopIndexes.length-1])); + this.com(i).setLeft(undefined); + } else { + nextRepeatIfContinueIndex.index = i; + } + } else if (this.com(i).command === "end") { + if (loopIndexes[loopIndexes-1] > ifIndexes[ifIndexes-1] ) { + loopIndexes.pop(); + } else { + ifIndexes.pop(); + } + } else if (this.com(i).command === "repeatIf") { + doIndexes.pop(); + if(nextRepeatIfContinueIndex.index) { + this.com(nextRepeatIfContinueIndex.index).setRight(this.com(i)); + this.com(nextRepeatIfContinueIndex.index).setLeft(undefined); + } + } + } + + inverseExtCommandSwitcher(i, endIndexes) { + if (!this.isControlFlowFunction(this.com(i).command)) { + // commands preceding 'else', 'elseIf' will point to appropriate end in the right + if (endIndexes.length > 0 && ["else", "elseIf"].includes(this.com(i+1).command)) { + this.com(i).setRight(this.com(endIndexes[endIndexes.length-1])); + } else { + this.com(i).setRight(this.com(i+1)); + } + + this.com(i).setLeft(undefined); + } else if (this.com(i).command === "end") { + endIndexes.push(i); + } else if (["if", "times", "while"].includes(this.com(i).command)) { + endIndexes.pop(); + } + } + + inverseControlFlowSwitcher(i, endBlockIndexes) { + let lastEndBlockIndex; + switch(this.com(i).command) { + case "if": + case "elseIf": + lastEndBlockIndex = endBlockIndexes.pop(); + if (this.commands[lastEndBlockIndex].command === "elseIf") { + this.com(i).setRight(this.com(i+1)); + this.com(i).setLeft(this.com(lastEndBlockIndex)); + } else { + this.com(i).setRight(this.com(i+1)); + this.com(i).setLeft(this.com(lastEndBlockIndex + 1)); + } + if (this.com(i).command === "elseIf") { + endBlockIndexes.push(i); + } + break; + case "times": + case "while": + this.com(i).setRight(this.com(i+1)); + this.com(i).setLeft(this.com(endBlockIndexes.pop() + 1)); + break; + case "else": + this.com(i).setRight(this.com(endBlockIndexes.pop() + 1)); + this.com(i).setLeft(undefined); + endBlockIndexes.push(i); + break; + case "end": + this.com(i).setLeft(undefined); + endBlockIndexes.push(i); + break; + default: + window.addLog(`Unknown control flow operator "${this.com(i).command}"`); + } + } + + controlFlowSwitcher(i, blocks, doBlockIndexes) { + if (["if", "else", "while", "elseIf", "times"].includes(this.com(i).command)) { + if (["else", "elseIf"].includes(this.com(i).command)) { + // treat if-elseif-else constructions as closed blocks + blocks.pop(); + } + blocks.push(this.com(i)); + } else if (this.com(i).command === "end") { + let lastBlock = blocks.pop(); + if(["if", "else", "elseIf"].includes(lastBlock.command)) { + this.com(i).setRight(this.com(i+1)); + } else if (lastBlock.command === "while" || lastBlock.command === "times" ) { + this.com(i).setRight(lastBlock); + } + } else if (this.com(i).command === "do") { + this.com(i).setLeft(undefined); + this.com(i).setRight(this.com(i+1)); + doBlockIndexes.push(i); + } else if (this.com(i).command === "repeatIf") { + this.com(i).setLeft(this.com(i+1)); + this.com(i).setRight(this.com(doBlockIndexes.pop())); + } else { + window.addLog(`Unknown control flow operator "${this.com(i).command}"`); + } + } + + isControlFlowFunction(command) { + switch(command) { + case "if": + case "elseIf": + case "else": + case "while": + case "times": + case "repeatIf": + case "do": + case "end": + case "continue": + case "break": + return true; + default: + return false; + } + } + + com(index) { + return this.commands[index]; + } + + static processCommands(commandsArray) { + let a = new PlaybackTree(commandsArray); + a.maintenance(); + } + + // TODO: maintenance function: remove when possible + maintenance() { + // runCommand(this.executionNodes[0]); + window.addLog("maintenance"); + this.executionNodes.forEach((node) => { + window.addLog(`[[ Command: ]] ${node.command}`); + if (node.left) { + window.addLog(`[[ Command left direction: ]] ${node.left.command}`); + } else { + window.addLog(`[[ Command left direction: ]] not defined`); + } + if (node.right) { + window.addLog(`[[ Command right direction: ]] ${node.right.command}`); + } else { + window.addLog(`[[ Command right direction: ]] not defined`); + } + window.addLog(`----------------------`); + }); + } +} diff --git a/packages/selenium-ide/src/neo/IO/SideeX/sandboxCommand.js b/packages/selenium-ide/src/neo/IO/SideeX/sandboxCommand.js new file mode 100644 index 000000000..268428ddd --- /dev/null +++ b/packages/selenium-ide/src/neo/IO/SideeX/sandboxCommand.js @@ -0,0 +1,81 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { xlateArgument } from "./formatCommand"; + +export default class SandboxCommand { + constructor() { + this.result = null; + this.iframe = document.getElementById("evalSandboxFrame"); + this.attachWatcher = this.attachWatcher.bind(this); + this.attachWatcher(); + } + + attachWatcher() { + window.addEventListener("message", (event) => { + if (event.data.evaluationResult) { + this.result = event.data.evaluationResult; + } + }); + } + + wait(...properties) { + if (!properties.length) + return Promise.reject("No arguments"); + let self = this; + let ref = this; + let inspecting = properties[properties.length - 1]; + for (let i = 0; i < properties.length - 1; i++) { + if (!ref[properties[i]] | !(ref[properties[i]] instanceof Array | ref[properties[i]] instanceof Object)) + return Promise.reject("Invalid Argument"); + ref = ref[properties[i]]; + } + return new Promise(function(resolve, reject) { + let counter = 0; + let interval = setInterval(function() { + if (ref[inspecting] === undefined || ref[inspecting] === false) { + counter++; + if (counter > self.waitTimes) { + reject("Timeout"); + clearInterval(interval); + } + } else { + resolve(); + clearInterval(interval); + } + }, self.waitInterval); + }); + } + + evalExpression(expression) { + // redo after + const message = { + command: "evaluateCommand", + evaluationCommand: xlateArgument(expression) + }; + this.iframe.contentWindow.postMessage(message, "*"); + return this.wait("result") + .then(() => { + let result = "falsy"; + if (this.result) { + result = "truthy"; + } + this.result = null; + return Promise.resolve({result: result}); + }); + } +}; diff --git a/packages/selenium-ide/src/neo/IO/SideeX/suiteValidator.js b/packages/selenium-ide/src/neo/IO/SideeX/suiteValidator.js new file mode 100644 index 000000000..11d6ee85d --- /dev/null +++ b/packages/selenium-ide/src/neo/IO/SideeX/suiteValidator.js @@ -0,0 +1,137 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +export default class SuiteValidator{ + constructor(commandNamesArray) { + this.blocksStack = []; + this.commandNamesArray = commandNamesArray; + this.error = {}; + this.process(); + } + + process() { + for(let i = 0; i < this.commandNamesArray.length; i++) { + const commandName = this.commandNamesArray[i]; + if(!this.isControlFlowFunction(commandName)) continue; + if (!["end", "repeatIf"].includes(commandName)) { + this.processBlock(commandName, i); + } else { + this.processBlockEnd(commandName, i); + } + if(this.isIntermediateValid() !== true) break; + } + } + + processBlock(command, index) { + if(["else", "elseIf"].includes(command)) { + if (this.isIfElseOpen()) { + this.pushToStack(command); + } else { + this.error = {index: index, error: "Incorrect if-elseIf-else block"}; + } + } else if (["continue", "break"].includes(command)) { + if (!this.isLoopOpen()) { + this.error = {index: index, error: "'continue' and 'break' can only be called inside loop"}; + } + } else { + this.pushToStack(command); + } + } + + isIfElseOpen() { + const lastIfCommandIndex = this.blocksStack.lastIndexOf("if"); + const lastElseCommandIndex = this.blocksStack.lastIndexOf("else"); + const ifPresent = lastIfCommandIndex > -1; + return ifPresent && lastElseCommandIndex < lastIfCommandIndex; + } + + isLoopOpen() { + let loopIsPresent = false; + ["times", "do", "while"].forEach((loop) => { + if (this.blocksStack.includes(loop)) { + loopIsPresent = true; + } + }); + return loopIsPresent; + } + + processBlockEnd(command, i) { + if(this.blocksStack.length === 0) { + this.error = {index: i, error: "Error with the syntax"}; + } + if(command === "end") { + this.closeEndBlock(i); + } else { + this.closeRepeatIfBlock(i); + } + } + + closeEndBlock(index) { + if(["else", "elseIf"].includes(this.blocksStack[this.blocksStack.length-1])) { + // remove all of the if-elseIf-else commands from the stack + const lastIfCommandIndex = this.blocksStack.lastIndexOf("if"); + this.blocksStack.splice(lastIfCommandIndex, this.blocksStack.length-lastIfCommandIndex); + } else if (this.blocksStack[this.blocksStack.length-1] === "do") { + this.error = {index: index, error: "'do' must be enclosed with 'repeatIf'"}; + } else { + this.blocksStack.pop(); + } + } + + closeRepeatIfBlock(index) { + if (this.blocksStack[this.blocksStack.length-1] === "do") { + this.blocksStack.pop(); + } else { + this.error = {index: index, error: "'do' must be enclosed with 'repeatIf'"}; + } + } + + pushToStack(command) { + this.blocksStack.push(command); + } + + isIntermediateValid() { + if(this.error["error"]) { + return this.error; + } else { + return true; + } + } + + findLastCommand(commandName) { + return this.commandNamesArray.lastIndexOf(commandName); + } + + isValid() { + if(this.blocksStack.length > 0) { + this.error = {index: this.findLastCommand(this.blocksStack[this.blocksStack.length-1]), error: "Error with the syntax"}; + } + return this.isIntermediateValid(); + } + + isControlFlowFunction(command) { + return ["if", "elseIf", "else", "while", + "times", "repeatIf", "do", "end", + "continue", "break"].includes(command); + } + + static validateCommands(commandsArray) { + let validator = new SuiteValidator(commandsArray); + return validator.isValid(); + } +} + diff --git a/packages/selenium-ide/src/neo/__test__/IO/suiteValidator.spec.js b/packages/selenium-ide/src/neo/__test__/IO/suiteValidator.spec.js new file mode 100644 index 000000000..1f2cb69d2 --- /dev/null +++ b/packages/selenium-ide/src/neo/__test__/IO/suiteValidator.spec.js @@ -0,0 +1,153 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import SuiteValidator from "../../IO/SideeX/suiteValidator"; + +describe("SuiteValidator if blocks", () => { + it("should treat as valid if-elseif-else-end block", () => { + expect(SuiteValidator.validateCommands(["if", "elseIf", "else", "end"])).toBe(true); + }); + + it("should treat as valid if-elseif-else-end block with multiple elseIf", () => { + expect(SuiteValidator.validateCommands(["if", "elseIf", "elseIf", "elseIf", "else", "end"])).toBe(true); + }); + + it("should treat as valid if-elseif-end block", () => { + expect(SuiteValidator.validateCommands(["if", "else", "end"])).toBe(true); + }); + + it("should treat as valid if-end block", () => { + expect(SuiteValidator.validateCommands(["if", "end"])).toBe(true); + }); + + it("should treat as valid if-elseif-end block", () => { + expect(SuiteValidator.validateCommands(["if", "elseIf", "elseIf", "elseIf", "end"])).toBe(true); + }); + + it("should treat as valid if-end block with embedded loops", () => { + expect(SuiteValidator.validateCommands(["if", "while", "end", "times", "end", "end"])).toBe(true); + }); + + it("should treat as valid if-elseif-end block with embedded do-repeatIf loop", () => { + expect(SuiteValidator.validateCommands(["if", "else", "do", "blob", "repeatIf", "end"])).toBe(true); + }); + + it("should treat as valid if-elseif-end block with embedded loop", () => { + expect(SuiteValidator.validateCommands(["if", "elseIf", "while", "blob", "end", "elseIf", "elseIf", "end"])).toBe(true); + }); + + it("should treat as valid while-end block", () => { + expect(SuiteValidator.validateCommands(["while", "blob", "end"])).toBe(true); + }); + + it("should treat as valid while-end block with embedded if-else-end", () => { + expect(SuiteValidator.validateCommands(["while", "if", "blob", "else", "end", "end"])).toBe(true); + }); + + it("should treat as valid while-end block with embedded times-end ", () => { + expect(SuiteValidator.validateCommands(["while", "blob", "times", "blob", "end", "end"])).toBe(true); + }); + + it("should treat as valid do-repeatIf block", () => { + expect(SuiteValidator.validateCommands(["do", "blob", "repeatIf"])).toBe(true); + }); + + it("should treat as valid do-repeatIf block with embedded if-else-end", () => { + expect(SuiteValidator.validateCommands(["do", "if", "blob", "else", "end", "repeatIf"])).toBe(true); + }); + + it("should treat as valid do-repeatIf block with embedded times-end ", () => { + expect(SuiteValidator.validateCommands(["do", "blob", "times", "blob", "end", "repeatIf"])).toBe(true); + }); + + it("should treat as valid do-repeatIf block with embedded continue", () => { + expect(SuiteValidator.validateCommands(["do", "if", "continue", "end", "repeatIf"])).toBe(true); + }); + + it("should treat as valid do-repeatIf block with embedded continue with embedded times-end with embedded continue", () => { + expect(SuiteValidator.validateCommands(["do", "blob", "times", "blob", "continue", "end", "continue", "repeatIf"])).toBe(true); + }); + + it("should treat as valid while-end block with embedded continue", () => { + expect(SuiteValidator.validateCommands(["while", "if", "blob", "continue", "else", "continue", "end", "end"])).toBe(true); + }); + + it("should treat as valid while-end block with embedded break", () => { + expect(SuiteValidator.validateCommands(["while", "if", "blob", "break", "else", "continue", "end", "end"])).toBe(true); + }); +}); + +describe("SuiteValidator failure", () => { + it("should fail if-else-elseIf-end block", () => { + expect(SuiteValidator.validateCommands(["if", "else", "elseIf", "end"])).not.toBe(true); + }); + + it("should fail if-else block", () => { + expect(SuiteValidator.validateCommands(["if", "else"])).not.toBe(true); + }); + + it("should fail if-elseif-else-end block with unclosed loop", () => { + expect(SuiteValidator.validateCommands(["if", "elseIf", "else", "while" , "end"])).not.toBe(true); + }); + + it("should fail if-elseif-else-end block with unclosed do loop", () => { + expect(SuiteValidator.validateCommands(["if", "elseIf", "else", "do" , "end"])).not.toBe(true); + }); + + it("should fail if-elseif-else-end block with unclosed repeatIf loop", () => { + expect(SuiteValidator.validateCommands(["if", "elseIf", "else", "repeatIf" , "end"])).not.toBe(true); + }); + + it("should fail while-end block with embedded if-else", () => { + expect(SuiteValidator.validateCommands(["while", "if", "blob", "else", "end"])).not.toBe(true); + }); + + it("should fail while-end block with embedded unclosed times", () => { + expect(SuiteValidator.validateCommands(["while", "blob", "times", "blob", "end"])).not.toBe(true); + }); + + it("should fail while-end block with embedded unclosed repeatIf", () => { + expect(SuiteValidator.validateCommands(["while", "blob", "repeatIf", "blob", "end"])).not.toBe(true); + }); + + it("should fail do-repeatIf block", () => { + expect(SuiteValidator.validateCommands(["do", "blob", "end"])).not.toBe(true); + }); + + it("should fail do-repeatIf block with embedded if-else", () => { + expect(SuiteValidator.validateCommands(["do", "if", "blob", "else", "repeatIf"])).not.toBe(true); + }); + + it("should fail do-repeatIf block with embedded times", () => { + expect(SuiteValidator.validateCommands(["do", "blob", "times", "blob", "repeatIf"])).not.toBe(true); + }); + + it("should fail do-repeatIf block with embedded times-end", () => { + expect(SuiteValidator.validateCommands(["do", "blob", "times", "blob", "end"])).not.toBe(true); + }); + + + it("should fail with continue outside of the loop", () => { + expect(SuiteValidator.validateCommands(["continue", "do", "if", "blob", "else", "end", "repeatIf"])).not.toBe(true); + }); + + + it("should fail with break outside of the loop", () => { + expect(SuiteValidator.validateCommands(["break", "do", "if", "blob", "else", "end", "repeatIf"])).not.toBe(true); + }); + +}); diff --git a/packages/selenium-ide/src/neo/components/TestRow/index.jsx b/packages/selenium-ide/src/neo/components/TestRow/index.jsx index c09933eb9..2efc2f064 100644 --- a/packages/selenium-ide/src/neo/components/TestRow/index.jsx +++ b/packages/selenium-ide/src/neo/components/TestRow/index.jsx @@ -106,6 +106,7 @@ class TestRow extends React.Component { command: PropTypes.string.isRequired, target: PropTypes.string, value: PropTypes.string, + level: PropTypes.number, isBreakpoint: PropTypes.bool, toggleBreakpoint: PropTypes.func, onClick: PropTypes.func, @@ -200,6 +201,7 @@ class TestRow extends React.Component { //setting component of context menu. this.props.setContextMenu(listMenu); + const index = this.props.index >= 0 ? {this.props.index + 1}. : null; const rendered =