Skip to content

feat: add Python pytest Appium script generator#57

Open
mimosa767 wants to merge 3 commits intokobiton:masterfrom
mimosa767:feature/python-pytest-generator
Open

feat: add Python pytest Appium script generator#57
mimosa767 wants to merge 3 commits intokobiton:masterfrom
mimosa767:feature/python-pytest-generator

Conversation

@mimosa767
Copy link

Summary

Adds Python (pytest) as a new supported language for the Appium script generator.

Types of changes

  • New feature

Changes

  • src/services/python.js — new Python script generator service
  • src/services/constant.js — added PYTHON to LANGUAGES and PYTEST to FRAMEWORK_NAMES
  • src/services/index.js — registered PythonAppiumScriptGenerator
  • src/templates/python/ — 11 new Python template files including config, test base, test suite, proxy server, OTP service, utils, and README


const configPath = path.join(outputDir, 'config.py')
let configContent = await readFile(configPath, 'utf8')
const desiredCapsCode = buildCode(desiredCapsMethodLines, 4)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the thorough review @chuong777 — really helpful feedback. I've pushed a fix commit (b87105d) that addresses all of your comments:

  • buildCode signature — fixed to use {language, initialIndent, lines} object form via a new _buildPythonCode helper, and added LANGUAGES.PYTHON case to line.js with 4-space indentation
  • _getLocatorCode missing — implemented with (AppiumBy.X, "value") tuple format for Python, with full selector type support and multi-device if/elif/else branching
  • compress signature — fixed to compress([{source: outputDir, name: false, type: 'dir'}], outputFile)
  • serverInfo properties — fixed to derive appiumServerUrl from serverInfo.apiUrl via new URL(), consistent with Java/Node.js generators
  • pressButtonpress — corrected command name, now reads action.value and supports action.count for repeated presses
  • setTextsendKeys — corrected command name, now reads action.value with single-quote escaping via _getString()
  • swipeFromElement — now hides keyboard and finds element first, consistent with Java/C# generators
  • default case — now throws instead of silently skipping unknown commands
  • _generateTestCaseLines — now receives appUnderTest and deviceSource, passes app_url for non-Kobiton sources
  • pytest-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})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_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.

}

const outputFile = path.join(workingDir, 'python.zip')
await compress(outputDir, outputFile)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

await createDir(outputDir)
await ncpAsync(templateDir, outputDir)

const {portalUrl, kobitonApiUrl, appiumServerUrl, username} = serverInfo
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

lines.push(new Line(`self.swipe(${x1}, ${y1}, ${x2}, ${y2}, ${duration || 800})`))
} break

case 'pressButton': {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
  ...
}

lines.push(new Line(`self.press_button('${buttonType}')`))
} break

case 'setText': {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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': {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Java's swipeFromElement (java.js:346-354) does three things that are skipped here:

  1. Hides keyboard if !isOnKeyboard
  2. Finds the element via findVisibleElement
  3. 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.

return lines
}

_generateTestCaseLines({devices}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: pytest-asyncio doesn't appear to be used — there's no async code in the templates. Could be removed to keep dependencies minimal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants