feat: add Python pytest Appium script generator#57
feat: add Python pytest Appium script generator#57mimosa767 wants to merge 3 commits intokobiton:masterfrom
Conversation
src/services/python.js
Outdated
|
|
||
| const configPath = path.join(outputDir, 'config.py') | ||
| let configContent = await readFile(configPath, 'utf8') | ||
| const desiredCapsCode = buildCode(desiredCapsMethodLines, 4) |
There was a problem hiding this comment.
This will crash at runtime. buildCode (in models/line.js) expects a single object arg {language, lines, initialIndent}, but here it receives positional args.
For reference, here is how Java does it (java.js:704-705):
_buildJavaCode(lines, initialIndent) {
return buildCode({language: LANGUAGES.JAVA, initialIndent, lines})
}You could add a similar helper, something like:
_buildPythonCode(lines, initialIndent) {
return buildCode({language: LANGUAGES.PYTHON, initialIndent, lines})
}Also, LANGUAGES.PYTHON needs a case in line.js's buildCode switch to set space = ' ' (4 spaces). Without it, the generated Python code will have no indentation, which is a syntax error in Python.
Same issue on lines 303 and 309.
There was a problem hiding this comment.
Thanks for the thorough review @chuong777 — really helpful feedback. I've pushed a fix commit (b87105d) that addresses all of your comments:
buildCodesignature — fixed to use{language, initialIndent, lines}object form via a new_buildPythonCodehelper, and addedLANGUAGES.PYTHONcase toline.jswith 4-space indentation_getLocatorCodemissing — implemented with(AppiumBy.X, "value")tuple format for Python, with full selector type support and multi-device if/elif/else branchingcompresssignature — fixed tocompress([{source: outputDir, name: false, type: 'dir'}], outputFile)serverInfoproperties — fixed to deriveappiumServerUrlfromserverInfo.apiUrlvianew URL(), consistent with Java/Node.js generatorspressButton→press— corrected command name, now readsaction.valueand supportsaction.countfor repeated pressessetText→sendKeys— corrected command name, now readsaction.valuewith single-quote escaping via_getString()swipeFromElement— now hides keyboard and finds element first, consistent with Java/C# generatorsdefaultcase — now throws instead of silently skipping unknown commands_generateTestCaseLines— now receivesappUnderTestanddeviceSource, passesapp_urlfor non-Kobiton sourcespytest-asyncio— removed from requirements.txt
Let me know if anything else needs attention!
| if (hasSelector) { | ||
| locatorVarName = this._getLocatorVarName(step, locatorVarNames) | ||
| rawLocatorVarName = locatorVarName.replace(LOCATOR_VAR_NAME_PREFIX, '') | ||
| const locatorCode = this._getLocatorCode({step, locatorVarName}) |
There was a problem hiding this comment.
_getLocatorCode is called here but not defined in this class or in BaseAppiumScriptGenerator. Each language generator needs its own implementation — see java.js:627, nodejs.js:527, or csharp.js:581 for examples.
For Python, it would need to produce something like (AppiumBy.XPATH, "//...") tuples instead of Java's MobileBy.xpath("...") syntax.
Without this method, any test step that has element selectors will throw TypeError: this._getLocatorCode is not a function.
src/services/python.js
Outdated
| } | ||
|
|
||
| const outputFile = path.join(workingDir, 'python.zip') | ||
| await compress(outputDir, outputFile) |
There was a problem hiding this comment.
The compress utility expects an array of objects as the first argument, not a plain string. This will produce a corrupted zip or throw.
Java's pattern (java.js:616):
await compress([{source: compressedDir, name: false, type: 'dir'}], outputFile)Suggested fix:
await compress([{source: outputDir, name: false, type: 'dir'}], outputFile)
src/services/python.js
Outdated
| await createDir(outputDir) | ||
| await ncpAsync(templateDir, outputDir) | ||
|
|
||
| const {portalUrl, kobitonApiUrl, appiumServerUrl, username} = serverInfo |
There was a problem hiding this comment.
kobitonApiUrl and appiumServerUrl don't exist as properties on serverInfo. In the other generators, the object has apiUrl, portalUrl, and username.
appiumServerUrl is computed locally from apiUrl — see java.js:538-539:
const kobitonApiUrl = new URL(serverInfo.apiUrl)
appiumServerUrl = `"${kobitonApiUrl.protocol}//${kobitonApiUrl.host}/wd/hub"`As-is, both values will be undefined and the generated config.py will have empty server URLs.
src/services/python.js
Outdated
| lines.push(new Line(`self.swipe(${x1}, ${y1}, ${x2}, ${y2}, ${duration || 800})`)) | ||
| } break | ||
|
|
||
| case 'pressButton': { |
There was a problem hiding this comment.
The command name here should be press (not pressButton) to match the actual command data. All other generators (Java, Node.js, C#) use case 'press'.
Also, Java's press command reads action.value (not action.buttonType) and supports a count parameter for repeated presses:
case 'press': {
const {value} = action
const count = action.count || 1
...
}
src/services/python.js
Outdated
| lines.push(new Line(`self.press_button('${buttonType}')`)) | ||
| } break | ||
|
|
||
| case 'setText': { |
There was a problem hiding this comment.
Similar to above — this should be sendKeys (not setText) to match the command name used by all other generators. The action property is action.value, not action.text.
Also, Java's sendKeys doesn't find an element first — it just calls sendKeys(value) directly. The element finding + send_keys pattern here has different semantics.
One more thing: the string text isn't escaped, so values containing single quotes will produce a Python syntax error in the generated code. Java uses this._getString(value) for escaping.
| lines.push(new Line('self.idle()')) | ||
| } break | ||
|
|
||
| default: |
There was a problem hiding this comment.
Java throws here instead of silently breaking:
default:
throw new Error(`Not support command = ${actionCommand}`)Silently skipping means generated test scripts will be missing steps without any warning. Consider throwing so the caller knows the script is incomplete, at least until all commands are implemented.
| lines.push(new Line(`self.touch_at_relative_point(${x}, ${y})`)) | ||
| } break | ||
|
|
||
| case 'swipeFromElement': { |
There was a problem hiding this comment.
Nit: Java's swipeFromElement (java.js:346-354) does three things that are skipped here:
- Hides keyboard if
!isOnKeyboard - Finds the element via
findVisibleElement - Swipes relative to the element via
swipeOnElement
The current code does a raw screen swipe ignoring the element. This is fine as a simplification for v1 if documented, but worth noting the difference.
src/services/python.js
Outdated
| return lines | ||
| } | ||
|
|
||
| _generateTestCaseLines({devices}) { |
There was a problem hiding this comment.
Nit: Java's _generateTestCaseLines also receives appUnderTest, testingFramework, and deviceSource. These are needed for non-Kobiton device sources (SauceLabs, BrowserStack) where getAppUrl(appVersionId) is called and the result passed to the desired caps method.
As-is, the generated test code always calls Config.${desiredCapsMethodName}() without arguments (line 152), but for non-Kobiton sources the method signature expects an app_url parameter (line 97). This would cause a TypeError at test runtime.
| @@ -0,0 +1,4 @@ | |||
| Appium-Python-Client==3.1.0 | |||
| pytest==7.4.0 | |||
| pytest-asyncio==0.21.0 | |||
There was a problem hiding this comment.
Nit: pytest-asyncio doesn't appear to be used — there's no async code in the templates. Could be removed to keep dependencies minimal.
Summary
Adds Python (pytest) as a new supported language for the Appium script generator.
Types of changes
Changes
src/services/python.js— new Python script generator servicesrc/services/constant.js— added PYTHON to LANGUAGES and PYTEST to FRAMEWORK_NAMESsrc/services/index.js— registered PythonAppiumScriptGeneratorsrc/templates/python/— 11 new Python template files including config, test base, test suite, proxy server, OTP service, utils, and README