diff --git a/packages/selenium-ide/src/content/bootstrap.html b/packages/selenium-ide/src/content/bootstrap.html index ea9c2ef25..5b5ddcb2e 100644 --- a/packages/selenium-ide/src/content/bootstrap.html +++ b/packages/selenium-ide/src/content/bootstrap.html @@ -21,4 +21,25 @@

Preparing to run your test

+ diff --git a/packages/selenium-ide/src/content/commands-api.js b/packages/selenium-ide/src/content/commands-api.js index e5053e63f..9afc86f6b 100644 --- a/packages/selenium-ide/src/content/commands-api.js +++ b/packages/selenium-ide/src/content/commands-api.js @@ -42,32 +42,39 @@ function doCommands(request, sender, sendResponse) { } else if (request.commands == "domWait") { selenium["doDomWait"]("", selenium.preprocessParameter("")); sendResponse({ dom_time: window.sideex_new_page }); + } else if (request.commands === "evaluateConditional") { + try { + let value = selenium["doEvaluateConditional"](request.target); + sendResponse({ result: "success", value: value }); + } catch(e) { + sendResponse({ result: e.message }); + } } else { const upperCase = request.commands.charAt(0).toUpperCase() + request.commands.slice(1); if (selenium["do" + upperCase] != null) { try { document.body.setAttribute("SideeXPlayingFlag", true); - let returnValue = selenium["do"+upperCase](selenium.preprocessParameter(request.target),selenium.preprocessParameter(request.value)); + let returnValue = selenium["do" + upperCase](selenium.preprocessParameter(request.target), selenium.preprocessParameter(request.value)); if (returnValue instanceof Promise) { // The command is a asynchronous function returnValue.then(function() { // Asynchronous command completed successfully document.body.removeAttribute("SideeXPlayingFlag"); - sendResponse({result: "success"}); + sendResponse({ result: "success" }); }).catch(function(reason) { // Asynchronous command failed document.body.removeAttribute("SideeXPlayingFlag"); - sendResponse({result: reason}); + sendResponse({ result: reason }); }); } else { // Synchronous command completed successfully document.body.removeAttribute("SideeXPlayingFlag"); - sendResponse({result: "success"}); + sendResponse({ result: "success" }); } } catch(e) { // Synchronous command failed document.body.removeAttribute("SideeXPlayingFlag"); - sendResponse({result: e.message}); + sendResponse({ result: e.message }); } } else { sendResponse({ result: "Unknown command: " + request.commands }); @@ -84,9 +91,9 @@ function doCommands(request, sender, sendResponse) { try { const element = selenium.browserbot.findElement(request.locator); const locator = locatorBuilders.buildAll(element).find(([loc, strat]) => (/^xpath/.test(strat)))[0]; //eslint-disable-line no-unused-vars - sendResponse({result: "success", locator}); + sendResponse({ result: "success", locator }); } catch(e) { - sendResponse({result: e.message}); + sendResponse({ result: e.message }); } } if (request.selectMode) { diff --git a/packages/selenium-ide/src/content/selenium-api.js b/packages/selenium-ide/src/content/selenium-api.js index 5fe464c5d..6e2568efa 100644 --- a/packages/selenium-ide/src/content/selenium-api.js +++ b/packages/selenium-ide/src/content/selenium-api.js @@ -228,8 +228,16 @@ Selenium.prototype.reset = function() { this.browserbot.resetPopups(); }; -Selenium.prototype.eval = function(script, scoped = true) { - return window.eval(scoped ? `(() => {${script}})()` : script); +Selenium.prototype.eval = function(script, scoped = true, isExpression = false) { + if (isExpression) { + return window.eval(scoped ? `(() => (${script}))()` : script); + } else { + return window.eval(scoped ? `(() => {${script}})()` : script); + } +}; + +Selenium.prototype.doEvaluateConditional = function(condition) { + return !!(this.eval(condition, true, true)); }; Selenium.prototype.doVerifyChecked = function(locator) { @@ -331,6 +339,16 @@ Selenium.prototype.doVerifyElementNotPresent = function(locator) { } }; +Selenium.prototype.doVerify = function(actual, expected) { + this.doAssert(actual, expected); +}; + +Selenium.prototype.doAssert = function(actual, expected) { + if (actual !== expected) { + throw new Error("Actual value '" + actual + "' did not match '" + expected + "'"); + } +}; + Selenium.prototype.doAssertChecked = function(locator) { let element = this.browserbot.findElement(locator); if (element.type !== "checkbox" && element.type !== "radio") { diff --git a/packages/selenium-ide/src/manifest.json b/packages/selenium-ide/src/manifest.json index d796eee70..0c3c93a73 100644 --- a/packages/selenium-ide/src/manifest.json +++ b/packages/selenium-ide/src/manifest.json @@ -42,7 +42,7 @@ "assets/prompt.css", "assets/bootstrap.html" ], - "content_security_policy": "script-src 'self'; object-src 'self'", + "content_security_policy": "script-src 'self' 'sha256-UUwVIygWwzdom+NTsGSMgQnlQ8Phnj5BIK5+S3HWX4Y=' 'unsafe-eval'; object-src 'self'", "content_scripts": [ { "matches": [ @@ -60,9 +60,8 @@ "all_frames": true } ], - "background": { - "scripts": [ - "assets/background.js" - ] + "background": + { + "scripts": ["assets/background.js"] } -} \ No newline at end of file +} diff --git a/packages/selenium-ide/src/neo/IO/ControlFlowValidator.js b/packages/selenium-ide/src/neo/IO/ControlFlowValidator.js deleted file mode 100644 index 0f8a1f512..000000000 --- a/packages/selenium-ide/src/neo/IO/ControlFlowValidator.js +++ /dev/null @@ -1,91 +0,0 @@ -// 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 ControlFlowValidator { - constructor(commandStack) { - this.commandStack = commandStack; - this.commandKeywordPairs = [ - ["if", "end"], - ["while", "endWhile"] - ]; - } - - process() { - if (this.isBoundByACompleteSegment()) { - if (this.hasIncompleteCommandSegments()) { - return false; - } else { - return true; - } - } else if (this.hasMoreThanOneCompleteSegment()) { - if (this.segmentsContainIncompleteSegments()) { - return false; - } else { - return true; - } - } else { - return false; - } - } - - segmentsContainIncompleteSegments() { - //let keywordIndices = []; - //let stack = this.commandStack; - //this.commandKeywordPairs.forEach(function(keywordPair) { - // keywordIndices.push([ - // stack.indexOf(keywordPair[0]), - // stack.indexOf(keywordPair[1]) - // ]); - //}); - //console.log(keywordIndices); - const ifIndex = this.commandStack.indexOf("if"); - const endIndex = this.commandStack.indexOf("end"); - const segment1 = this.commandStack.slice(ifIndex, endIndex + 1); - const whileIndex = this.commandStack.indexOf("while"); - const endWhileIndex = this.commandStack.indexOf("endWhile"); - const segment2 = this.commandStack.slice(whileIndex, endWhileIndex + 1); - return (this.hasIncompleteCommandSegments(segment1) || this.hasIncompleteCommandSegments(segment2)); - } - - startsWith(command) { - return this.commandStack[0] === command; - } - - endsWith(command) { - return this.commandStack[this.commandStack.length - 1] === command; - } - - commandCount(commandName, segment) { - const stack = (segment ? segment : this.commandStack); - return stack.filter(command => command === commandName).length; - } - - isBoundByACompleteSegment() { - return (this.startsWith("if") && this.endsWith("end") || - this.startsWith("while") && this.endsWith("endWhile")); - } - - hasIncompleteCommandSegments(segment) { - return (this.commandCount("if", segment) !== this.commandCount("end", segment) || - this.commandCount("while", segment) !== this.commandCount("endWhile", segment)); - } - - hasMoreThanOneCompleteSegment() { - return ((this.commandCount("if") > 0 && this.commandCount("if") === this.commandCount("end")) && - (this.commandCount("while") > 0 && this.commandCount("while") === this.commandCount("endWhile"))); - } -} diff --git a/packages/selenium-ide/src/neo/IO/SideeX/ext-command.js b/packages/selenium-ide/src/neo/IO/SideeX/ext-command.js index f607dbbb2..c99301182 100644 --- a/packages/selenium-ide/src/neo/IO/SideeX/ext-command.js +++ b/packages/selenium-ide/src/neo/IO/SideeX/ext-command.js @@ -370,20 +370,30 @@ export default class ExtCommand { let upperCase = command.charAt(0).toUpperCase() + command.slice(1); return "do" + upperCase; } -} -export function isExtCommand(command) { - switch(command) { - case "pause": - case "open": - case "selectFrame": - case "selectWindow": - case "run": - case "setSpeed": - case "store": - case "close": - return true; - default: - return false; + isExtCommand(command) { + switch(command) { + case "pause": + case "open": + case "selectFrame": + case "selectWindow": + case "run": + case "setSpeed": + case "store": + case "close": + return true; + default: + return false; + } + } + + isWindowMethodCommand(command) { + return (command == "answerOnNextPrompt" + || command == "chooseCancelOnNextPrompt" + || command == "assertPrompt" + || command == "chooseOkOnNextConfirmation" + || command == "chooseCancelOnNextConfirmation" + || command == "assertConfirmation" + || command == "assertAlert"); } } diff --git a/packages/selenium-ide/src/neo/IO/SideeX/playback.js b/packages/selenium-ide/src/neo/IO/SideeX/playback.js index ad75d84ab..ad199b66e 100644 --- a/packages/selenium-ide/src/neo/IO/SideeX/playback.js +++ b/packages/selenium-ide/src/neo/IO/SideeX/playback.js @@ -20,9 +20,10 @@ import FatalError from "../../../errors/fatal"; import NoResponseError from "../../../errors/no-response"; import PlaybackState, { PlaybackStates } from "../../stores/view/PlaybackState"; import UiState from "../../stores/view/UiState"; -import { canExecuteCommand, executeCommand } from "../../../plugin/commandExecutor"; -import ExtCommand, { isExtCommand } from "./ext-command"; -import { xlateArgument } from "./formatCommand"; +import { canExecuteCommand } from "../../../plugin/commandExecutor"; +import ExtCommand from "./ext-command"; +import { createPlaybackTree } from "../../playback/playback-tree"; +import { ControlFlowCommandChecks } from "../../models/Command"; export const extCommand = new ExtCommand(); // In order to not break the separation of the execution loop from the state of the playback @@ -41,12 +42,32 @@ let ignoreBreakpoint = false; function play(currUrl) { baseUrl = currUrl; ignoreBreakpoint = false; + initPlaybackTree(); prepareToPlay() .then(executionLoop) .then(finishPlaying) .catch(catchPlayingError); } +function initPlaybackTree() { + try { + if (PlaybackState.runningQueue.length === 1 && + ControlFlowCommandChecks.isControlFlow(PlaybackState.runningQueue[0].command)) { + reportError( + "Unable to execute control flow command by itself. You can execute this \ + command by running the entire test or by right-clicking on the command \ + and selecting 'Play from here'.", + false, + 0); + } else { + let playbackTree = createPlaybackTree(PlaybackState.runningQueue); + PlaybackState.setCurrentExecutingCommandNode(playbackTree.startingCommandNode); + } + } catch (error) { + reportError(error.message, false, error.index); + } +} + function playAfterConnectionFailed() { prepareToPlayAfterConnectionFailed() .then(executionLoop) @@ -55,34 +76,18 @@ function playAfterConnectionFailed() { } function didFinishQueue() { - return (PlaybackState.currentPlayingIndex >= PlaybackState.runningQueue.length && PlaybackState.isPlaying); + return !PlaybackState.currentExecutingCommandNode; } function isStopping() { return (!PlaybackState.isPlaying || PlaybackState.paused || PlaybackState.isStopping); } -function incrementPlayingIndex() { - if (PlaybackState.currentPlayingIndex < 0) { - PlaybackState.setPlayingIndex(0); - } else { - PlaybackState.setPlayingIndex(PlaybackState.currentPlayingIndex + 1); - } -} - -function isCallStackEmpty() { - return !PlaybackState.callstack.length; -} - function executionLoop() { - incrementPlayingIndex(); - if (didFinishQueue() && !isCallStackEmpty()) { - PlaybackState.unwindTestCase(); - return executionLoop(); - } else if (isStopping() || didFinishQueue()) { + if (isStopping() || didFinishQueue()) { return false; } - const command = PlaybackState.runningQueue[PlaybackState.currentPlayingIndex]; + const command = PlaybackState.currentExecutingCommandNode.command; const stackIndex = PlaybackState.callstack.length ? PlaybackState.callstack.length - 1 : undefined; if (!command.command) return executionLoop(); // breakpoint @@ -91,14 +96,15 @@ function executionLoop() { else if (ignoreBreakpoint) ignoreBreakpoint = false; // paused if (isStopping()) return false; - if (isExtCommand(command.command)) { - return doDelay().then(() => ( - (extCommand[extCommand.name(command.command)](xlateArgument(command.target), xlateArgument(command.value))) - .then(() => { + if (extCommand.isExtCommand(command.command)) { + return doDelay().then(() => { + return (PlaybackState.currentExecutingCommandNode.execute(extCommand)) + .then((result) => { // we need to set the stackIndex manually because run command messes with that PlaybackState.setCommandStateAtomically(command.id, stackIndex, PlaybackStates.Passed); - }).then(executionLoop) - )); + PlaybackState.setCurrentExecutingCommandNode(result.next); + }).then(executionLoop); + }); } else if (isImplicitWait(command)) { notifyWaitDeprecation(command); return executionLoop(); @@ -115,7 +121,6 @@ function executionLoop() { } function prepareToPlay() { - PlaybackState.setPlayingIndex(PlaybackState.currentPlayingIndex - 1); return extCommand.init(baseUrl); } @@ -130,7 +135,6 @@ function finishPlaying() { function catchPlayingError(message) { if (isReceivingEndError(message)) { setTimeout(function() { - PlaybackState.setPlayingIndex(PlaybackState.currentPlayingIndex - 1); playAfterConnectionFailed(); }, 100); } else { @@ -139,8 +143,13 @@ function catchPlayingError(message) { } } -function reportError(error, nonFatal) { - const { id } = PlaybackState.runningQueue[PlaybackState.currentPlayingIndex]; +function reportError(error, nonFatal, index) { + let id; + if (!isNaN(index)) { + id = PlaybackState.runningQueue[index].id; + } else if (PlaybackState.currentExecutingCommandNode) { + id = PlaybackState.currentExecutingCommandNode.command.id; + } let message = error; if (error.message === "this.playingFrameLocations[this.currentPlayingTabId] is undefined") { message = "The current tab is invalid for testing (e.g. about:home), surf to a webpage before using the extension"; @@ -161,7 +170,6 @@ reaction( () => PlaybackState.paused, paused => { if (!paused) { - PlaybackState.setPlayingIndex(PlaybackState.currentPlayingIndex - 1); ignoreBreakpoint = true; playAfterConnectionFailed(); } @@ -242,7 +250,7 @@ function doDomWait(res, domTime, domCount = 0) { function doCommand(res, implicitTime = Date.now(), implicitCount = 0) { if (!PlaybackState.isPlaying || PlaybackState.paused) return; - const { id, command, target, value } = PlaybackState.runningQueue[PlaybackState.currentPlayingIndex]; + const { id, command, target, value } = PlaybackState.currentExecutingCommandNode.command; let p = new Promise(function(resolve, reject) { let count = 0; @@ -265,28 +273,31 @@ function doCommand(res, implicitTime = Date.now(), implicitCount = 0) { canExecuteCommand(command) ? doPluginCommand(id, command, target, value, implicitTime, implicitCount) : doSeleniumCommand(id, command, target, value, implicitTime, implicitCount) - )); + )).then(result => { + PlaybackState.setCurrentExecutingCommandNode(result.next); + }); } function doSeleniumCommand(id, command, target, value, implicitTime, implicitCount) { - return (command !== "type" - ? extCommand.sendMessage(command, xlateArgument(target), xlateArgument(value), isWindowMethodCommand(command)) - : extCommand.doType(xlateArgument(target), xlateArgument(value), isWindowMethodCommand(command))).then(function(result) { + return PlaybackState.currentExecutingCommandNode.execute(extCommand).then(function(result) { if (result.result !== "success") { // implicit if (isElementNotFound(result.result)) { return doImplicitWait(result.result, id, target, implicitTime, implicitCount); } else { - PlaybackState.setCommandState(id, /^verify/.test(command) ? PlaybackStates.Failed : PlaybackStates.Fatal, result.result); + let isVerify = /^verify/.test(command); + PlaybackState.setCommandState(id, isVerify ? PlaybackStates.Failed : PlaybackStates.Fatal, result.result); + return result; } } else { PlaybackState.setCommandState(id, PlaybackStates.Passed); + return result; } }); } function doPluginCommand(id, command, target, value, implicitTime, implicitCount) { - return executeCommand(command, target, value, { + return PlaybackState.currentExecutingCommandNode.execute(extCommand, { commandId: id, isNested: !!PlaybackState.callstack.length, runId: PlaybackState.runId, @@ -294,8 +305,13 @@ function doPluginCommand(id, command, target, value, implicitTime, implicitCount frameId: extCommand.getCurrentPlayingFrameId(), tabId: extCommand.currentPlayingTabId, windowId: extCommand.currentPlayingWindowId - }).then(res => { - PlaybackState.setCommandState(id, res.status ? res.status : PlaybackStates.Passed, res && res.message || undefined); + }).then(result => { + PlaybackState.setCommandState( + id, + result.status ? result.status : PlaybackStates.Passed, + result && result.message || undefined + ); + return result; }).catch(err => { if (isElementNotFound(err.message)) { return doImplicitWait(err.message, id, target, implicitTime, implicitCount); @@ -331,7 +347,7 @@ function doImplicitWait(error, commandId, target, implicitTime, implicitCount) { function doDelay() { return new Promise((res) => { - if (PlaybackState.currentPlayingIndex + 1 === PlaybackState.runningQueue.length) { + if (PlaybackState.currentExecutingCommandNode.next === undefined) { return res(true); } else { setTimeout(() => { @@ -352,17 +368,8 @@ function isReceivingEndError(reason) { reason.message == "Could not establish connection. Receiving end does not exist." || // Google Chrome misspells "response" reason.message == "The message port closed before a reponse was received." || - reason.message == "The message port closed before a response was received." ); -} - -function isWindowMethodCommand(command) { - return (command == "answerOnNextPrompt" - || command == "chooseCancelOnNextPrompt" - || command == "assertPrompt" - || command == "chooseOkOnNextConfirmation" - || command == "chooseCancelOnNextConfirmation" - || command == "assertConfirmation" - || command == "assertAlert"); + reason.message == "The message port closed before a response was received." || + reason.message == "result is undefined"); // from command node eval } function isImplicitWait(command) { diff --git a/packages/selenium-ide/src/neo/__test__/IO/ControlFlowValidator.spec.js b/packages/selenium-ide/src/neo/__test__/IO/ControlFlowValidator.spec.js deleted file mode 100644 index ac6aafb69..000000000 --- a/packages/selenium-ide/src/neo/__test__/IO/ControlFlowValidator.spec.js +++ /dev/null @@ -1,134 +0,0 @@ -// 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 ControlFlowValidator from "../../IO/ControlFlowValidator"; - -describe("Control Flow Validations", () => { - test("if-end block", () => { - let validator = new ControlFlowValidator(["if", "end"]); - expect(validator.process()).toBeTruthy(); - }); - - test("if-if-end-end block", () => { - let validator = new ControlFlowValidator(["if", "if", "end", "end"]); - expect(validator.process()).toBeTruthy(); - }); - - test("if-else-end block", () => { - let validator = new ControlFlowValidator(["if", "else", "end"]); - expect(validator.process()).toBeTruthy(); - }); - - test("if-elseif-end block", () => { - let validator = new ControlFlowValidator(["if", "elseIf", "end"]); - expect(validator.process()).toBeTruthy(); - }); - - test("if-elseif-elseif-elseif-end block", () => { - let validator = new ControlFlowValidator(["if", "elseIf", "elseIf", "elseIf", "end"]); - expect(validator.process()).toBeTruthy(); - }); - - test("if-while-endwhile-end block", () => { - let validator = new ControlFlowValidator(["if", "while", "endWhile", "end"]); - expect(validator.process()).toBeTruthy(); - }); - - test("while-endwhile block", () => { - let validator = new ControlFlowValidator(["while", "endWhile"]); - expect(validator.process()).toBeTruthy(); - }); - - test("while-if-end-endwhile block", () => { - let validator = new ControlFlowValidator(["while", "if", "end", "endWhile"]); - expect(validator.process()).toBeTruthy(); - }); - - test("if-end-while-endwhile block", () => { - let validator = new ControlFlowValidator(["if", "end", "while", "endWhile"]); - expect(validator.process()).toBeTruthy(); - }); -}); - -describe("Control Flow Invalidations", () => { - test.skip("returns command that invalidates segment", () => { - }); - - test.skip("returns commands that invalidate segment", () => { - }); - - test("if block", () => { - let validator = new ControlFlowValidator(["if"]); - expect(validator.process()).toBeFalsy(); - }); - - test("end-if block", () => { - let validator = new ControlFlowValidator(["end", "if"]); - expect(validator.process()).toBeFalsy(); - }); - - test("if-if-end block", () => { - let validator = new ControlFlowValidator(["if", "if", "end"]); - expect(validator.process()).toBeFalsy(); - }); - - test("else-if-end block", () => { - let validator = new ControlFlowValidator(["else", "if", "end"]); - expect(validator.process()).toBeFalsy(); - }); - - test("if-while-end-endwhile block", () => { - let validator = new ControlFlowValidator(["if", "while", "end", "endWhile"]); - expect(validator.process()).toBeFalsy(); - }); - - test("while-if-endwhile-end block", () => { - let validator = new ControlFlowValidator(["while", "if", "endWhile", "end"]); - expect(validator.process()).toBeFalsy(); - }); - - test("while-if-endwhile block", () => { - let validator = new ControlFlowValidator(["while", "if", "endWhile"]); - expect(validator.process()).toBeFalsy(); - }); - - test("while block", () => { - let validator = new ControlFlowValidator(["while"]); - expect(validator.process()).toBeFalsy(); - }); - - test("if-while-end block", () => { - let validator = new ControlFlowValidator(["if", "while", "end"]); - expect(validator.process()).toBeFalsy(); - }); - - test("if-end-while-if-endwhile block", () => { - let validator = new ControlFlowValidator(["if", "end", "while", "if", "endWhile"]); - expect(validator.process()).toBeFalsy(); - }); - - test.skip("if-else-elseif-end block", () => { - let validator = new ControlFlowValidator(["if", "else", "elseIf", "end"]); - expect(validator.process()).toBeFalsy(); - }); - - test.skip("while-else-endwhile block", () => { - let validator = new ControlFlowValidator(["while", "else", "endWhile"]); - expect(validator.process()).toBeFalsy(); - }); - -}); diff --git a/packages/selenium-ide/src/neo/__test__/playback/playback-tree/command-leveler.spec.js b/packages/selenium-ide/src/neo/__test__/playback/playback-tree/command-leveler.spec.js new file mode 100644 index 000000000..ac2746715 --- /dev/null +++ b/packages/selenium-ide/src/neo/__test__/playback/playback-tree/command-leveler.spec.js @@ -0,0 +1,60 @@ +// 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 Command, { ControlFlowCommandNames } from "../../../models/Command"; +import { deriveCommandLevels } from "../../../playback/playback-tree/command-leveler"; + +function createCommand(name) { + return new Command(null, name, "", ""); +} + +describe("Control Flow", () => { + describe("Preprocess", () => { + describe("Leveling", () => { + test("returns leveled command stack", () => { + let stack = deriveCommandLevels([ + createCommand(ControlFlowCommandNames.if), + createCommand("command"), + createCommand(ControlFlowCommandNames.else), + createCommand(ControlFlowCommandNames.while), + createCommand("command"), + createCommand(ControlFlowCommandNames.end), + createCommand(ControlFlowCommandNames.do), + createCommand("command"), + createCommand(ControlFlowCommandNames.while), + createCommand("command"), + createCommand(ControlFlowCommandNames.end), + createCommand(ControlFlowCommandNames.repeatIf), + createCommand(ControlFlowCommandNames.end) + ]); + expect(stack[0]).toEqual(0); // if + expect(stack[1]).toEqual(1); // command + expect(stack[2]).toEqual(0); // else + expect(stack[3]).toEqual(1); // while + expect(stack[4]).toEqual(2); // command + expect(stack[5]).toEqual(1); // end + expect(stack[6]).toEqual(1); // do + expect(stack[7]).toEqual(2); // command + expect(stack[8]).toEqual(2); // while + expect(stack[9]).toEqual(3); // command + expect(stack[10]).toEqual(2); // end + expect(stack[11]).toEqual(1); // repeatIf + expect(stack[12]).toEqual(0); // end + }); + }); + }); +}); diff --git a/packages/selenium-ide/src/neo/__test__/playback/playback-tree/command-node.spec.js b/packages/selenium-ide/src/neo/__test__/playback/playback-tree/command-node.spec.js new file mode 100644 index 000000000..5ecf2a1df --- /dev/null +++ b/packages/selenium-ide/src/neo/__test__/playback/playback-tree/command-node.spec.js @@ -0,0 +1,140 @@ +// 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 Command, { ControlFlowCommandNames } from "../../../models/Command"; +import { CommandNode } from "../../../playback/playback-tree/command-node"; + +describe("Command Node", () => { + it("control flow check returns correct result", () => { + let command = new Command(undefined, "if", "", ""); + let node = new CommandNode(command); + expect(node.isControlFlow()).toBeTruthy(); + command = new Command(undefined, "command", "", ""); + node = new CommandNode(command); + node.left = "asdf"; + expect(node.isControlFlow()).toBeTruthy(); + command = new Command(undefined, "command", "", ""); + node = new CommandNode(command); + node.right = "asdf"; + expect(node.isControlFlow()).toBeTruthy(); + }); + it("retry limit defaults to 1000", () => { + const command = new Command(undefined, ControlFlowCommandNames.times, "", ""); + const node = new CommandNode(command); + node.timesVisited = 999; + expect(node._isRetryLimit()).toBeFalsy(); + node.timesVisited = 1000; + expect(node._isRetryLimit()).toBeTruthy(); + }); + it("retry limit can be overriden", () => { + const command = new Command(undefined, ControlFlowCommandNames.repeatIf, "", 5); + const node = new CommandNode(command); + node.timesVisited = 5; + expect(node._isRetryLimit()).toBeTruthy(); + }); + it("execute resolves with an error message when too many retries attempted in a loop", () => { + const command = new Command(undefined, ControlFlowCommandNames.while, "", 2); + const node = new CommandNode(command); + node.timesVisited = 3; + node.execute().then((result) => { + expect(result.result).toEqual("Max retry limit exceeded. To override it, specify a new limit in the value input field."); + }); + }); + it("evaluate resolves with an error message on 'times' when an invalid number is provided", () => { + const command = new Command(undefined, ControlFlowCommandNames.times, "asdf", ""); + const node = new CommandNode(command); + node._evaluate().then((result) => { + expect(result.result).toEqual("Invalid number provided as a target."); + }); + }); + it("timesVisited only incremenrts for control flow commands", () => { + let command = new Command(undefined, ControlFlowCommandNames.times, "", ""); + let node = new CommandNode(command); + expect(node.timesVisited).toBe(0); + node._incrementTimesVisited(); + expect(node.timesVisited).toBe(1); + command = new Command(undefined, "command", "", ""); + node = new CommandNode(command); + expect(node.timesVisited).toBe(0); + node._incrementTimesVisited(); + expect(node.timesVisited).toBe(0); + }); + it("evaluationResult returns the 'right' node on true", () => { + const command = new Command(undefined, "a", "", ""); + const node = new CommandNode(command); + node.right = "b"; + node.left = "c"; + const result = node._evaluationResult({ result: "success", value: true }); + expect(result.next).toEqual("b"); + }); + it("evaluationResult returns the 'left' node on false", () => { + const command = new Command(undefined, "a", "", ""); + const node = new CommandNode(command); + node.right = "b"; + node.left = "c"; + const result = node._evaluationResult({ result: "success", value: false }); + expect(result.next).toEqual("c"); + }); + it("evaluationResult returns a message when unsuccessful", () => { + const command = new Command(undefined, "a", "", ""); + const node = new CommandNode(command); + node.right = "b"; + node.left = "c"; + const result = node._evaluationResult({ result: "no dice" }); + expect(result.next).toBeUndefined(); + expect(result.result).toEqual("no dice"); + }); + it("executionResult returns the 'next' node on extCommand", () => { + const extCommand = { isExtCommand: function() { return true; } }; + const command = new Command(undefined, "open", "", ""); + let nodeA = new CommandNode(command); + const nodeB = new CommandNode(command); + nodeA.next = nodeB; + expect(nodeA._executionResult(extCommand).next).toEqual(nodeB); + }); + it("executionResult returns the 'next' node on Selenium command", () => { + const extCommand = { isExtCommand: function() { return false; } }; + const command = new Command(undefined, "click", "", ""); + let nodeA = new CommandNode(command); + const nodeB = new CommandNode(command); + nodeA.next = nodeB; + expect(nodeA._executionResult(extCommand, { result: "success" }).next).toEqual(nodeB); + }); + it("executionResult returns the 'next' node and result message on verify command", () => { + const extCommand = { isExtCommand: function() { return false; } }; + const command = new Command(undefined, "verify", "", ""); + let nodeA = new CommandNode(command); + const nodeB = new CommandNode(command); + nodeA.next = nodeB; + expect(nodeA._executionResult(extCommand, { result: "failed with error" }).next).toEqual(nodeB); + }); + it("executionResult returns a 'next' node on control flow", () => { + const extCommand = { isExtCommand: function() { return false; } }; + const command = new Command(undefined, ControlFlowCommandNames.if, "", ""); + let nodeA = new CommandNode(command); + const nodeB = new CommandNode(command); + expect(nodeA._executionResult(extCommand, { result: "success", next: nodeB }).next).toEqual(nodeB); + }); + it("executionResult returns a message when unsuccessful", () => { + const extCommand = { isExtCommand: function() { return false; } }; + const command = new Command(undefined, "command", "", ""); + const node = new CommandNode(command); + const result = node._executionResult(extCommand, { result: "no dice" }); + expect(result.next).toBeUndefined(); + expect(result.result).toEqual("no dice"); + }); +}); diff --git a/packages/selenium-ide/src/neo/__test__/playback/playback-tree/playback-tree.spec.js b/packages/selenium-ide/src/neo/__test__/playback/playback-tree/playback-tree.spec.js new file mode 100644 index 000000000..fff59fd8a --- /dev/null +++ b/packages/selenium-ide/src/neo/__test__/playback/playback-tree/playback-tree.spec.js @@ -0,0 +1,366 @@ +// 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 { createPlaybackTree, createCommandNodesFromCommandStack } from "../../../playback/playback-tree"; +import Command, { ControlFlowCommandNames } from "../../../models/Command"; + +function createCommand(name) { + return new Command(null, name, "", ""); +} + +describe("Control Flow", () => { + describe("Process", () => { + describe("Linked List Validation", () => { + test("nodes contain command references and levels", () => { + let input = [ createCommand("command1"), createCommand("command2") ]; + let stack = createCommandNodesFromCommandStack(input); + expect(stack[0].command).toEqual(input[0]); + expect(stack[0].level).toEqual(0); + expect(stack[1].command).toEqual(input[1]); + expect(stack[1].level).toEqual(0); + }); + test("command-command", () => { + let input = [ + createCommand("command1"), + createCommand("command2") + ]; + let stack = createCommandNodesFromCommandStack(input); + expect(stack[0].next).toEqual(stack[1]); + expect(stack[0].left).toBeUndefined(); + expect(stack[0].right).toBeUndefined(); + expect(stack[1].next).toBeUndefined(); + expect(stack[1].left).toBeUndefined(); + expect(stack[1].right).toBeUndefined(); + }); + test("if-command-elseIf-command-else-command-end", () => { + let input = [ + createCommand(ControlFlowCommandNames.if), + createCommand("command"), + createCommand(ControlFlowCommandNames.elseIf), + createCommand("command"), + createCommand(ControlFlowCommandNames.else), + createCommand("command"), + createCommand(ControlFlowCommandNames.end) + ]; + let stack = createCommandNodesFromCommandStack(input); + // if + expect(stack[0].next).toBeUndefined(); + expect(stack[0].right).toEqual(stack[1]); + expect(stack[0].left).toEqual(stack[2]); + // command + expect(stack[1].next).toBeUndefined(); + expect(stack[1].right).toBeUndefined(); + expect(stack[1].left).toBeUndefined(); + // elseIf + expect(stack[2].next).toBeUndefined(); + expect(stack[2].right).toEqual(stack[3]); + expect(stack[2].left).toEqual(stack[5]); + // command + expect(stack[3].next).toBeUndefined(); + expect(stack[3].right).toBeUndefined(); + expect(stack[3].left).toBeUndefined(); + // else + expect(stack[4].next).toBeUndefined(); + expect(stack[4].right).toBeUndefined(); + expect(stack[4].left).toBeUndefined(); + // command + expect(stack[5].next).toBeUndefined(); + expect(stack[5].right).toBeUndefined(); + expect(stack[5].left).toBeUndefined(); + // end + expect(stack[6].next).toBeUndefined(); + expect(stack[6].right).toBeUndefined(); + expect(stack[6].left).toBeUndefined(); + }); + test("while-end", () => { + let input = [ + createCommand(ControlFlowCommandNames.while), + createCommand(ControlFlowCommandNames.end) + ]; + let stack = createCommandNodesFromCommandStack(input); + expect(stack[0].next).toBeUndefined(); + expect(stack[0].right).toBeUndefined(); + expect(stack[0].left).toBeUndefined(); + expect(stack[1].next).toBeUndefined(); + expect(stack[1].right).toBeUndefined(); + expect(stack[1].left).toBeUndefined(); + }); + test("while-command-end", () => { + let input = [ + createCommand(ControlFlowCommandNames.while), + createCommand("command"), + createCommand(ControlFlowCommandNames.end) + ]; + let stack = createCommandNodesFromCommandStack(input); + expect(stack[0].next).toBeUndefined(); + expect(stack[0].right).toEqual(stack[1]); + expect(stack[0].left).toBeUndefined(); + expect(stack[1].next).toEqual(stack[0]); + expect(stack[1].right).toBeUndefined(); + expect(stack[1].left).toBeUndefined(); + expect(stack[2].next).toBeUndefined(); + expect(stack[2].right).toBeUndefined(); + expect(stack[2].left).toBeUndefined(); + }); + test("while-command-command-end", () => { + let input = [ + createCommand(ControlFlowCommandNames.while), + createCommand("command"), + createCommand("command"), + createCommand(ControlFlowCommandNames.end) + ]; + let stack = createCommandNodesFromCommandStack(input); + expect(stack[0].next).toBeUndefined(); + expect(stack[0].right).toEqual(stack[1]); + expect(stack[0].left).toBeUndefined(); + expect(stack[1].next).toEqual(stack[2]); + expect(stack[1].right).toBeUndefined(); + expect(stack[1].left).toBeUndefined(); + expect(stack[2].next).toEqual(stack[0]); + expect(stack[2].right).toBeUndefined(); + expect(stack[2].left).toBeUndefined(); + expect(stack[3].next).toBeUndefined(); + expect(stack[3].right).toBeUndefined(); + expect(stack[3].left).toBeUndefined(); + }); + test("if-command-while-command-end-end", () => { + let input = [ + createCommand(ControlFlowCommandNames.if), + createCommand("command"), + createCommand(ControlFlowCommandNames.while), + createCommand("command"), + createCommand(ControlFlowCommandNames.end), + createCommand(ControlFlowCommandNames.end) + ]; + let stack = createCommandNodesFromCommandStack(input); + // if + expect(stack[0].next).toBeUndefined(); + expect(stack[0].right).toEqual(stack[1]); + expect(stack[0].left).toBeUndefined(); + // command + expect(stack[1].next).toEqual(stack[2]); + expect(stack[1].right).toBeUndefined(); + expect(stack[1].left).toBeUndefined(); + // while + expect(stack[2].next).toBeUndefined(); + expect(stack[2].right).toEqual(stack[3]); + expect(stack[2].left).toBeUndefined(); + // command + expect(stack[3].next).toEqual(stack[2]); + expect(stack[3].right).toBeUndefined(); + expect(stack[3].left).toBeUndefined(); + // end + expect(stack[4].next).toBeUndefined(); + expect(stack[4].right).toBeUndefined(); + expect(stack[4].left).toBeUndefined(); + // end + expect(stack[5].next).toBeUndefined(); + expect(stack[5].right).toBeUndefined(); + expect(stack[5].left).toBeUndefined(); + }); + test("if-while-command-end-command-else-command-end", () => { + let input = [ + createCommand(ControlFlowCommandNames.if), + createCommand(ControlFlowCommandNames.while), + createCommand("command"), + createCommand(ControlFlowCommandNames.end), + createCommand("command"), + createCommand(ControlFlowCommandNames.else), + createCommand("command"), + createCommand(ControlFlowCommandNames.end) + ]; + let stack = createCommandNodesFromCommandStack(input); + // if + expect(stack[0].next).toBeUndefined(); + expect(stack[0].right).toEqual(stack[1]); + expect(stack[0].left).toEqual(stack[6]); + // while + expect(stack[1].next).toBeUndefined(); + expect(stack[1].right).toEqual(stack[2]); + expect(stack[1].left).toEqual(stack[4]); + // command + expect(stack[2].next).toEqual(stack[1]); + expect(stack[2].right).toBeUndefined(); + expect(stack[2].left).toBeUndefined(); + // end + expect(stack[3].next).toBeUndefined(); + expect(stack[3].right).toBeUndefined(); + expect(stack[3].left).toBeUndefined(); + // command + expect(stack[4].next).toBeUndefined(); + expect(stack[4].right).toBeUndefined(); + expect(stack[4].left).toBeUndefined(); + // else + expect(stack[5].next).toBeUndefined(); + expect(stack[5].right).toBeUndefined(); + expect(stack[5].left).toBeUndefined(); + // command + expect(stack[6].next).toBeUndefined(); + expect(stack[6].right).toBeUndefined(); + expect(stack[6].left).toBeUndefined(); + // end + expect(stack[7].next).toBeUndefined(); + expect(stack[7].right).toBeUndefined(); + expect(stack[7].left).toBeUndefined(); + }); + test("do-command-repeatIf-command", () => { + let input = [ + createCommand(ControlFlowCommandNames.do), + createCommand("command"), + createCommand(ControlFlowCommandNames.repeatIf), + createCommand("command") + ]; + let stack = createCommandNodesFromCommandStack(input); + expect(stack[0].next.command).toEqual(input[2]); + expect(stack[0].right).toBeUndefined(); + expect(stack[0].left).toBeUndefined(); + expect(stack[1].next).toBeUndefined(); + expect(stack[1].right.command).toEqual(input[1]); + expect(stack[1].left.command).toEqual(input[3]); + expect(stack[2].next).toBeUndefined(); + expect(stack[2].right).toBeUndefined(); + expect(stack[2].left).toBeUndefined(); + }); + test("do-command-while-command-end-repeatIf", () => { + let input = [ + createCommand(ControlFlowCommandNames.do), + createCommand("command"), + createCommand(ControlFlowCommandNames.while), + createCommand("command"), + createCommand(ControlFlowCommandNames.end), + createCommand(ControlFlowCommandNames.repeatIf) + ]; + let stack = createCommandNodesFromCommandStack(input); + expect(stack[0].next.command).toEqual(input[2]); + expect(stack[0].right).toBeUndefined(); + expect(stack[0].left).toBeUndefined(); + expect(stack[1].next).toBeUndefined(); + expect(stack[1].right.command).toEqual(input[3]); + expect(stack[1].left.command).toEqual(input[5]); + expect(stack[2].next.command).toEqual(input[2]); + expect(stack[2].right).toBeUndefined(); + expect(stack[2].left).toBeUndefined(); + expect(stack[3].next).toBeUndefined(); + expect(stack[3].right).toBeUndefined(); + expect(stack[3].left).toBeUndefined(); + expect(stack[4].next).toBeUndefined(); + expect(stack[4].right.command).toEqual(input[1]); + expect(stack[4].left).toBeUndefined(); + }); + test("times-command-end", () => { + let input = [ + createCommand(ControlFlowCommandNames.times), + createCommand("command"), + createCommand(ControlFlowCommandNames.end) + ]; + let stack = createCommandNodesFromCommandStack(input); + expect(stack[0].next).toBeUndefined(); + expect(stack[0].right).toEqual(stack[1]); + expect(stack[0].left).toBeUndefined(); + expect(stack[1].next).toEqual(stack[0]); + expect(stack[1].right).toBeUndefined(); + expect(stack[1].left).toBeUndefined(); + expect(stack[2].next).toBeUndefined(); + expect(stack[2].right).toBeUndefined(); + expect(stack[2].left).toBeUndefined(); + }); + test("if-if-if-if-end-end-end-command-end", () => { + let input = [ + createCommand(ControlFlowCommandNames.if), + createCommand(ControlFlowCommandNames.if), + createCommand(ControlFlowCommandNames.if), + createCommand(ControlFlowCommandNames.if), + createCommand(ControlFlowCommandNames.end), + createCommand(ControlFlowCommandNames.end), + createCommand(ControlFlowCommandNames.end), + createCommand("command"), + createCommand(ControlFlowCommandNames.end) + ]; + let stack = createCommandNodesFromCommandStack(input); + expect(stack[0].next).toBeUndefined(); + expect(stack[0].right).toEqual(stack[1]); + expect(stack[0].left).toBeUndefined(); + expect(stack[1].next).toBeUndefined(); + expect(stack[1].right).toEqual(stack[2]); + expect(stack[1].left).toEqual(stack[7]); + expect(stack[2].next).toBeUndefined(); + expect(stack[2].right).toEqual(stack[3]); + expect(stack[2].left).toEqual(stack[7]); + expect(stack[3].next).toBeUndefined(); + expect(stack[3].right).toBeUndefined(); + expect(stack[3].left).toEqual(stack[7]); + expect(stack[4].next).toBeUndefined(); + expect(stack[4].right).toBeUndefined(); + expect(stack[4].left).toBeUndefined(); + expect(stack[5].next).toBeUndefined(); + expect(stack[5].right).toBeUndefined(); + expect(stack[5].left).toBeUndefined(); + expect(stack[6].next).toBeUndefined(); + expect(stack[6].right).toBeUndefined(); + expect(stack[6].left).toBeUndefined(); + expect(stack[7].next).toBeUndefined(); + expect(stack[7].right).toBeUndefined(); + expect(stack[7].left).toBeUndefined(); + expect(stack[8].next).toBeUndefined(); + expect(stack[8].right).toBeUndefined(); + expect(stack[8].left).toBeUndefined(); + }); + }); + }); + describe("Processed", () => { + it("do-command-repeatIf-end skips do", () => { + let input = [ + createCommand(ControlFlowCommandNames.do), + createCommand("command"), + createCommand(ControlFlowCommandNames.repeatIf) + ]; + let tree = createPlaybackTree(input); + expect(tree.startingCommandNode.command).toEqual(input[1]); + expect(tree.startingCommandNode.next.command).toEqual(input[2]); + expect(tree.startingCommandNode.next.right.command).toEqual(input[1]); + expect(tree.startingCommandNode.next.left).toBeUndefined(); + }); + it("populated tree exists with correct values", () => { + let input = [ + createCommand(ControlFlowCommandNames.if), + createCommand("command"), + createCommand(ControlFlowCommandNames.else), + createCommand(ControlFlowCommandNames.while), + createCommand("command"), + createCommand(ControlFlowCommandNames.end), + createCommand(ControlFlowCommandNames.do), + createCommand("command"), + createCommand(ControlFlowCommandNames.while), + createCommand("command"), + createCommand(ControlFlowCommandNames.end), + createCommand(ControlFlowCommandNames.repeatIf), + createCommand(ControlFlowCommandNames.end) + ]; + let tree = createPlaybackTree(input); + expect(tree.startingCommandNode.command).toEqual(input[0]); // if + expect(tree.startingCommandNode.right.command).toEqual(input[1]); // if -> command + expect(tree.startingCommandNode.right.next).toBeUndefined(); // if command -> undefined (end + 1) + expect(tree.startingCommandNode.left.command).toEqual(input[3]); // if -> while (else + 1) + expect(tree.startingCommandNode.left.right.command).toEqual(input[4]); // while -> command + expect(tree.startingCommandNode.left.left.command).toEqual(input[7]); // while -> command (end + 1, do + 1) + expect(tree.startingCommandNode.left.left.next.right.command).toEqual(input[9]); // do -> while -> command + expect(tree.startingCommandNode.left.left.next.left.command).toEqual(input[11]); // do -> while -> repeatIf (end + 1) + expect(tree.startingCommandNode.left.left.next.left.right.command).toEqual(input[7]); // repeatIf -> do command + expect(tree.startingCommandNode.left.left.next.left.left).toBeUndefined(); // repeatIf -> undefined (end + 1) + }); + }); +}); diff --git a/packages/selenium-ide/src/neo/__test__/playback/playback-tree/syntax-validation.spec.js b/packages/selenium-ide/src/neo/__test__/playback/playback-tree/syntax-validation.spec.js new file mode 100644 index 000000000..62431002e --- /dev/null +++ b/packages/selenium-ide/src/neo/__test__/playback/playback-tree/syntax-validation.spec.js @@ -0,0 +1,183 @@ +// 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 { validateControlFlowSyntax } from "../../../playback/playback-tree/syntax-validation"; +import Command, { ControlFlowCommandNames } from "../../../models/Command"; + +function createCommand(name) { + return new Command(null, name, "", ""); +} + +describe("Control Flow", () => { + describe("Preprocess", () => { + describe("Syntax Validation", () => { + test("if, end", () => { + let result = validateControlFlowSyntax([ + createCommand(ControlFlowCommandNames.if), + createCommand(ControlFlowCommandNames.end) + ]); + expect(result).toBeTruthy(); + }); + test("if, else, end", () => { + let result = validateControlFlowSyntax([ + createCommand(ControlFlowCommandNames.if), + createCommand(ControlFlowCommandNames.else), + createCommand(ControlFlowCommandNames.end) + ]); + expect(result).toBeTruthy(); + }); + test("if, elseIf, end", () => { + let result = validateControlFlowSyntax([ + createCommand(ControlFlowCommandNames.if), + createCommand(ControlFlowCommandNames.elseIf), + createCommand(ControlFlowCommandNames.end) + ]); + expect(result).toBeTruthy(); + }); + test("if, elseIf, else, end", () => { + let result = validateControlFlowSyntax([ + createCommand(ControlFlowCommandNames.if), + createCommand(ControlFlowCommandNames.elseIf), + createCommand(ControlFlowCommandNames.else), + createCommand(ControlFlowCommandNames.end) + ]); + expect(result).toBeTruthy(); + }); + test("while, end", () => { + let result = new validateControlFlowSyntax([ + createCommand(ControlFlowCommandNames.while), + createCommand(ControlFlowCommandNames.end) + ]); + expect(result).toBeTruthy(); + }); + test("times, end", () => { + let result = validateControlFlowSyntax([ + createCommand("times"), + createCommand(ControlFlowCommandNames.end) + ]); + expect(result).toBeTruthy(); + }); + test("do, repeatIf", () => { + let result = validateControlFlowSyntax([ + createCommand(ControlFlowCommandNames.do), + createCommand(ControlFlowCommandNames.repeatIf) + ]); + expect(result).toBeTruthy(); + }); + test("do, while, end, repeatIf", () => { + let result = validateControlFlowSyntax([ + createCommand(ControlFlowCommandNames.do), + createCommand(ControlFlowCommandNames.while), + createCommand(ControlFlowCommandNames.end), + createCommand(ControlFlowCommandNames.repeatIf) + ]); + expect(result).toBeTruthy(); + }); + }); + describe("Syntax Invalidation", () => { + test("if", () => { + let input = [createCommand(ControlFlowCommandNames.if)]; + expect(function() { validateControlFlowSyntax(input); }).toThrow("Incomplete block at if"); + }); + test("if, if, end", () => { + let input = [ + createCommand(ControlFlowCommandNames.if), + createCommand(ControlFlowCommandNames.if), + createCommand(ControlFlowCommandNames.end) + ]; + expect(function() { validateControlFlowSyntax(input); }).toThrow("Incomplete block at if"); + }); + test("if, else, elseIf, end", () => { + let input = [ + createCommand(ControlFlowCommandNames.if), + createCommand(ControlFlowCommandNames.else), + createCommand(ControlFlowCommandNames.elseIf), + createCommand(ControlFlowCommandNames.end) + ]; + expect(function() { validateControlFlowSyntax(input); }).toThrow("Incorrect command order of else if / else"); + }); + test("if, else, else, end", () => { + let input = [ + createCommand(ControlFlowCommandNames.if), + createCommand(ControlFlowCommandNames.else), + createCommand(ControlFlowCommandNames.else), + createCommand(ControlFlowCommandNames.end) + ]; + expect(function() { validateControlFlowSyntax(input); }).toThrow("Too many else commands used"); + }); + test("else", () => { + let input = [ createCommand(ControlFlowCommandNames.else) ]; + expect(function() { validateControlFlowSyntax(input); }).toThrow("An else used outside of an if block"); + }); + test("else, else", () => { + let input = [ + createCommand(ControlFlowCommandNames.else), + createCommand(ControlFlowCommandNames.else) + ]; + expect(function() { validateControlFlowSyntax(input); }).toThrow("An else used outside of an if block"); + }); + test("elseIf", () => { + let input = [ createCommand(ControlFlowCommandNames.elseIf) ]; + expect(function() { validateControlFlowSyntax(input); }).toThrow("An else if used outside of an if block"); + }); + test("while", () => { + let input = [createCommand(ControlFlowCommandNames.while)]; + expect(function() { validateControlFlowSyntax(input); }).toThrow("Incomplete block at while"); + }); + test("if, while", () => { + let input = [ + createCommand(ControlFlowCommandNames.if), + createCommand(ControlFlowCommandNames.while) + ]; + expect(function() { validateControlFlowSyntax(input); }).toThrow("Incomplete block at while"); + }); + test("if, while, end", () => { + let input = [ + createCommand(ControlFlowCommandNames.if), + createCommand(ControlFlowCommandNames.while), + createCommand(ControlFlowCommandNames.end) + ]; + expect(function() { validateControlFlowSyntax(input); }).toThrow("Incomplete block at if"); + }); + test("if, while, else, end", () => { + let input = [ + createCommand(ControlFlowCommandNames.if), + createCommand(ControlFlowCommandNames.while), + createCommand(ControlFlowCommandNames.else), + createCommand(ControlFlowCommandNames.end) + ]; + expect(function() { validateControlFlowSyntax(input); }).toThrow("An else used outside of an if block"); + }); + test("times", () => { + let input = [createCommand("times")]; + expect(function() { validateControlFlowSyntax(input); }).toThrow("Incomplete block at times"); + }); + test("repeatIf", () => { + let input = [createCommand(ControlFlowCommandNames.repeatIf)]; + expect(function() { validateControlFlowSyntax(input); }).toThrow("A repeat if used without a do block"); + }); + test("do", () => { + let input = [createCommand(ControlFlowCommandNames.do)]; + expect(function() { validateControlFlowSyntax(input); }).toThrow("Incomplete block at do"); + }); + test("end", () => { + let input = [createCommand(ControlFlowCommandNames.end)]; + expect(function() { validateControlFlowSyntax(input); }).toThrow("Use of end without an opening keyword"); + }); + }); + }); +}); diff --git a/packages/selenium-ide/src/neo/components/TestRow/index.jsx b/packages/selenium-ide/src/neo/components/TestRow/index.jsx index cea94d624..c9b50ff06 100644 --- a/packages/selenium-ide/src/neo/components/TestRow/index.jsx +++ b/packages/selenium-ide/src/neo/components/TestRow/index.jsx @@ -125,7 +125,8 @@ class TestRow extends React.Component { moveSelection: PropTypes.func, setSectionFocus: PropTypes.func, onContextMenu: PropTypes.func, - setContextMenu: PropTypes.func + setContextMenu: PropTypes.func, + level: PropTypes.number }; componentDidMount() { if (this.props.selected) { @@ -209,6 +210,7 @@ class TestRow extends React.Component { } } render() { + const commandIndentation = ; const listMenu = (!this.props.isPristine && !this.props.readOnly) ? }> Cut Copy @@ -255,6 +257,7 @@ class TestRow extends React.Component { {this.props.command.comment} + { commandIndentation } {this.props.command.command} {this.props.command.target} diff --git a/packages/selenium-ide/src/neo/components/TestTable/index.jsx b/packages/selenium-ide/src/neo/components/TestTable/index.jsx index 3dd3551a8..46f8c01f3 100644 --- a/packages/selenium-ide/src/neo/components/TestTable/index.jsx +++ b/packages/selenium-ide/src/neo/components/TestTable/index.jsx @@ -25,6 +25,7 @@ import UiState from "../../stores/view/UiState"; import PlaybackState from "../../stores/view/PlaybackState"; import TestRow from "../TestRow"; import "./style.css"; +import { deriveCommandLevels } from "../../playback/playback-tree/command-leveler"; @observer export default class TestTable extends React.Component { @@ -34,6 +35,7 @@ export default class TestTable extends React.Component { this.detectNewCommand = this.detectNewCommand.bind(this); this.disposeNewCommand = this.disposeNewCommand.bind(this); this.newObserverDisposer = observe(this.props.commands, this.detectNewCommand); + this.commandLevels = []; } static propTypes = { commands: MobxPropTypes.arrayOrObservableArray, @@ -60,6 +62,7 @@ export default class TestTable extends React.Component { } } render() { + if (this.props.commands) this.commandLevels = deriveCommandLevels(this.props.commands); const commandStatePrefix = this.props.callstackIndex !== undefined ? `${this.props.callstackIndex}:` : ""; return ([
@@ -97,6 +100,7 @@ export default class TestTable extends React.Component { pasteFromClipboard={UiState.pasteFromClipboard} clearAllCommands={this.props.clearAllCommands} setSectionFocus={UiState.setSectionFocus} + level={this.commandLevels[index]} /> )).concat( { + return this._executionResult(extCommand, result); + }); + } + + _executeCommand(extCommand, options) { + if (extCommand.isExtCommand(this.command.command)) { + return extCommand[extCommand.name(this.command.command)]( + xlateArgument(this.command.target), + xlateArgument(this.command.value)); + } else if (this.isControlFlow()) { + return this._evaluate(extCommand); + } else if (this.command.command === "type") { + return extCommand.doType( + xlateArgument(this.command.target), + xlateArgument(this.command.value), + extCommand.isWindowMethodCommand(this.command.command)); + } else if (canExecuteCommand(this.command.command)) { + return executeCommand( + this.command.command, + this.command.target, + this.command.value, + options); + } else { + return extCommand.sendMessage( + this.command.command, + xlateArgument(this.command.target), + xlateArgument(this.command.value), + extCommand.isWindowMethodCommand(this.command.command)); + } + } + + _executionResult(extCommand, result) { + if (extCommand.isExtCommand(this.command.command)) { + return { + next: this.next + }; + } else if (result.result === "success") { + this._incrementTimesVisited(); + return { + result: "success", + next: this.isControlFlow() ? result.next : this.next + }; + } else if (canExecuteCommand(this.command.command)) { + let _result = { ...result }; + _result.next = this.next; + return _result; + } else { + if (this.command.command.match(/^verify/)) { + return { + result: result.result, + next: this.next + }; + } else { + return result; + } + } + } + + _evaluate(extCommand) { + let expression = this.command.target; + if (ControlFlowCommandChecks.isTimes(this.command)) { + const number = Math.floor(+expression); + if (isNaN(number)) { + return Promise.resolve({ + result: "Invalid number provided as a target." + }); + } + expression = `${this.timesVisited} < ${number}`; + } + return (extCommand.sendMessage("evaluateConditional", expression, "", false)) + .then((result) => { + return this._evaluationResult(result); + }); + } + + _evaluationResult(result) { + if (result.result === "success") { + if (result.value) { + return { + result: "success", + next: this.right + }; + } else { + return { + result: "success", + next: this.left + }; + } + } else { + return result; + } + } + + _incrementTimesVisited() { + if (ControlFlowCommandChecks.isLoop(this.command)) this.timesVisited++; + } + + _isRetryLimit() { + if (ControlFlowCommandChecks.isLoop(this.command)) { + let limit = 1000; + let value = Math.floor(+this.command.value); + if (this.command.value && !isNaN(value)) { + limit = value; + } + return (this.timesVisited >= limit); + } + } + +} diff --git a/packages/selenium-ide/src/neo/playback/playback-tree/control-flow-syntax-error.js b/packages/selenium-ide/src/neo/playback/playback-tree/control-flow-syntax-error.js new file mode 100644 index 000000000..86a18eb43 --- /dev/null +++ b/packages/selenium-ide/src/neo/playback/playback-tree/control-flow-syntax-error.js @@ -0,0 +1,23 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// dControlFlowCommandChecks.istributed with thControlFlowCommandChecks.is work for additional information +// regarding copyright ownership. The SFC licenses thControlFlowCommandChecks.is file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use thControlFlowCommandChecks.is 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 dControlFlowCommandChecks.istributed under the License ControlFlowCommandChecks.is dControlFlowCommandChecks.istributed on an +// "AS ControlFlowCommandChecks.is" BASControlFlowCommandChecks.is, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permControlFlowCommandChecks.issions and limitations +// under the License. + +export class ControlFlowSyntaxError extends SyntaxError { + constructor(message, index) { + super(message); + this.index = index; + } +} diff --git a/packages/selenium-ide/src/neo/playback/playback-tree/index.js b/packages/selenium-ide/src/neo/playback/playback-tree/index.js new file mode 100644 index 000000000..43fe7f691 --- /dev/null +++ b/packages/selenium-ide/src/neo/playback/playback-tree/index.js @@ -0,0 +1,147 @@ +// 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 { CommandNode } from "./command-node"; +import { State } from "./state"; +import { validateControlFlowSyntax } from "./syntax-validation"; +import { deriveCommandLevels } from "./command-leveler"; +import { ControlFlowCommandNames, ControlFlowCommandChecks } from "../../models/Command"; +export { createPlaybackTree }; // public API +export { createCommandNodesFromCommandStack }; // for testing + +function createPlaybackTree(commandStack) { + let nodes = createCommandNodesFromCommandStack(commandStack); + return { startingCommandNode: nodes[0] }; +} + +function createCommandNodesFromCommandStack(commandStack) { + validateControlFlowSyntax(commandStack); + let levels = deriveCommandLevels(commandStack); + let nodes = createCommandNodes(commandStack, levels); + return connectCommandNodes(nodes); +} + +function createCommandNodes(commandStack, levels) { + let commandNodes = []; + commandStack.forEach(function(command, index) { + let node = new CommandNode(command); + node.index = index; + node.level = levels[index]; + commandNodes.push(node); + }); + return commandNodes; +} + +function connectCommandNodes(_commandNodeStack) { + let commandNodeStack = [ ..._commandNodeStack ]; + let state = new State; + commandNodeStack.forEach(function(commandNode) { + let nextCommandNode = commandNodeStack[commandNode.index + 1]; + if (connectCommandNode[commandNode.command.command]) { + connectCommandNode[commandNode.command.command](commandNode, nextCommandNode, commandNodeStack, state); + } else { + connectDefault(commandNode, nextCommandNode, commandNodeStack, state); + } + }); + if (ControlFlowCommandChecks.isTerminal(commandNodeStack[0].command)) { + commandNodeStack.shift(); + } + return commandNodeStack; +} + +let connectCommandNode = { + [ControlFlowCommandNames.do]: trackBranchOpen, + [ControlFlowCommandNames.elseIf]: connectConditional, + [ControlFlowCommandNames.end]: trackBranchClose, + [ControlFlowCommandNames.if]: connectConditionalForBranchOpen, + [ControlFlowCommandNames.repeatIf]: connectDo, + [ControlFlowCommandNames.times]: connectConditionalForBranchOpen, + [ControlFlowCommandNames.while]: connectConditionalForBranchOpen +}; + +function connectDefault (commandNode, _nextCommandNode, stack, state) { + let nextCommandNode; + if (ControlFlowCommandChecks.isIf(state.top()) && ControlFlowCommandChecks.isElseOrElseIf(_nextCommandNode.command)) { + let next = findNextNodeBy(stack, commandNode.index, state.top().level, ControlFlowCommandNames.end); + nextCommandNode = deriveNext(next, stack); + } else if (ControlFlowCommandChecks.isLoop(state.top()) && ControlFlowCommandChecks.isEnd(_nextCommandNode.command)) { + nextCommandNode = stack[state.top().index]; + } else if (ControlFlowCommandChecks.isElse(commandNode.command)) { + nextCommandNode = undefined; + } else { + nextCommandNode = deriveNext(_nextCommandNode, stack); + } + connectNext(commandNode, nextCommandNode); +} + +function trackBranchOpen (commandNode, nextCommandNode, stack, state) { + state.push({ command: commandNode.command.command, level: commandNode.level, index: commandNode.index }); +} + +function trackBranchClose (commandNode, nextCommandNode, stack, state) { + state.pop(); +} + +function connectConditionalForBranchOpen (commandNode, nextCommandNode, stack, state) { + trackBranchOpen(commandNode, nextCommandNode, stack, state); + connectConditional(commandNode, nextCommandNode, stack); +} + +function connectConditional (commandNode, nextCommandNode, stack) { + let left = findNextNodeBy(stack, commandNode.index, commandNode.level); + let right = ControlFlowCommandChecks.isEnd(nextCommandNode.command) ? undefined : nextCommandNode; + commandNode.right = right; + commandNode.left = deriveNext(left, stack); +} + +function connectNext (commandNode, nextCommandNode) { + commandNode.next = nextCommandNode; +} + +function connectDo (commandNode, nextCommandNode, stack, state) { + commandNode.right = stack[state.top().index + 1]; + commandNode.left = deriveNext(nextCommandNode, stack); + trackBranchClose(commandNode, nextCommandNode, stack, state); +} + +function findNextNodeBy(stack, index, level, commandName) { + for(let i = index + 1; i < stack.length + 1; i++) { + if (commandName) { + if (stack[i].level === level && + stack[i].command.command === commandName) { + return stack[i]; + } + } else { + if (stack[i].level === level) { + return stack[i]; + } + } + } +} + +function deriveNext (targetNode, stack) { + if (targetNode && ControlFlowCommandChecks.isTerminal(targetNode.command)) { + let result = stack[targetNode.index + 1]; + if (result && ControlFlowCommandChecks.isTerminal(result.command)) { + return deriveNext(result, stack); + } else { + return result; + } + } else { + return targetNode; + } +} diff --git a/packages/selenium-ide/src/neo/__test__/IO/CommandHandler.spec.js b/packages/selenium-ide/src/neo/playback/playback-tree/state.js similarity index 63% rename from packages/selenium-ide/src/neo/__test__/IO/CommandHandler.spec.js rename to packages/selenium-ide/src/neo/playback/playback-tree/state.js index 141ace0e8..913269d32 100644 --- a/packages/selenium-ide/src/neo/__test__/IO/CommandHandler.spec.js +++ b/packages/selenium-ide/src/neo/playback/playback-tree/state.js @@ -15,11 +15,25 @@ // specific language governing permissions and limitations // under the License. -//import { registerCommand, canExecuteCommand, executeCommand } from "../../plugin/commandExecutor"; -//import { CommandHandler } from "../IO/commandHandler"; +export class State { + constructor() { + this._state = []; + } + + empty() { + return (this._state.length === 0); + } + + push(obj) { + this._state.push(obj); + } + + pop() { + this._state.pop(); + } + + top() { + return this._state[this._state.length - 1]; + } +} -describe("Command Handler", () => { - it.skip("should check for command type (e.g., extCommand, Selenium, Control Flow", () => { }); - it.skip("should derive the correct command object from the provided type", () => { }); - it.skip("should validate the test case (through delegation)", () => { }); -}); diff --git a/packages/selenium-ide/src/neo/playback/playback-tree/syntax-validation.js b/packages/selenium-ide/src/neo/playback/playback-tree/syntax-validation.js new file mode 100644 index 000000000..c92c67987 --- /dev/null +++ b/packages/selenium-ide/src/neo/playback/playback-tree/syntax-validation.js @@ -0,0 +1,87 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// dControlFlowCommandChecks.istributed with thControlFlowCommandChecks.is work for additional information +// regarding copyright ownership. The SFC licenses thControlFlowCommandChecks.is file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use thControlFlowCommandChecks.is 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 dControlFlowCommandChecks.istributed under the License ControlFlowCommandChecks.is dControlFlowCommandChecks.istributed on an +// "AS ControlFlowCommandChecks.is" BASControlFlowCommandChecks.is, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permControlFlowCommandChecks.issions and limitations +// under the License. + +import { ControlFlowCommandNames, ControlFlowCommandChecks } from "../../models/Command"; +import { State } from "./state"; +import { ControlFlowSyntaxError } from "./control-flow-syntax-error"; + +export function validateControlFlowSyntax(commandStack) { + let state = new State; + commandStack.forEach(function(command, commandIndex) { + if (validateCommand[command.command]) { + validateCommand[command.command](command.command, commandIndex, state); + } + }); + if (!state.empty()) { + throw new ControlFlowSyntaxError("Incomplete block at " + state.top().command, state.top().index); + } else { + return true; + } +} + +const validateCommand = { + [ControlFlowCommandNames.do]: trackControlFlowBranchOpen, + [ControlFlowCommandNames.else]: validateElse, + [ControlFlowCommandNames.elseIf]: validateElseIf, + [ControlFlowCommandNames.end]: validateEnd, + [ControlFlowCommandNames.if]: trackControlFlowBranchOpen, + [ControlFlowCommandNames.repeatIf]: validateRepeatIf, + [ControlFlowCommandNames.times]: trackControlFlowBranchOpen, + [ControlFlowCommandNames.while]: trackControlFlowBranchOpen +}; + +function trackControlFlowBranchOpen (commandName, commandIndex, state) { + state.push({ command: commandName, index: commandIndex }); +} + +function validateElse (commandName, commandIndex, state) { + if (!ControlFlowCommandChecks.isIfBlock(state.top())) { + throw new ControlFlowSyntaxError("An else used outside of an if block", commandIndex); + } + if (ControlFlowCommandChecks.isElse(state.top())) { + throw new ControlFlowSyntaxError("Too many else commands used", commandIndex); + } + state.push({ command: commandName, index: commandIndex }); +} + +function validateElseIf (commandName, commandIndex, state) { + if (!ControlFlowCommandChecks.isIfBlock(state.top())) { + throw new ControlFlowSyntaxError("An else if used outside of an if block", commandIndex); + } + if (ControlFlowCommandChecks.isElse(state.top())) { + throw new ControlFlowSyntaxError("Incorrect command order of else if / else", commandIndex); + } + state.push({ command: commandName, index: commandIndex }); +} + +function validateEnd (commandName, commandIndex, state) { + if (ControlFlowCommandChecks.isBlockOpen(state.top())) { + state.pop(); + } else if (ControlFlowCommandChecks.isElseOrElseIf(state.top())) { + state.pop(); + validateEnd(commandName, commandIndex, state); + } else { + throw new ControlFlowSyntaxError("Use of end without an opening keyword", commandIndex); + } +} + +function validateRepeatIf (commandName, commandIndex, state) { + if (!ControlFlowCommandChecks.isDo(state.top())) { + throw new ControlFlowSyntaxError("A repeat if used without a do block", commandIndex); + } + state.pop(); +} diff --git a/packages/selenium-ide/src/neo/stores/seed.js b/packages/selenium-ide/src/neo/stores/seed.js index 0e99a2551..db6fa6cb7 100644 --- a/packages/selenium-ide/src/neo/stores/seed.js +++ b/packages/selenium-ide/src/neo/stores/seed.js @@ -49,10 +49,60 @@ export default function seed(store, numberOfSuites = 5) { } } - const url = "http://the-internet.herokuapp.com"; + const url = "http://the-internet.local"; store.setUrl(url); store.addUrl(url); + const controlFlowIfTest = store.createTestCase("control flow if"); + controlFlowIfTest.createCommand(undefined, "if", "true"); + controlFlowIfTest.createCommand(undefined, "echo", "foo"); + controlFlowIfTest.createCommand(undefined, "elseIf", "true"); + controlFlowIfTest.createCommand(undefined, "echo", "bar"); + controlFlowIfTest.createCommand(undefined, "else"); + controlFlowIfTest.createCommand(undefined, "echo", "baz"); + controlFlowIfTest.createCommand(undefined, "end"); + + const controlFlowElseIfTest = store.createTestCase("control flow else if"); + controlFlowElseIfTest.createCommand(undefined, "if", "false"); + controlFlowElseIfTest.createCommand(undefined, "echo", "foo"); + controlFlowElseIfTest.createCommand(undefined, "elseIf", "true"); + controlFlowElseIfTest.createCommand(undefined, "echo", "bar"); + controlFlowElseIfTest.createCommand(undefined, "else"); + controlFlowElseIfTest.createCommand(undefined, "echo", "baz"); + controlFlowElseIfTest.createCommand(undefined, "end"); + + const controlFlowElseTest = store.createTestCase("control flow else"); + controlFlowElseTest.createCommand(undefined, "if", "false"); + controlFlowElseTest.createCommand(undefined, "echo", "foo"); + controlFlowElseTest.createCommand(undefined, "elseIf", "false"); + controlFlowElseTest.createCommand(undefined, "echo", "bar"); + controlFlowElseTest.createCommand(undefined, "else"); + controlFlowElseTest.createCommand(undefined, "echo", "baz"); + controlFlowElseTest.createCommand(undefined, "end"); + + const controlFlowDoTest = store.createTestCase("control flow do"); + controlFlowDoTest.createCommand(undefined, "do"); + controlFlowDoTest.createCommand(undefined, "echo", "foo"); + controlFlowDoTest.createCommand(undefined, "repeatIf", "true", "2"); + + const controlFlowTimesTest = store.createTestCase("control flow times"); + controlFlowTimesTest.createCommand(undefined, "times", "2"); + controlFlowTimesTest.createCommand(undefined, "echo", "foo"); + controlFlowTimesTest.createCommand(undefined, "end"); + + const controlFlowWhileTest = store.createTestCase("control flow while"); + controlFlowWhileTest.createCommand(undefined, "while", "true", "2"); + controlFlowWhileTest.createCommand(undefined, "echo", "foo"); + controlFlowWhileTest.createCommand(undefined, "end"); + + const executeScriptSandboxTest = store.createTestCase("execute script"); + executeScriptSandboxTest.createCommand(undefined, "executeScript", "return true", "blah"); + executeScriptSandboxTest.createCommand(undefined, "echo", "${blah}"); + executeScriptSandboxTest.createCommand(undefined, "verify", "${blah}", "false"); + executeScriptSandboxTest.createCommand(undefined, "assert", "${blah}", "true"); + executeScriptSandboxTest.createCommand(undefined, "executeScript", "true"); + executeScriptSandboxTest.createCommand(undefined, "echo", "${blah}"); + const checkTest = store.createTestCase("check"); checkTest.createCommand(undefined, "open", "/checkboxes"); checkTest.createCommand(undefined, "check", "css=input"); @@ -113,16 +163,34 @@ export default function seed(store, numberOfSuites = 5) { submitTest.createCommand(undefined, "submit", "css=#login"); submitTest.createCommand(undefined, "assertElementPresent", "css=.flash.success"); - const smokeSuite = store.createSuite("smoke"); + const suiteControlFlow = store.createSuite("control flow"); + suiteControlFlow.addTestCase(controlFlowIfTest); + suiteControlFlow.addTestCase(controlFlowElseIfTest); + suiteControlFlow.addTestCase(controlFlowElseTest); + suiteControlFlow.addTestCase(controlFlowDoTest); + suiteControlFlow.addTestCase(controlFlowTimesTest); + suiteControlFlow.addTestCase(controlFlowWhileTest); + const suiteAll = store.createSuite("all tests"); store.tests.forEach(function(test) { - smokeSuite.addTestCase(test); + suiteAll.addTestCase(test); }); + const smokeSuite = store.createSuite("smoke"); + smokeSuite.addTestCase(checkTest); + smokeSuite.addTestCase(clickTest); + smokeSuite.addTestCase(clickAtTest); + smokeSuite.addTestCase(executeScriptSandboxTest); + smokeSuite.addTestCase(framesTest); + smokeSuite.addTestCase(selectTest); + smokeSuite.addTestCase(sendKeysTest); + smokeSuite.addTestCase(storeTextTest); + smokeSuite.addTestCase(submitTest); + UiState.changeView("Test suites"); - let suiteState = UiState.getSuiteState(smokeSuite); + let suiteState = UiState.getSuiteState(suiteControlFlow); suiteState.setOpen(true); - UiState.selectTest(checkTest, smokeSuite); + UiState.selectTest(controlFlowIfTest, suiteControlFlow); store.changeName("seed project"); diff --git a/packages/selenium-ide/src/neo/stores/view/PlaybackState.js b/packages/selenium-ide/src/neo/stores/view/PlaybackState.js index 235ef5598..23e862cc6 100644 --- a/packages/selenium-ide/src/neo/stores/view/PlaybackState.js +++ b/packages/selenium-ide/src/neo/stores/view/PlaybackState.js @@ -50,6 +50,7 @@ class PlaybackState { @observable paused = false; @observable delay = 0; @observable callstack = []; + @observable currentExecutingCommandNode = null; constructor() { this.maxDelay = 3000; @@ -208,7 +209,7 @@ class PlaybackState { this.currentRunningTest = this._testsToRun.shift(); this.runningQueue = this.currentRunningTest.commands.peek(); this.clearStack(); - this.currentPlayingIndex = 0; + //this.currentPlayingIndex = 0; this.errors = 0; this.forceTestCaseFailure = false; PluginManager.emitMessage({ @@ -291,9 +292,9 @@ class PlaybackState { this.aborted = true; this._testsToRun = []; if (this.paused) { - this.setCommandStateAtomically(this.runningQueue[this.currentPlayingIndex].id, this.callstack.length ? this.callstack.length - 1 : undefined, PlaybackStates.Fatal, "Playback aborted"); + this.setCommandStateAtomically(this.currentExecutingCommandNode.command.id, this.callstack.length ? this.callstack.length - 1 : undefined, PlaybackStates.Fatal, "Playback aborted"); } else { - this.setCommandStateAtomically(this.runningQueue[this.currentPlayingIndex].id, this.callstack.length ? this.callstack.length - 1 : undefined, PlaybackStates.Undetermined, "Aborting..."); + this.setCommandStateAtomically(this.currentExecutingCommandNode.command.id, this.callstack.length ? this.callstack.length - 1 : undefined, PlaybackStates.Undetermined, "Aborting..."); } this.stopPlayingGracefully(); } @@ -351,6 +352,10 @@ class PlaybackState { }); } + @action.bound setCurrentExecutingCommandNode(node) { + this.currentExecutingCommandNode = node; + } + @action.bound setPlayingIndex(index) { this.currentPlayingIndex = index; } @@ -369,8 +374,7 @@ class PlaybackState { if (!this.pauseOnExceptions) { this.stopPlayingGracefully(); } else if (this.pauseOnExceptions) { - this.break(this.runningQueue[this.currentPlayingIndex]); - this.setPlayingIndex(this.currentPlayingIndex - 1); + this.break(this.currentExecutingCommandNode.command); } } } diff --git a/packages/selianize/__tests__/command.spec.js b/packages/selianize/__tests__/command.spec.js index d36d78f7f..11d5eb135 100644 --- a/packages/selianize/__tests__/command.spec.js +++ b/packages/selianize/__tests__/command.spec.js @@ -16,7 +16,7 @@ // under the License. import CommandEmitter, { registerEmitter } from "../src/command"; -import { Commands } from "../../selenium-ide/src/neo/models/Command"; +import { Commands, ControlFlowCommandNames } from "../../selenium-ide/src/neo/models/Command"; describe("command code emitter", () => { it("should skip empty commands", () => { @@ -690,4 +690,88 @@ describe("command code emitter", () => { const snapshot = "this commands snapshot"; return expect(CommandEmitter.emit(command, undefined, snapshot)).resolves.toBe(snapshot); }); + it("should emit `if` command", () => { + const command = { + command: ControlFlowCommandNames.if, + target: "true", + value: "" + }; + return expect(CommandEmitter.emit(command)).resolves.toBe(`if (${command.target}) {`); + }); + it("should emit `else if` command", () => { + const command = { + command: ControlFlowCommandNames.elseIf, + target: "true", + value: "" + }; + return expect(CommandEmitter.emit(command)).resolves.toBe(`} else if (${command.target}) {`); + }); + it("should emit `else` command", () => { + const command = { + command: ControlFlowCommandNames.else, + target: "true", + value: "" + }; + return expect(CommandEmitter.emit(command)).resolves.toBe("} else {"); + }); + it("should emit `times` command", () => { + const command = { + command: ControlFlowCommandNames.times, + target: "5", + value: "" + }; + return expect(CommandEmitter.emit(command)).resolves.toBe(`const times = ${command.target};for(let i = 0; i < times; i++) {`); + }); + it("should emit `while` command", () => { + const command = { + command: ControlFlowCommandNames.while, + target: "true", + value: "" + }; + return expect(CommandEmitter.emit(command)).resolves.toBe(`while (${command.target}) {`); + }); + it("should emit `end` command", () => { + const command = { + command: ControlFlowCommandNames.end, + target: "", + value: "" + }; + return expect(CommandEmitter.emit(command)).resolves.toBe("}"); + }); + it("should emit `do` command", () => { + const command = { + command: ControlFlowCommandNames.do, + target: "", + value: "" + }; + return expect(CommandEmitter.emit(command)).resolves.toBe("do {"); + }); + it("should emit `repeatIf` command", () => { + const command = { + command: ControlFlowCommandNames.repeatIf, + target: "true", + value: "" + }; + return expect(CommandEmitter.emit(command)).resolves.toBe(`} while(${command.target});`); + }); + it("should emit `assert` command", () => { + const command = { + command: "assert", + target: "${varrrName}", + value: "true" + }; + return expect(CommandEmitter.emit(command)).resolves.toBe( + `expect(vars.varrrName === ${command.value});` + ); + }); + it("should emit `verify` command", () => { + const command = { + command: "verify", + target: "${varrrName}", + value: "true" + }; + return expect(CommandEmitter.emit(command)).resolves.toBe( + `expect(vars.varrrName === ${command.value});` + ); + }); }); diff --git a/packages/selianize/src/command.js b/packages/selianize/src/command.js index f2addefa9..eb94ad671 100644 --- a/packages/selianize/src/command.js +++ b/packages/selianize/src/command.js @@ -95,7 +95,17 @@ const emitters = { chooseCancelOnNextConfirmation: skip, chooseCancelOnNextPrompt: skip, chooseOkOnNextConfirmation: skip, - setSpeed: skip + setSpeed: skip, + do: emitControlFlowDo, + else: emitControlFlowElse, + elseIf: emitControlFlowElseIf, + end: emitControlFlowEnd, + if: emitControlFlowIf, + repeatIf: emitControlFlowRepeatIf, + times: emitControlFlowTimes, + while: emitControlFlowWhile, + assert: emitAssert, + verify: emitAssert }; export function emit(command, options = config, snapshot) { @@ -350,3 +360,39 @@ async function emitSubmit(locator) { function skip() { return Promise.resolve(); } + +function emitControlFlowDo() { + return Promise.resolve("do {"); +} + +function emitControlFlowElse() { + return Promise.resolve("} else {"); +} + +function emitControlFlowElseIf(target) { + return Promise.resolve(`} else if (${target}) {`); +} + +function emitControlFlowEnd() { + return Promise.resolve("}"); +} + +function emitControlFlowIf(target) { + return Promise.resolve(`if (${target}) {`); +} + +function emitControlFlowRepeatIf(target) { + return Promise.resolve(`} while(${target});`); +} + +function emitControlFlowTimes(target) { + return Promise.resolve(`const times = ${target};for(let i = 0; i < times; i++) {`); +} + +function emitControlFlowWhile(target) { + return Promise.resolve(`while (${target}) {`); +} + +function emitAssert(varName, value) { + return Promise.resolve(`expect(${varName.replace(/\$\{/, "").replace(/\}/, "")} === ${value});`); +}