From 4d849d160020f3a6b79820ae2abf818ee0656fd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20Egl=C4=ABtis?= Date: Tue, 7 Nov 2023 15:59:52 +0200 Subject: [PATCH 1/2] feat: add .NET NUnit recorder --- .../lib/client-frameworks/dotnet-nunit.js | 354 ++++++++++++++++++ app/renderer/lib/client-frameworks/index.js | 2 + 2 files changed, 356 insertions(+) create mode 100644 app/renderer/lib/client-frameworks/dotnet-nunit.js diff --git a/app/renderer/lib/client-frameworks/dotnet-nunit.js b/app/renderer/lib/client-frameworks/dotnet-nunit.js new file mode 100644 index 000000000..615468bdf --- /dev/null +++ b/app/renderer/lib/client-frameworks/dotnet-nunit.js @@ -0,0 +1,354 @@ +import Framework from './framework'; +import _ from 'lodash'; + +class DotNetNUnitFramework extends Framework { + + get language () { + return 'csharp'; + } + + wrapWithBoilerplate (code) { + let [pkg, cls] = (() => { + if (this.caps.platformName) { + switch (this.caps.platformName.toLowerCase()) { + case 'ios': return ['iOS', 'IOSDriver']; + case 'android': return ['Android', 'AndroidDriver']; + case 'windows': return ['Windows', 'WindowsDriver']; + case 'mac2': return ['Mac', 'MacDriver']; + default: return ['unknownPlatform', 'UnknownDriver']; + } + } else { + return ['unknownPlatform', 'UnknownDriver']; + } + })(); + let capStr = this.indent(_.map(this.caps, (v, k) => + `options.AddAdditionalAppiumOption(${JSON.stringify(k)}, ${JSON.stringify(v)})` + ).join('\n'), 8); + + return `// This sample code supports Appium .NET client >=5 +// https://github.com/appium/dotnet-client +using System; +using System.Drawing; +using NUnit.Framework; +using OpenQA.Selenium; +using OpenQA.Selenium.Interactions; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.${pkg}; +using OpenQA.Selenium.Appium.Enums; + +namespace AppiumTests; + +public class Tests +{ + private ${cls} _driver; + + [OneTimeSetUp] + public void SetUp() + { + var serverUri = new Uri("${this.serverUrl}"); + var options = new AppiumOptions(); +${capStr} + + _driver = new ${cls}(serverUri, options); + } + + [OneTimeTearDown] + public void TearDown() + { + _driver.Dispose(); + } + + [Test] + public void SampleTest() + { +${this.indent(code, 8)} + } +} +`; + } + + addComment(comment) { + return `// ${comment}`; + } + + codeFor_findAndAssign (strategy, locator, localVar, isArray) { + let suffixMap = { + xpath: 'XPath', + 'accessibility id': 'AccessibilityId', + id: 'Id', + 'class name': 'ClassName', + name: 'Name', + '-android uiautomator': 'AndroidUIAutomator', + '-android datamatcher': 'AndroidDataMatcher', + // '-android viewtag': 'not supported', + '-ios predicate string': 'IosNsPredicate', + '-ios class chain': 'IosClassChain', + }; + if (!suffixMap[strategy]) { + return this.handleUnsupportedLocatorStrategy(strategy, locator); + } + if (isArray) { + return `var ${localVar} = _driver.FindElements(MobileBy.${suffixMap[strategy]}(${JSON.stringify(locator)}));`; + } else { + return `var ${localVar} = _driver.FindElement(MobileBy.${suffixMap[strategy]}(${JSON.stringify(locator)}));`; + } + } + + codeFor_click (varName, varIndex) { + return `${this.getVarName(varName, varIndex)}.Click();`; + } + + codeFor_clear (varName, varIndex) { + return `${this.getVarName(varName, varIndex)}.Clear();`; + } + + codeFor_sendKeys (varName, varIndex, text) { + return `${this.getVarName(varName, varIndex)}.SendKeys(${JSON.stringify(text)});`; + } + + codeFor_tap (varNameIgnore, varIndexIgnore, pointerActions) { + const {x, y} = this.getTapCoordinatesFromPointerActions(pointerActions); + return ` +var finger = new PointerInputDevice(PointerKind.Touch); +var tapPoint = new Point(${x}, ${y}); +var tap = new ActionSequence(finger); +tap.AddAction(finger.CreatePointerMove(CoordinateOrigin.Viewport, tapPoint.X, tapPoint.Y, TimeSpan.Zero)); +tap.AddAction(finger.CreatePointerDown(MouseButton.Left)); +tap.AddAction(new PauseInteraction(finger, TimeSpan.FromMilliseconds(50))); +tap.AddAction(finger.CreatePointerUp(MouseButton.Left)); +driver.PerformActions(new List { tap }); +`; + } + + codeFor_swipe (varNameIgnore, varIndexIgnore, pointerActions) { + const {x1, y1, x2, y2} = this.getSwipeCoordinatesFromPointerActions(pointerActions); + return ` +var finger = new PointerInputDevice(PointerKind.Touch); +var start = new Point(${x1}, ${y1}); +var end = new Point(${x2}, ${y2}); +var swipe = new ActionSequence(finger); +swipe.AddAction(finger.CreatePointerMove(CoordinateOrigin.Viewport, start.X, start.Y, TimeSpan.Zero)); +swipe.AddAction(finger.CreatePointerDown(MouseButton.Left)); +swipe.AddAction(finger.CreatePointerMove(CoordinateOrigin.Viewport, end.X, end.Y, TimeSpan.FromMilliseconds(1000))); +swipe.AddAction(finger.CreatePointerUp(MouseButton.Left)); +driver.PerformActions(new List { swipe }); +`; + } + + // Execute Script + + codeFor_executeScriptNoArgs (scriptCmd) { + return `_driver.ExecuteScript(${JSON.stringify(scriptCmd)});`; + } + + codeFor_executeScriptWithArgs (scriptCmd, jsonArg) { + // change the JSON object into a format accepted by C# Dictionary: a sequence of tuples + // first create an array for each key-value pair + const argsValuesArray = _.toPairs(jsonArg[0]); + // then wrap each key-value array in curly brackets + const argsValuesStrings = argsValuesArray.map((kv) => `{${JSON.stringify(kv).slice(1, -1)}}`); + return `_driver.ExecuteScript(${JSON.stringify(scriptCmd)}, new Dictionary {${argsValuesStrings.join(', ')}});`; + } + + // App Management + + codeFor_getCurrentActivity () { + return `var activityName = ${this.codeFor_executeScriptNoArgs('mobile: getCurrentActivity')}`; + } + + codeFor_getCurrentPackage () { + return `var packageName = ${this.codeFor_executeScriptNoArgs('mobile: getCurrentPackage')}`; + } + + codeFor_installApp (varNameIgnore, varIndexIgnore, app) { + return `_driver.InstallApp("${app}");`; + } + + codeFor_isAppInstalled (varNameIgnore, varIndexIgnore, app) { + return `var isAppInstalled = _driver.IsAppInstalled("${app}");`; + } + + codeFor_activateApp (varNameIgnore, varIndexIgnore, app) { + return `_driver.ActivateApp("${app}");`; + } + + codeFor_terminateApp (varNameIgnore, varIndexIgnore, app) { + return `_driver.TerminateApp("${app}");`; + } + + codeFor_removeApp (varNameIgnore, varIndexIgnore, app) { + return `_driver.RemoveApp("${app}")`; + } + + codeFor_getStrings (varNameIgnore, varIndexIgnore, language, stringFile) { + return `var appStrings = _driver.GetAppStringDictionary(${language ? `"${language}", ` : ''}${stringFile ? `"${stringFile}"` : ''});`; + } + + // Clipboard + + codeFor_getClipboard () { + return `var clipboardText = _driver.GetClipboardText();`; + } + + codeFor_setClipboard (varNameIgnore, varIndexIgnore, clipboardText) { + return `_driver.SetClipboardText("${clipboardText}");`; + } + + // File Transfer + + codeFor_pushFile (varNameIgnore, varIndexIgnore, pathToInstallTo, fileContentString) { + return `_driver.PushFile("${pathToInstallTo}", "${fileContentString}");`; + } + + codeFor_pullFile (varNameIgnore, varIndexIgnore, pathToPullFrom) { + return `var fileBase64 = _driver.PullFile("${pathToPullFrom}");`; + } + + codeFor_pullFolder (varNameIgnore, varIndexIgnore, folderToPullFrom) { + return `var folderBase64 = _driver.PullFolder("${folderToPullFrom}");`; + } + + // Device Interaction + + codeFor_isLocked () { + return `let isLocked = ${this.codeFor_executeScriptNoArgs('mobile: isLocked')}`; + } + + codeFor_rotateDevice () { + return `// Not supported: rotateDevice`; + } + + codeFor_touchId (varNameIgnore, varIndexIgnore, match) { + return `_driver.PerformTouchID(${match});`; + } + + codeFor_toggleEnrollTouchId () { + return `// Not supported: toggleEnrollTouchId`; + } + + // Keyboard + + codeFor_isKeyboardShown () { + return `let isKeyboardShown = _driver.IsKeyboardShown();`; + } + + // Connectivity + + codeFor_toggleAirplaneMode () { + return `_driver.ToggleAirplaneMode();`; + } + + codeFor_toggleData () { + return `_driver.ToggleData();`; + } + + codeFor_toggleWiFi () { + return `_driver.ToggleWiFi();`; + } + + codeFor_sendSMS (varNameIgnore, varIndexIgnore, phoneNumber, text) { + return `_driver.SendSms("${phoneNumber}", "${text}");`; + } + + codeFor_gsmCall (varNameIgnore, varIndexIgnore, phoneNumber, action) { + return `_driver.MakeGsmCall("${phoneNumber}", "${action}");`; + } + + codeFor_gsmSignal (varNameIgnore, varIndexIgnore, signalStrength) { + return `_driver.SetGsmSignalStrength("${signalStrength}");`; + } + + codeFor_gsmVoice (varNameIgnore, varIndexIgnore, state) { + return `_driver.SetGsmVoice("${state}");`; + } + + // Session + + codeFor_getSession () { + return `let sessionDetails = _driver.SessionDetails;`; + } + + codeFor_setTimeouts (/*varNameIgnore, varIndexIgnore, timeoutsJson*/) { + return '/* TODO implement setTimeouts */'; + } + + codeFor_getOrientation () { + return `let orientation = _driver.Orientation;`; + } + + codeFor_setOrientation (varNameIgnore, varIndexIgnore, orientation) { + return `_driver.Orientation = "${orientation}";`; + } + + codeFor_getGeoLocation () { + return `let location = _driver.Location;`; + } + + codeFor_setGeoLocation (varNameIgnore, varIndexIgnore, latitude, longitude, altitude) { + return `_driver.Location = new Location { Latitude = ${latitude}, Longitude = ${longitude}, Altitude = ${altitude} };`; + } + + codeFor_getLogTypes () { + return `let logTypes = _driver.Manage().Logs.AvailableLogTypes;`; + } + + codeFor_getLogs (varNameIgnore, varIndexIgnore, logType) { + return `let logs = _driver.Manage().Logs.GetLog("${logType}");`; + } + + codeFor_updateSettings (varNameIgnore, varIndexIgnore, settingsJson) { + try { + let settings = []; + for (let [settingName, settingValue] of _.toPairs(settingsJson)) { + settings.push(`_driver.SetSetting("${settingName}", ${JSON.stringify(settingValue)});`); + } + return settings.join('\n'); + } catch (e) { + return `// Could not parse: ${settingsJson}`; + } + } + + codeFor_getSettings () { + return `let settings = _driver.Settings();`; + } + + // Web + + codeFor_navigateTo (varNameIgnore, varIndexIgnore, url) { + return `_driver.Url = "${url}";`; + } + + codeFor_getUrl () { + return `var currentUrl = _driver.Url;`; + } + + codeFor_back () { + return `_driver.Navigate().Back();`; + } + + codeFor_forward () { + return `_driver.Navigate().Forward();`; + } + + codeFor_refresh () { + return `_driver.Navigate().Refresh();`; + } + + // Context + + codeFor_getContext () { + return `var context = _driver.Context;`; + } + + codeFor_getContexts () { + return `var contexts = _driver.Contexts;`; + } + + codeFor_switchContext (varNameIgnore, varIndexIgnore, name) { + return `_driver.Context = "${name}";`; + } +} + +DotNetNUnitFramework.readableName = '.NET - NUnit'; + +export default DotNetNUnitFramework; diff --git a/app/renderer/lib/client-frameworks/index.js b/app/renderer/lib/client-frameworks/index.js index 3d830ce2e..f034f3b2e 100644 --- a/app/renderer/lib/client-frameworks/index.js +++ b/app/renderer/lib/client-frameworks/index.js @@ -1,3 +1,4 @@ +import DotNetNUnitFramework from './dotnet-nunit'; import JsWdIoFramework from './js-wdio'; import JsOxygenFramework from './js-oxygen'; import JavaFramework from './java'; @@ -6,6 +7,7 @@ import RubyFramework from './ruby'; import RobotFramework from './robot'; const frameworks = { + dotNetNUnit: DotNetNUnitFramework, jsWdIo: JsWdIoFramework, jsOxygen: JsOxygenFramework, java: JavaFramework, From b328d04c3e81737b560d0e8df29c3ba0c7caa671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgars=20Egl=C4=ABtis?= Date: Tue, 7 Nov 2023 16:20:55 +0200 Subject: [PATCH 2/2] fix: handle parsing of lists and dictionaries --- .../lib/client-frameworks/dotnet-nunit.js | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/app/renderer/lib/client-frameworks/dotnet-nunit.js b/app/renderer/lib/client-frameworks/dotnet-nunit.js index 615468bdf..f9abd9db5 100644 --- a/app/renderer/lib/client-frameworks/dotnet-nunit.js +++ b/app/renderer/lib/client-frameworks/dotnet-nunit.js @@ -7,6 +7,19 @@ class DotNetNUnitFramework extends Framework { return 'csharp'; } + getCSharpVal (jsonVal) { + if (Array.isArray(jsonVal)) { + const convertedItems = jsonVal.map((item) => this.getCSharpVal(item)); + return `{${convertedItems.join(', ')}}`; + } else if (typeof jsonVal === 'object') { + const convertedItems = _.map(jsonVal, (v, k) => + `{${JSON.stringify(k)}, ${this.getCSharpVal(v)}}` + ); + return `new Dictionary {${convertedItems.join(', ')}}`; + } + return JSON.stringify(jsonVal); + } + wrapWithBoilerplate (code) { let [pkg, cls] = (() => { if (this.caps.platformName) { @@ -22,7 +35,7 @@ class DotNetNUnitFramework extends Framework { } })(); let capStr = this.indent(_.map(this.caps, (v, k) => - `options.AddAdditionalAppiumOption(${JSON.stringify(k)}, ${JSON.stringify(v)})` + `options.AddAdditionalAppiumOption(${JSON.stringify(k)}, ${this.getCSharpVal(v)})` ).join('\n'), 8); return `// This sample code supports Appium .NET client >=5 @@ -142,12 +155,8 @@ driver.PerformActions(new List { swipe }); } codeFor_executeScriptWithArgs (scriptCmd, jsonArg) { - // change the JSON object into a format accepted by C# Dictionary: a sequence of tuples - // first create an array for each key-value pair - const argsValuesArray = _.toPairs(jsonArg[0]); - // then wrap each key-value array in curly brackets - const argsValuesStrings = argsValuesArray.map((kv) => `{${JSON.stringify(kv).slice(1, -1)}}`); - return `_driver.ExecuteScript(${JSON.stringify(scriptCmd)}, new Dictionary {${argsValuesStrings.join(', ')}});`; + // C# Dictionary accepts a sequence of tuples + return `_driver.ExecuteScript(${JSON.stringify(scriptCmd)}, ${this.getCSharpVal(jsonArg[0])});`; } // App Management @@ -300,7 +309,7 @@ driver.PerformActions(new List { swipe }); try { let settings = []; for (let [settingName, settingValue] of _.toPairs(settingsJson)) { - settings.push(`_driver.SetSetting("${settingName}", ${JSON.stringify(settingValue)});`); + settings.push(`_driver.SetSetting("${settingName}", ${this.getCSharpVal(settingValue)});`); } return settings.join('\n'); } catch (e) {